Skip to content

Commit c7daf7e

Browse files
jelbournpkozlowski-opensource
authored andcommitted
feat(compiler): extract directive docs info (#51733)
Based on top of #51685 This expands on the extraction with information for directives, including inputs and outputs. As part of this change, I've refactored the extraction code related to class and to directives into their own extractor classes to more cleanly separate extraction logic based on type of statement. PR Close #51733
1 parent 7f6d9a7 commit c7daf7e

File tree

6 files changed

+442
-142
lines changed

6 files changed

+442
-142
lines changed

packages/compiler-cli/src/ngtsc/docs/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ ts_library(
1010
]),
1111
module_name = "@angular/compiler-cli/src/ngtsc/docs",
1212
deps = [
13+
"//packages/compiler-cli/src/ngtsc/imports",
1314
"//packages/compiler-cli/src/ngtsc/metadata",
15+
"//packages/compiler-cli/src/ngtsc/reflection",
1416
"//packages/compiler-cli/src/ngtsc/util",
1517
"@npm//@types/node",
1618
"@npm//typescript",
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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 ts from 'typescript';
10+
11+
import {Reference} from '../../imports';
12+
import {DirectiveMeta, InputMapping, InputOrOutput, MetadataReader} from '../../metadata';
13+
import {ClassDeclaration} from '../../reflection';
14+
15+
import {ClassEntry, DirectiveEntry, EntryType, MemberEntry, MemberTags, MemberType, MethodEntry, PropertyEntry} from './entities';
16+
import {extractFunction} from './function-extractor';
17+
18+
/** Extractor to pull info for API reference documentation for a TypeScript class. */
19+
class ClassExtractor {
20+
constructor(
21+
protected declaration: ClassDeclaration,
22+
protected reference: Reference,
23+
protected checker: ts.TypeChecker,
24+
) {}
25+
26+
/** Extract docs info specific to classes. */
27+
extract(): ClassEntry {
28+
return {
29+
name: this.declaration.name!.text,
30+
entryType: EntryType.undecorated_class,
31+
members: this.extractAllClassMembers(this.declaration as ts.ClassDeclaration),
32+
};
33+
}
34+
35+
/** Extracts doc info for a class's members. */
36+
protected extractAllClassMembers(classDeclaration: ts.ClassDeclaration): MemberEntry[] {
37+
const members: MemberEntry[] = [];
38+
39+
for (const member of classDeclaration.members) {
40+
if (this.isMemberExcluded(member)) continue;
41+
42+
const memberEntry = this.extractClassMember(member);
43+
if (memberEntry) {
44+
members.push(memberEntry);
45+
}
46+
}
47+
48+
return members;
49+
}
50+
51+
/** Extract docs for a class's members (methods and properties). */
52+
protected extractClassMember(memberDeclaration: ts.ClassElement): MemberEntry|undefined {
53+
if (ts.isMethodDeclaration(memberDeclaration)) {
54+
return this.extractMethod(memberDeclaration);
55+
} else if (ts.isPropertyDeclaration(memberDeclaration)) {
56+
return this.extractClassProperty(memberDeclaration);
57+
}
58+
59+
// We only expect methods and properties. If we encounter something else,
60+
// return undefined and let the rest of the program filter it out.
61+
return undefined;
62+
}
63+
64+
/** Extracts docs for a class method. */
65+
protected extractMethod(methodDeclaration: ts.MethodDeclaration): MethodEntry {
66+
return {
67+
...extractFunction(methodDeclaration),
68+
memberType: MemberType.method,
69+
memberTags: this.getMemberTags(methodDeclaration),
70+
};
71+
}
72+
73+
/** Extracts doc info for a property declaration. */
74+
protected extractClassProperty(propertyDeclaration: ts.PropertyDeclaration): PropertyEntry {
75+
return {
76+
name: propertyDeclaration.name.getText(),
77+
getType: 'TODO',
78+
setType: 'TODO',
79+
memberType: MemberType.property,
80+
memberTags: this.getMemberTags(propertyDeclaration),
81+
};
82+
}
83+
84+
/** Gets the tags for a member (protected, readonly, static, etc.) */
85+
protected getMemberTags(member: ts.MethodDeclaration|ts.PropertyDeclaration): MemberTags[] {
86+
const tags: MemberTags[] = this.getMemberTagsFromModifiers(member.modifiers ?? []);
87+
88+
if (member.questionToken) {
89+
tags.push(MemberTags.optional);
90+
}
91+
92+
return tags;
93+
}
94+
95+
/** Get the tags for a member that come from the declaration modifiers. */
96+
private getMemberTagsFromModifiers(mods: Iterable<ts.ModifierLike>): MemberTags[] {
97+
const tags: MemberTags[] = [];
98+
for (const mod of mods) {
99+
const tag = this.getTagForMemberModifier(mod);
100+
if (tag) tags.push(tag);
101+
}
102+
return tags;
103+
}
104+
105+
/** Gets the doc tag corresponding to a class member modifier (readonly, protected, etc.). */
106+
private getTagForMemberModifier(mod: ts.ModifierLike): MemberTags|undefined {
107+
switch (mod.kind) {
108+
case ts.SyntaxKind.StaticKeyword:
109+
return MemberTags.static;
110+
case ts.SyntaxKind.ReadonlyKeyword:
111+
return MemberTags.readonly;
112+
case ts.SyntaxKind.ProtectedKeyword:
113+
return MemberTags.protected;
114+
default:
115+
return undefined;
116+
}
117+
}
118+
119+
/**
120+
* Gets whether a given class member should be excluded from public API docs.
121+
* This is the case if:
122+
* - The member does not have a name
123+
* - The member is neither a method nor property
124+
* - The member is protected
125+
*/
126+
private isMemberExcluded(member: ts.ClassElement): boolean {
127+
return !member.name || !this.isMethodOrProperty(member) ||
128+
!!member.modifiers?.some(mod => mod.kind === ts.SyntaxKind.PrivateKeyword);
129+
}
130+
131+
/** Gets whether a class member is either a member or a property. */
132+
private isMethodOrProperty(member: ts.ClassElement): member is ts.MethodDeclaration
133+
|ts.PropertyDeclaration {
134+
return ts.isMethodDeclaration(member) || ts.isPropertyDeclaration(member);
135+
}
136+
}
137+
138+
/** Extractor to pull info for API reference documentation for an Angular directive. */
139+
class DirectiveExtractor extends ClassExtractor {
140+
constructor(
141+
declaration: ClassDeclaration,
142+
reference: Reference,
143+
protected metadata: DirectiveMeta,
144+
checker: ts.TypeChecker,
145+
) {
146+
super(declaration, reference, checker);
147+
}
148+
149+
/** Extract docs info for directives and components (including underlying class info). */
150+
override extract(): DirectiveEntry {
151+
return {
152+
...super.extract(),
153+
isStandalone: this.metadata.isStandalone,
154+
selector: this.metadata.selector ?? '',
155+
exportAs: this.metadata.exportAs ?? [],
156+
entryType: this.metadata.isComponent ? EntryType.component : EntryType.directive,
157+
};
158+
}
159+
160+
override extractClassProperty(propertyDeclaration: ts.PropertyDeclaration): PropertyEntry {
161+
const entry = super.extractClassProperty(propertyDeclaration);
162+
163+
const inputMetadata = this.getInputMetadata(propertyDeclaration);
164+
if (inputMetadata) {
165+
entry.memberTags.push(MemberTags.input);
166+
entry.inputAlias = inputMetadata.bindingPropertyName;
167+
}
168+
169+
const outputMetadata = this.getOutputMetadata(propertyDeclaration);
170+
if (outputMetadata) {
171+
entry.memberTags.push(MemberTags.output);
172+
entry.outputAlias = outputMetadata.bindingPropertyName;
173+
}
174+
175+
return entry;
176+
}
177+
178+
/** Gets the input metadata for a directive property. */
179+
private getInputMetadata(prop: ts.PropertyDeclaration): InputMapping|undefined {
180+
const propName = prop.name.getText();
181+
return this.metadata.inputs?.getByClassPropertyName(propName) ?? undefined;
182+
}
183+
184+
/** Gets the output metadata for a directive property. */
185+
private getOutputMetadata(prop: ts.PropertyDeclaration): InputOrOutput|undefined {
186+
const propName = prop.name.getText();
187+
return this.metadata?.outputs?.getByClassPropertyName(propName) ?? undefined;
188+
}
189+
}
190+
191+
/** Extracts documentation info for a class, potentially including Angular-specific info. */
192+
export function extractClass(
193+
classDeclaration: ClassDeclaration, metadataReader: MetadataReader,
194+
typeChecker: ts.TypeChecker): ClassEntry {
195+
const ref = new Reference(classDeclaration);
196+
const metadata = metadataReader.getDirectiveMetadata(ref);
197+
const extractor = metadata ?
198+
new DirectiveExtractor(classDeclaration, ref, metadata, typeChecker) :
199+
new ClassExtractor(classDeclaration, ref, typeChecker);
200+
201+
return extractor.extract();
202+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ export interface ClassEntry extends DocEntry {
4848
members: MemberEntry[];
4949
}
5050

51+
/** Documentation entity for an Angular directives and components. */
52+
export interface DirectiveEntry extends ClassEntry {
53+
selector: string;
54+
exportAs: string[];
55+
isStandalone: boolean;
56+
}
57+
5158
export interface FunctionEntry extends DocEntry {
5259
params: ParameterEntry[];
5360
returnType: string;
@@ -64,6 +71,8 @@ export interface MemberEntry {
6471
export interface PropertyEntry extends MemberEntry {
6572
getType: string;
6673
setType: string;
74+
inputAlias?: string;
75+
outputAlias?: string;
6776
}
6877

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

0 commit comments

Comments
 (0)