Skip to content

Commit 2e41488

Browse files
jelbournpkozlowski-opensource
authored andcommitted
feat(compiler): extract docs info for enums, pipes, and NgModules (#51733)
Based on top of #51717 This commit adds extraction for enums, pipes, and NgModules. It also adds a couple of tests for JsDoc extraction that weren't covered in the previous commit. PR Close #51733
1 parent e0b1bb3 commit 2e41488

File tree

8 files changed

+388
-10
lines changed

8 files changed

+388
-10
lines changed

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

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from '@angul
1111
import ts from 'typescript';
1212

1313
import {Reference} from '../../imports';
14-
import {DirectiveMeta, InputMapping, InputOrOutput, MetadataReader} from '../../metadata';
14+
import {DirectiveMeta, InputMapping, InputOrOutput, MetadataReader, NgModuleMeta, PipeMeta} from '../../metadata';
1515
import {ClassDeclaration} from '../../reflection';
1616

17-
import {ClassEntry, DirectiveEntry, EntryType, MemberEntry, MemberTags, MemberType, MethodEntry, PropertyEntry} from './entities';
17+
import {ClassEntry, DirectiveEntry, EntryType, MemberEntry, MemberTags, MemberType, MethodEntry, PipeEntry, PropertyEntry} from './entities';
1818
import {extractResolvedTypeString} from './type_extractor';
1919

2020
/** A class member declaration that is *like* a property (including accessors) */
@@ -211,15 +211,69 @@ class DirectiveExtractor extends ClassExtractor {
211211
}
212212
}
213213

214+
/** Extractor to pull info for API reference documentation for an Angular pipe. */
215+
class PipeExtractor extends ClassExtractor {
216+
constructor(
217+
declaration: ClassDeclaration&ts.ClassDeclaration,
218+
reference: Reference,
219+
private metadata: PipeMeta,
220+
typeChecker: ts.TypeChecker,
221+
) {
222+
super(declaration, reference, typeChecker);
223+
}
224+
225+
override extract(): PipeEntry {
226+
return {
227+
...super.extract(),
228+
pipeName: this.metadata.name,
229+
entryType: EntryType.Pipe,
230+
isStandalone: this.metadata.isStandalone,
231+
};
232+
}
233+
}
234+
235+
/** Extractor to pull info for API reference documentation for an Angular pipe. */
236+
class NgModuleExtractor extends ClassExtractor {
237+
constructor(
238+
declaration: ClassDeclaration&ts.ClassDeclaration,
239+
reference: Reference,
240+
private metadata: NgModuleMeta,
241+
typeChecker: ts.TypeChecker,
242+
) {
243+
super(declaration, reference, typeChecker);
244+
}
245+
246+
override extract(): ClassEntry {
247+
return {
248+
...super.extract(),
249+
entryType: EntryType.NgModule,
250+
};
251+
}
252+
}
253+
214254
/** Extracts documentation info for a class, potentially including Angular-specific info. */
215255
export function extractClass(
216-
classDeclaration: ClassDeclaration&ts.ClassDeclaration, metadataReader: MetadataReader,
217-
typeChecker: ts.TypeChecker): ClassEntry {
256+
classDeclaration: ClassDeclaration&ts.ClassDeclaration,
257+
metadataReader: MetadataReader,
258+
typeChecker: ts.TypeChecker,
259+
): ClassEntry {
218260
const ref = new Reference(classDeclaration);
219-
const metadata = metadataReader.getDirectiveMetadata(ref);
220-
const extractor = metadata ?
221-
new DirectiveExtractor(classDeclaration, ref, metadata, typeChecker) :
222-
new ClassExtractor(classDeclaration, ref, typeChecker);
261+
262+
let extractor: ClassExtractor;
263+
264+
let directiveMetadata = metadataReader.getDirectiveMetadata(ref);
265+
let pipeMetadata = metadataReader.getPipeMetadata(ref);
266+
let ngModuleMetadata = metadataReader.getNgModuleMetadata(ref);
267+
268+
if (directiveMetadata) {
269+
extractor = new DirectiveExtractor(classDeclaration, ref, directiveMetadata, typeChecker);
270+
} else if (pipeMetadata) {
271+
extractor = new PipeExtractor(classDeclaration, ref, pipeMetadata, typeChecker);
272+
} else if (ngModuleMetadata) {
273+
extractor = new NgModuleExtractor(classDeclaration, ref, ngModuleMetadata, typeChecker);
274+
} else {
275+
extractor = new ClassExtractor(classDeclaration, ref, typeChecker);
276+
}
223277

224278
return extractor.extract();
225279
}

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export enum EntryType {
1717
Enum = 'enum',
1818
Function = 'function',
1919
Interface = 'interface',
20+
NgModule = 'ng_module',
2021
Pipe = 'pipe',
2122
TypeAlias = 'type_alias',
2223
UndecoratedClass = 'undecorated_class',
@@ -28,6 +29,7 @@ export enum MemberType {
2829
Method = 'method',
2930
Getter = 'getter',
3031
Setter = 'setter',
32+
EnumItem = 'enum_item',
3133
}
3234

3335
/** Informational tags applicable to class members. */
@@ -64,19 +66,30 @@ export interface ClassEntry extends DocEntry {
6466
members: MemberEntry[];
6567
}
6668

69+
/** Documentation entity for a TypeScript enum. */
70+
export interface EnumEntry extends DocEntry {
71+
members: EnumMemberEntry[];
72+
}
73+
6774
/** Documentation entity for an Angular directives and components. */
6875
export interface DirectiveEntry extends ClassEntry {
6976
selector: string;
7077
exportAs: string[];
7178
isStandalone: boolean;
7279
}
7380

81+
export interface PipeEntry extends ClassEntry {
82+
pipeName: string;
83+
isStandalone: boolean;
84+
// TODO: add `isPure`.
85+
}
86+
7487
export interface FunctionEntry extends DocEntry {
7588
params: ParameterEntry[];
7689
returnType: string;
7790
}
7891

79-
/** Sub-entry for a single class member. */
92+
/** Sub-entry for a single class or enum member. */
8093
export interface MemberEntry {
8194
name: string;
8295
memberType: MemberType;
@@ -85,6 +98,12 @@ export interface MemberEntry {
8598
jsdocTags: JsDocTagEntry[];
8699
}
87100

101+
/** Sub-entry for an enum member. */
102+
export interface EnumMemberEntry extends MemberEntry {
103+
type: string;
104+
value: string;
105+
}
106+
88107
/** Sub-entry for a class property. */
89108
export interface PropertyEntry extends MemberEntry {
90109
type: string;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
import {EntryType, EnumEntry, EnumMemberEntry, MemberType} from '@angular/compiler-cli/src/ngtsc/docs/src/entities';
10+
import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from '@angular/compiler-cli/src/ngtsc/docs/src/jsdoc_extractor';
11+
import {extractResolvedTypeString} from '@angular/compiler-cli/src/ngtsc/docs/src/type_extractor';
12+
import ts from 'typescript';
13+
14+
15+
/** Extracts documentation entry for an enum. */
16+
export function extractEnum(
17+
declaration: ts.EnumDeclaration, typeChecker: ts.TypeChecker): EnumEntry {
18+
return {
19+
name: declaration.name.getText(),
20+
entryType: EntryType.Enum,
21+
members: extractEnumMembers(declaration, typeChecker),
22+
rawComment: extractRawJsDoc(declaration),
23+
description: extractJsDocDescription(declaration),
24+
jsdocTags: extractJsDocTags(declaration),
25+
};
26+
}
27+
28+
/** Extracts doc info for an enum's members. */
29+
function extractEnumMembers(
30+
declaration: ts.EnumDeclaration, checker: ts.TypeChecker): EnumMemberEntry[] {
31+
return declaration.members.map(member => ({
32+
name: member.name.getText(),
33+
type: extractResolvedTypeString(member, checker),
34+
value: getEnumMemberValue(member),
35+
memberType: MemberType.EnumItem,
36+
jsdocTags: extractJsDocTags(member),
37+
description: extractJsDocDescription(member),
38+
memberTags: [],
39+
}));
40+
}
41+
42+
/** Gets the explicitly assigned value for an enum member, or an empty string if there is none. */
43+
function getEnumMemberValue(memberNode: ts.EnumMember): string {
44+
// If the enum member has a child number literal or string literal,
45+
// we use that literal as the "value" of the member.
46+
const literal =
47+
memberNode.getChildren().find(n => ts.isNumericLiteral(n) || ts.isStringLiteral(n));
48+
return literal?.getText() ?? '';
49+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {extractEnum} from '@angular/compiler-cli/src/ngtsc/docs/src/enum_extractor';
910
import {FunctionExtractor} from '@angular/compiler-cli/src/ngtsc/docs/src/function_extractor';
1011
import ts from 'typescript';
1112

@@ -51,6 +52,10 @@ export class DocsExtractor {
5152
}
5253
});
5354
}
55+
56+
if (ts.isEnumDeclaration(statement)) {
57+
entries.push(extractEnum(statement, this.typeChecker));
58+
}
5459
}
5560

5661
return entries;
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
import {DocEntry} from '@angular/compiler-cli/src/ngtsc/docs';
10+
import {ClassEntry, DirectiveEntry, EntryType, EnumEntry, MemberTags, PropertyEntry} from '@angular/compiler-cli/src/ngtsc/docs/src/entities';
11+
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
12+
import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing';
13+
14+
import {NgtscTestEnvironment} from '../env';
15+
16+
const testFiles = loadStandardTestFiles({fakeCore: true, fakeCommon: true});
17+
18+
runInEachFileSystem(os => {
19+
let env!: NgtscTestEnvironment;
20+
21+
describe('ngtsc enum docs extraction', () => {
22+
beforeEach(() => {
23+
env = NgtscTestEnvironment.setup(testFiles);
24+
env.tsconfig();
25+
});
26+
27+
it('should extract enum info without explicit values', () => {
28+
env.write('test.ts', `
29+
export enum PizzaTopping {
30+
/** It is cheese */
31+
Cheese,
32+
33+
/** Or "tomato" if you are British */
34+
Tomato,
35+
}
36+
`);
37+
38+
const docs: DocEntry[] = env.driveDocsExtraction();
39+
40+
expect(docs.length).toBe(1);
41+
expect(docs[0].entryType).toBe(EntryType.Enum);
42+
43+
const enumEntry = docs[0] as EnumEntry;
44+
expect(enumEntry.name).toBe('PizzaTopping');
45+
expect(enumEntry.members.length).toBe(2);
46+
47+
const [cheeseEntry, tomatoEntry] = enumEntry.members;
48+
49+
expect(cheeseEntry.name).toBe('Cheese');
50+
expect(cheeseEntry.description).toBe('It is cheese');
51+
expect(cheeseEntry.value).toBe('');
52+
53+
expect(tomatoEntry.name).toBe('Tomato');
54+
expect(tomatoEntry.description).toBe('Or "tomato" if you are British');
55+
expect(tomatoEntry.value).toBe('');
56+
});
57+
58+
it('should extract enum info with explicit values', () => {
59+
env.write('test.ts', `
60+
export enum PizzaTopping {
61+
/** It is cheese */
62+
Cheese = 0,
63+
64+
/** Or "tomato" if you are British */
65+
Tomato = "tomato",
66+
}
67+
`);
68+
69+
const docs: DocEntry[] = env.driveDocsExtraction();
70+
71+
expect(docs.length).toBe(1);
72+
expect(docs[0].entryType).toBe(EntryType.Enum);
73+
74+
const enumEntry = docs[0] as EnumEntry;
75+
expect(enumEntry.name).toBe('PizzaTopping');
76+
expect(enumEntry.members.length).toBe(2);
77+
78+
const [cheeseEntry, tomatoEntry] = enumEntry.members;
79+
80+
expect(cheeseEntry.name).toBe('Cheese');
81+
expect(cheeseEntry.description).toBe('It is cheese');
82+
expect(cheeseEntry.value).toBe('0');
83+
expect(cheeseEntry.type).toBe('PizzaTopping.Cheese');
84+
85+
expect(tomatoEntry.name).toBe('Tomato');
86+
expect(tomatoEntry.description).toBe('Or "tomato" if you are British');
87+
expect(tomatoEntry.value).toBe('"tomato"');
88+
expect(tomatoEntry.type).toBe('PizzaTopping.Tomato');
89+
});
90+
});
91+
});

packages/compiler-cli/test/ngtsc/doc_extraction/jsdoc_extraction_spec.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {DocEntry} from '@angular/compiler-cli/src/ngtsc/docs';
10-
import {ConstantEntry, EntryType, FunctionEntry} from '@angular/compiler-cli/src/ngtsc/docs/src/entities';
10+
import {ClassEntry, FunctionEntry, MethodEntry} from '@angular/compiler-cli/src/ngtsc/docs/src/entities';
1111
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
1212
import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing';
1313

@@ -248,5 +248,45 @@ runInEachFileSystem(os => {
248248
expect(dataEntry.description).toBe('The data to save.');
249249
expect(timingEntry.description).toBe('Long description\nwith multiple lines.');
250250
});
251+
252+
it('should extract class member descriptions', () => {
253+
env.write('test.ts', `
254+
export class UserProfile {
255+
/** A user identifier. */
256+
userId: number = 0;
257+
258+
/** Name of the user */
259+
get name(): string { return ''; }
260+
261+
/** Name of the user */
262+
set name(value: string) { }
263+
264+
/**
265+
* Save the user.
266+
* @param config Setting for saving.
267+
* @returns Whether it succeeded
268+
*/
269+
save(config: object): boolean { return false; }
270+
}
271+
`);
272+
273+
const docs: DocEntry[] = env.driveDocsExtraction();
274+
expect(docs.length).toBe(1);
275+
const classEntry = docs[0] as ClassEntry;
276+
277+
expect(classEntry.members.length).toBe(4);
278+
const [userIdEntry, nameGetterEntry, nameSetterEntry, ] = classEntry.members;
279+
280+
expect(userIdEntry.description).toBe('A user identifier.');
281+
expect(nameGetterEntry.description).toBe('Name of the user');
282+
expect(nameSetterEntry.description).toBe('Name of the user');
283+
284+
const saveEntry = classEntry.members[3] as MethodEntry;
285+
expect(saveEntry.description).toBe('Save the user.');
286+
287+
expect(saveEntry.params[0].description).toBe('Setting for saving.');
288+
expect(saveEntry.jsdocTags.length).toBe(2);
289+
expect(saveEntry.jsdocTags[1]).toEqual({name: 'returns', comment: 'Whether it succeeded'});
290+
});
251291
});
252292
});

0 commit comments

Comments
 (0)