Skip to content

Commit 7bfe207

Browse files
jelbournatscott
authored andcommitted
feat(compiler): extract api for fn overloads and abtract classes (#52040)
This commit adds support for extracting function overloads. Interestingly, this worked in an earlier version when the code was extracting all statements in every source file, but the existing compiler API for extracting all exported declarations from an entry-point only returns the first function declaration in cases when there are overloads. This also marks abstract classes as abstract, required inputs as required, and filters out Angular-private APIs. PR Close #52040
1 parent 4a75c44 commit 7bfe207

File tree

9 files changed

+321
-52
lines changed

9 files changed

+321
-52
lines changed

packages/compiler-cli/src/ngtsc/docs/src/class_extractor.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {DirectiveMeta, InputMapping, InputOrOutput, MetadataReader, NgModuleMeta
1515
import {ClassDeclaration} from '../../reflection';
1616

1717
import {ClassEntry, DirectiveEntry, EntryType, InterfaceEntry, MemberEntry, MemberTags, MemberType, MethodEntry, PipeEntry, PropertyEntry} from './entities';
18+
import {isAngularPrivateName} from './filters';
1819
import {extractResolvedTypeString} from './type_extractor';
1920

2021
// For the purpose of extraction, we can largely treat properties and accessors the same.
@@ -48,6 +49,7 @@ class ClassExtractor {
4849
extract(): ClassEntry {
4950
return {
5051
name: this.declaration.name.text,
52+
isAbstract: this.isAbstract(),
5153
entryType: ts.isInterfaceDeclaration(this.declaration) ? EntryType.Interface :
5254
EntryType.UndecoratedClass,
5355
members: this.extractAllClassMembers(this.declaration),
@@ -75,7 +77,7 @@ class ClassExtractor {
7577

7678
/** Extract docs for a class's members (methods and properties). */
7779
protected extractClassMember(memberDeclaration: MemberElement): MemberEntry|undefined {
78-
if (this.isMethod(memberDeclaration)) {
80+
if (this.isMethod(memberDeclaration) && !this.isImplementationForOverload(memberDeclaration)) {
7981
return this.extractMethod(memberDeclaration);
8082
} else if (this.isProperty(memberDeclaration)) {
8183
return this.extractClassProperty(memberDeclaration);
@@ -148,6 +150,8 @@ class ClassExtractor {
148150
return MemberTags.Readonly;
149151
case ts.SyntaxKind.ProtectedKeyword:
150152
return MemberTags.Protected;
153+
case ts.SyntaxKind.AbstractKeyword:
154+
return MemberTags.Abstract;
151155
default:
152156
return undefined;
153157
}
@@ -158,11 +162,13 @@ class ClassExtractor {
158162
* This is the case if:
159163
* - The member does not have a name
160164
* - The member is neither a method nor property
161-
* - The member is protected
165+
* - The member is private
166+
* - The member has a name that marks it as Angular-internal.
162167
*/
163168
private isMemberExcluded(member: MemberElement): boolean {
164169
return !member.name || !this.isDocumentableMember(member) ||
165-
!!member.modifiers?.some(mod => mod.kind === ts.SyntaxKind.PrivateKeyword);
170+
!!member.modifiers?.some(mod => mod.kind === ts.SyntaxKind.PrivateKeyword) ||
171+
isAngularPrivateName(member.name.getText());
166172
}
167173

168174
/** Gets whether a class member is a method, property, or accessor. */
@@ -181,6 +187,29 @@ class ClassExtractor {
181187
// Classes have declarations, interface have signatures
182188
return ts.isMethodDeclaration(member) || ts.isMethodSignature(member);
183189
}
190+
191+
/** Gets whether the declaration for this extractor is abstract. */
192+
private isAbstract(): boolean {
193+
const modifiers = this.declaration.modifiers ?? [];
194+
return modifiers.some(mod => mod.kind === ts.SyntaxKind.AbstractKeyword);
195+
}
196+
197+
/** Gets whether a method is the concrete implementation for an overloaded function. */
198+
private isImplementationForOverload(method: MethodLike): boolean {
199+
// Method signatures (in an interface) are never implementations.
200+
if (method.kind === ts.SyntaxKind.MethodSignature) return false;
201+
202+
const methodsWithSameName =
203+
this.declaration.members.filter(member => member.name?.getText() === method.name.getText())
204+
.sort((a, b) => a.pos - b.pos);
205+
206+
// No overloads.
207+
if (methodsWithSameName.length === 1) return false;
208+
209+
// The implementation is always the last declaration, so we know this is the
210+
// implementation if it's the last position.
211+
return method.pos === methodsWithSameName[methodsWithSameName.length - 1].pos;
212+
}
184213
}
185214

186215
/** Extractor to pull info for API reference documentation for an Angular directive. */
@@ -213,6 +242,7 @@ class DirectiveExtractor extends ClassExtractor {
213242
if (inputMetadata) {
214243
entry.memberTags.push(MemberTags.Input);
215244
entry.inputAlias = inputMetadata.bindingPropertyName;
245+
entry.isRequiredInput = inputMetadata.required;
216246
}
217247

218248
const outputMetadata = this.getOutputMetadata(propertyDeclaration);

packages/compiler-cli/src/ngtsc/docs/src/entities.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export enum MemberType {
3434

3535
/** Informational tags applicable to class members. */
3636
export enum MemberTags {
37+
Abstract = 'abstract',
3738
Static = 'static',
3839
Readonly = 'readonly',
3940
Protected = 'protected',
@@ -63,6 +64,7 @@ export interface ConstantEntry extends DocEntry {
6364

6465
/** Documentation entity for a TypeScript class. */
6566
export interface ClassEntry extends DocEntry {
67+
isAbstract: boolean;
6668
members: MemberEntry[];
6769
}
6870

@@ -114,6 +116,7 @@ export interface PropertyEntry extends MemberEntry {
114116
type: string;
115117
inputAlias?: string;
116118
outputAlias?: string;
119+
isRequiredInput?: boolean;
117120
}
118121

119122
/** Sub-entry for a class method. */

packages/compiler-cli/src/ngtsc/docs/src/extractor.ts

Lines changed: 65 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflectio
1616
import {extractClass, extractInterface} from './class_extractor';
1717
import {extractConstant, isSyntheticAngularConstant} from './constant_extractor';
1818
import {DocEntry} from './entities';
19+
import {isAngularPrivateName} from './filters';
1920

21+
type DeclarationWithExportName = readonly[string, ts.Declaration];
2022

2123
/**
2224
* Extracts all information from a source file that may be relevant for generating
@@ -34,51 +36,80 @@ export class DocsExtractor {
3436
extractAll(sourceFile: ts.SourceFile): DocEntry[] {
3537
const entries: DocEntry[] = [];
3638

39+
const exportedDeclarations = this.getExportedDeclarations(sourceFile);
40+
for (const [exportName, node] of exportedDeclarations) {
41+
// Skip any symbols with an Angular-internal name.
42+
if (isAngularPrivateName(exportName)) continue;
43+
44+
const entry = this.extractDeclaration(node);
45+
if (entry) {
46+
// The exported name of an API may be different from its declaration name, so
47+
// use the declaration name.
48+
entries.push({...entry, name: exportName});
49+
}
50+
}
51+
52+
return entries;
53+
}
54+
55+
/** Extract the doc entry for a single declaration. */
56+
private extractDeclaration(node: ts.Declaration): DocEntry|null {
57+
// Ignore anonymous classes.
58+
if (isNamedClassDeclaration(node)) {
59+
return extractClass(node, this.metadataReader, this.typeChecker);
60+
}
61+
62+
if (ts.isInterfaceDeclaration(node)) {
63+
return extractInterface(node, this.typeChecker);
64+
}
65+
66+
if (ts.isFunctionDeclaration(node)) {
67+
const functionExtractor = new FunctionExtractor(node, this.typeChecker);
68+
return functionExtractor.extract();
69+
}
70+
71+
if (ts.isVariableDeclaration(node) && !isSyntheticAngularConstant(node)) {
72+
return extractConstant(node, this.typeChecker);
73+
}
74+
75+
if (ts.isEnumDeclaration(node)) {
76+
return extractEnum(node, this.typeChecker);
77+
}
78+
79+
return null;
80+
}
81+
82+
/** Gets the list of exported declarations for doc extraction. */
83+
private getExportedDeclarations(sourceFile: ts.SourceFile): DeclarationWithExportName[] {
3784
// Use the reflection host to get all the exported declarations from this
3885
// source file entry point.
3986
const reflector = new TypeScriptReflectionHost(this.typeChecker);
4087
const exportedDeclarationMap = reflector.getExportsOfModule(sourceFile);
4188

42-
// Sort the declaration nodes into declaration position because their order is lost in
43-
// reading from the export map. This is primarily useful for testing and debugging.
44-
const exportedDeclarations =
89+
// Augment each declaration with the exported name in the public API.
90+
let exportedDeclarations =
4591
Array.from(exportedDeclarationMap?.entries() ?? [])
46-
.map(([exportName, declaration]) => [exportName, declaration.node] as const)
47-
.sort(([a, declarationA], [b, declarationB]) => declarationA.pos - declarationB.pos);
48-
49-
for (const [exportName, node] of exportedDeclarations) {
50-
let entry: DocEntry|undefined = undefined;
51-
52-
// Ignore anonymous classes.
53-
if (isNamedClassDeclaration(node)) {
54-
entry = extractClass(node, this.metadataReader, this.typeChecker);
55-
}
56-
57-
if (ts.isInterfaceDeclaration(node)) {
58-
entry = extractInterface(node, this.typeChecker);
59-
}
92+
.map(([exportName, declaration]) => [exportName, declaration.node] as const);
6093

61-
if (ts.isFunctionDeclaration(node)) {
62-
const functionExtractor = new FunctionExtractor(node, this.typeChecker);
63-
entry = functionExtractor.extract();
64-
}
94+
// Cache the declaration count since we're going to be appending more declarations as
95+
// we iterate.
96+
const declarationCount = exportedDeclarations.length;
6597

66-
if (ts.isVariableDeclaration(node) && !isSyntheticAngularConstant(node)) {
67-
entry = extractConstant(node, this.typeChecker);
68-
}
98+
// The exported declaration map only includes one function declaration in situations
99+
// where a function has overloads, so we add the overloads here.
100+
for (let i = 0; i < declarationCount; i++) {
101+
const [exportName, declaration] = exportedDeclarations[i];
102+
if (ts.isFunctionDeclaration(declaration)) {
103+
const extractor = new FunctionExtractor(declaration, this.typeChecker);
104+
const overloads = extractor.getOverloads().map(overload => [exportName, overload] as const);
69105

70-
if (ts.isEnumDeclaration(node)) {
71-
entry = extractEnum(node, this.typeChecker);
72-
}
73-
74-
// The exported name of an API may be different from its declaration name, so
75-
// use the declaration name.
76-
if (entry) {
77-
entry.name = exportName;
78-
entries.push(entry);
106+
exportedDeclarations.push(...overloads);
79107
}
80108
}
81109

82-
return entries;
110+
// Sort the declaration nodes into declaration position because their order is lost in
111+
// reading from the export map. This is primarily useful for testing and debugging.
112+
return exportedDeclarations.sort(
113+
([a, declarationA], [b, declarationB]) => declarationA.pos - declarationB.pos);
83114
}
84115
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
/** Gets whether a symbol's name indicates it is an Angular-private API. */
10+
export function isAngularPrivateName(name: string) {
11+
const firstChar = name[0] ?? '';
12+
return firstChar === 'ɵ' || firstChar === '_';
13+
}

packages/compiler-cli/src/ngtsc/docs/src/function_extractor.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,36 @@ export class FunctionExtractor {
4848
isRestParam: !!param.dotDotDotToken,
4949
}));
5050
}
51+
52+
/** Gets all overloads for the function (excluding this extractor's FunctionDeclaration). */
53+
getOverloads(): ts.FunctionDeclaration[] {
54+
const overloads = [];
55+
56+
// The symbol for this declaration has reference to the other function declarations for
57+
// the overloads.
58+
const symbol = this.getSymbol();
59+
60+
const declarationCount = symbol?.declarations?.length ?? 0;
61+
if (declarationCount > 1) {
62+
// Stop iterating before the final declaration, which is the actual implementation.
63+
for (let i = 0; i < declarationCount - 1; i++) {
64+
const overloadDeclaration = symbol?.declarations?.[i];
65+
66+
// Skip the declaration we started with.
67+
if (overloadDeclaration?.pos === this.declaration.pos) continue;
68+
69+
if (overloadDeclaration && ts.isFunctionDeclaration(overloadDeclaration) &&
70+
overloadDeclaration.modifiers?.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword)) {
71+
overloads.push(overloadDeclaration);
72+
}
73+
}
74+
}
75+
76+
return overloads;
77+
}
78+
79+
private getSymbol(): ts.Symbol|undefined {
80+
return this.typeChecker.getSymbolsInScope(this.declaration, ts.SymbolFlags.Function)
81+
.find(s => s.name === this.declaration.name?.getText());
82+
}
5183
}

0 commit comments

Comments
 (0)