refactor(compiler): extract call and construct signatures of interfaces (#54053)

This adds initial support for extracting and rendering call and construct
signatures of classes, like within the new `InputFunction` for signal
inputs.

For now, signatures are a rare occasion and represented as class member
entries. In the future we might consider exposing this via its own entry
type, and field on the class/interface entry.

PR Close #54053
This commit is contained in:
Paul Gschwendtner
2024-01-23 17:28:00 +00:00
committed by Jessica Janiuk
parent 58b8a232d6
commit fe4343cf13
6 changed files with 81 additions and 26 deletions
+1 -1
View File
@@ -152,7 +152,7 @@
"@actions/core": "^1.10.0",
"@angular-devkit/architect-cli": "^0.1701.0-rc",
"@angular/animations": "^17.1.0-next",
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#8a32f9fd9af99832f337db9fe29842797121e96d",
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#e97da496dc89481ef8e91433f2a1b674a2360340",
"@angular/docs": "https://github.com/angular/dev-infra-private-docs-builds.git#1ff2479610f1f145433b95bbe85f6606e6e975c1",
"@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#cdbbb64c01e02d4edda03d66fa0919504d8c5dfc",
"@babel/helper-remap-async-to-generator": "^7.18.9",
@@ -30,13 +30,20 @@ type PropertyDeclarationLike = ts.PropertyDeclaration|ts.AccessorDeclaration;
/** Type representing either a class declaration ro an interface declaration. */
type ClassDeclarationLike = ts.ClassDeclaration|ts.InterfaceDeclaration;
/** Type representing either a class member node or an interface member node. */
/** Type representing either a class or interface member. */
type MemberElement = ts.ClassElement|ts.TypeElement;
/** Type representing either a class method declaration or an interface method signature. */
/** Type representing a signature element of an interface. */
type SignatureElement = ts.CallSignatureDeclaration|ts.ConstructSignatureDeclaration;
/**
* Type representing either:
*/
type MethodLike = ts.MethodDeclaration|ts.MethodSignature;
/** Type representing either a class property declaration or an interface property signature. */
/**
* Type representing either a class property declaration or an interface property signature.
*/
type PropertyLike = PropertyDeclarationLike|ts.PropertySignature;
/** Extractor to pull info for API reference documentation for a TypeScript class or interface. */
@@ -53,7 +60,7 @@ class ClassExtractor {
isAbstract: this.isAbstract(),
entryType: ts.isInterfaceDeclaration(this.declaration) ? EntryType.Interface :
EntryType.UndecoratedClass,
members: this.extractAllClassMembers(),
members: this.extractSignatures().concat(this.extractAllClassMembers()),
generics: extractGenerics(this.declaration),
description: extractJsDocDescription(this.declaration),
jsdocTags: extractJsDocTags(this.declaration),
@@ -92,9 +99,15 @@ class ClassExtractor {
return undefined;
}
/** Extract docs for all call signatures in the current class/interface. */
protected extractSignatures(): MemberEntry[] {
return this.computeAllSignatureDeclarations().map(s => this.extractSignature(s));
}
/** Extracts docs for a class method. */
protected extractMethod(methodDeclaration: MethodLike): MethodEntry {
const functionExtractor = new FunctionExtractor(methodDeclaration, this.typeChecker);
const functionExtractor = new FunctionExtractor(
methodDeclaration.name.getText(), methodDeclaration, this.typeChecker);
return {
...functionExtractor.extract(),
memberType: MemberType.Method,
@@ -102,6 +115,20 @@ class ClassExtractor {
};
}
/** Extracts docs for a signature element (usually inside an interface). */
protected extractSignature(signature: SignatureElement): MethodEntry {
// No name for the function if we are dealing with call signatures.
// For construct signatures we are using `new` as the name of the function for now.
// TODO: Consider exposing a new entry type for signature types.
const functionExtractor = new FunctionExtractor(
ts.isConstructSignatureDeclaration(signature) ? 'new' : '', signature, this.typeChecker);
return {
...functionExtractor.extract(),
memberType: MemberType.Method,
memberTags: [],
};
}
/** Extracts doc info for a property declaration. */
protected extractClassProperty(propertyDeclaration: PropertyLike): PropertyEntry {
return {
@@ -137,16 +164,35 @@ class ClassExtractor {
return tags;
}
/** Computes all signature declarations of the class/interface. */
private computeAllSignatureDeclarations(): SignatureElement[] {
const type = this.typeChecker.getTypeAtLocation(this.declaration);
const signatures = [
...type.getCallSignatures(),
...type.getConstructSignatures(),
];
const result: SignatureElement[] = [];
for (const signature of signatures) {
const decl = signature.getDeclaration();
if (this.isDocumentableSignature(decl) && this.isDocumentableMember(decl)) {
result.push(decl);
}
}
return result;
}
/** Gets all member declarations, including inherited members. */
private getMemberDeclarations(): MemberElement[] {
// We rely on TypeScript to resolve all the inherited members to their
// ultimate form via `getPropertiesOfType`. This is important because child
// ultimate form via `getProperties`. This is important because child
// classes may narrow types or add method overloads.
const type = this.typeChecker.getTypeAtLocation(this.declaration);
const members = type.getProperties();
// While the properties of the declaration type represent the properties that exist
// on a clas *instance*, static members are properties on the class symbol itself.
// on a class *instance*, static members are properties on the class symbol itself.
const typeOfConstructor = this.typeChecker.getTypeOfSymbol(type.symbol);
const staticMembers = typeOfConstructor.getProperties();
@@ -221,6 +267,13 @@ class ClassExtractor {
return ts.isMethodDeclaration(member) || ts.isMethodSignature(member);
}
/** Gets whether the given signature declaration is documentable. */
private isDocumentableSignature(signature: ts.SignatureDeclaration):
signature is SignatureElement {
return ts.isConstructSignatureDeclaration(signature) ||
ts.isCallSignatureDeclaration(signature);
}
/** Gets whether the declaration for this extractor is abstract. */
private isAbstract(): boolean {
const modifiers = this.declaration.modifiers ?? [];
@@ -120,6 +120,7 @@ export interface FunctionEntry extends DocEntry {
params: ParameterEntry[];
returnType: string;
generics: GenericEntry[];
isNewType: boolean;
}
/** Sub-entry for a single class or enum member. */
@@ -66,7 +66,8 @@ export class DocsExtractor {
}
if (ts.isFunctionDeclaration(node)) {
const functionExtractor = new FunctionExtractor(node, this.typeChecker);
// Name is guaranteed to be set, because it's exported directly.
const functionExtractor = new FunctionExtractor(node.name!.getText(), node, this.typeChecker);
return functionExtractor.extract();
}
@@ -107,7 +108,7 @@ export class DocsExtractor {
for (let i = 0; i < declarationCount; i++) {
const [exportName, declaration] = exportedDeclarations[i];
if (ts.isFunctionDeclaration(declaration)) {
const extractor = new FunctionExtractor(declaration, this.typeChecker);
const extractor = new FunctionExtractor(exportName, declaration, this.typeChecker);
const overloads = extractor.getOverloads().map(overload => [exportName, overload] as const);
exportedDeclarations.push(...overloads);
@@ -13,10 +13,13 @@ import {extractGenerics} from './generics_extractor';
import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from './jsdoc_extractor';
import {extractResolvedTypeString} from './type_extractor';
export type FunctionLike = ts.FunctionDeclaration|ts.MethodDeclaration|ts.MethodSignature;
export type FunctionLike = ts.FunctionDeclaration|ts.MethodDeclaration|ts.MethodSignature|
ts.CallSignatureDeclaration|ts.ConstructSignatureDeclaration;
export class FunctionExtractor {
constructor(private declaration: FunctionLike, private typeChecker: ts.TypeChecker) {}
constructor(
private name: string, private declaration: FunctionLike,
private typeChecker: ts.TypeChecker) {}
extract(): FunctionEntry {
// TODO: is there any real situation in which the signature would not be available here?
@@ -28,9 +31,8 @@ export class FunctionExtractor {
return {
params: this.extractAllParams(this.declaration.parameters),
// We know that the function has a name here because we would have skipped it
// already before getting to this point if it was anonymous.
name: this.declaration.name!.getText(),
name: this.name,
isNewType: ts.isConstructSignatureDeclaration(this.declaration),
returnType,
entryType: EntryType.Function,
generics: extractGenerics(this.declaration),
+9 -11
View File
@@ -298,10 +298,9 @@
"@angular/core" "^13.0.0 || ^14.0.0-0"
reflect-metadata "^0.1.13"
"@angular/build-tooling@https://github.com/angular/dev-infra-private-build-tooling-builds.git#8a32f9fd9af99832f337db9fe29842797121e96d":
version "0.0.0-ae9155d1083fd8390e98bfe09a5ccf89f0375b52"
uid "8a32f9fd9af99832f337db9fe29842797121e96d"
resolved "https://github.com/angular/dev-infra-private-build-tooling-builds.git#8a32f9fd9af99832f337db9fe29842797121e96d"
"@angular/build-tooling@https://github.com/angular/dev-infra-private-build-tooling-builds.git#e97da496dc89481ef8e91433f2a1b674a2360340":
version "0.0.0-f0fa701114b57b7d633a2ab3813034ab1735e3c0"
resolved "https://github.com/angular/dev-infra-private-build-tooling-builds.git#e97da496dc89481ef8e91433f2a1b674a2360340"
dependencies:
"@angular-devkit/build-angular" "17.1.0-rc.1"
"@angular/benchpress" "0.3.0"
@@ -332,7 +331,7 @@
marked-mangle "^1.1.4"
preact "^10.17.1"
preact-render-to-string "^6.2.1"
prettier "3.2.2"
prettier "3.2.4"
protractor "^7.0.0"
selenium-webdriver "4.16.0"
send "^0.18.0"
@@ -400,7 +399,6 @@
"@angular/docs@https://github.com/angular/dev-infra-private-docs-builds.git#1ff2479610f1f145433b95bbe85f6606e6e975c1":
version "0.0.0-ae9155d1083fd8390e98bfe09a5ccf89f0375b52"
uid "1ff2479610f1f145433b95bbe85f6606e6e975c1"
resolved "https://github.com/angular/dev-infra-private-docs-builds.git#1ff2479610f1f145433b95bbe85f6606e6e975c1"
dependencies:
"@angular/cdk" "17.1.0-rc.0"
@@ -488,7 +486,6 @@
"@angular/ng-dev@https://github.com/angular/dev-infra-private-ng-dev-builds.git#cdbbb64c01e02d4edda03d66fa0919504d8c5dfc":
version "0.0.0-ae9155d1083fd8390e98bfe09a5ccf89f0375b52"
uid cdbbb64c01e02d4edda03d66fa0919504d8c5dfc
resolved "https://github.com/angular/dev-infra-private-ng-dev-builds.git#cdbbb64c01e02d4edda03d66fa0919504d8c5dfc"
dependencies:
"@yarnpkg/lockfile" "^1.1.0"
@@ -13448,10 +13445,10 @@ prelude-ls@~1.1.2:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
prettier@3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.2.tgz#96e580f7ca9c96090ad054616c0c4597e2844b65"
integrity sha512-HTByuKZzw7utPiDO523Tt2pLtEyK7OibUD9suEJQrPUCYQqrHr74GGX6VidMrovbf/I50mPqr8j/II6oBAuc5A==
prettier@3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.4.tgz#4723cadeac2ce7c9227de758e5ff9b14e075f283"
integrity sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==
prettier@^3.0.0:
version "3.0.3"
@@ -14589,6 +14586,7 @@ select-hose@^2.0.0:
integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==
"selenium-webdriver4@npm:selenium-webdriver@4.16.0", selenium-webdriver@4.16.0:
name selenium-webdriver4
version "4.16.0"
resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.16.0.tgz#2f1a2426d876aa389d1c937b00f034c2c7808360"
integrity sha512-IbqpRpfGE7JDGgXHJeWuCqT/tUqnLvZ14csSwt+S8o4nJo3RtQoE9VR4jB47tP/A8ArkYsh/THuMY6kyRP6kuA==