-
Notifications
You must be signed in to change notification settings - Fork 27.2k
feat(compiler): extract doc info for JsDoc #51717
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
b662468
feat(compiler): initial skeleton for API doc extraction
jelbourn 5d962ad
feat(compiler): expand class api doc extraction
jelbourn 676ea32
feat(compiler): extract directive docs info
jelbourn 9d30c42
feat(compiler): extract docs for accessors, rest params, and types
jelbourn 77ac4f5
feat(compiler): extract docs for top level functions and consts
jelbourn 6b91f2f
feat(compiler): extract doc info for JsDoc
jelbourn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| load("//tools:defaults.bzl", "ts_library") | ||
|
|
||
| package(default_visibility = ["//visibility:public"]) | ||
|
|
||
| # Compiler code pertaining to extracting data for generating API reference documentation. | ||
| ts_library( | ||
| name = "docs", | ||
| srcs = ["index.ts"] + glob([ | ||
| "src/**/*.ts", | ||
| ]), | ||
| module_name = "@angular/compiler-cli/src/ngtsc/docs", | ||
| deps = [ | ||
| "//packages/compiler-cli/src/ngtsc/imports", | ||
| "//packages/compiler-cli/src/ngtsc/metadata", | ||
| "//packages/compiler-cli/src/ngtsc/reflection", | ||
| "//packages/compiler-cli/src/ngtsc/util", | ||
| "@npm//@types/node", | ||
| "@npm//typescript", | ||
| ], | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| /** | ||
| * @license | ||
| * Copyright Google LLC All Rights Reserved. | ||
| * | ||
| * Use of this source code is governed by an MIT-style license that can be | ||
| * found in the LICENSE file at https://angular.io/license | ||
| */ | ||
|
|
||
| export {DocEntry} from './src/entities'; | ||
| export {DocsExtractor} from './src/extractor'; |
225 changes: 225 additions & 0 deletions
225
packages/compiler-cli/src/ngtsc/docs/src/class_extractor.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| /** | ||
| * @license | ||
| * Copyright Google LLC All Rights Reserved. | ||
| * | ||
| * Use of this source code is governed by an MIT-style license that can be | ||
| * found in the LICENSE file at https://angular.io/license | ||
| */ | ||
|
|
||
| import {FunctionExtractor} from '@angular/compiler-cli/src/ngtsc/docs/src/function_extractor'; | ||
| import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from '@angular/compiler-cli/src/ngtsc/docs/src/jsdoc_extractor'; | ||
| import ts from 'typescript'; | ||
|
|
||
| import {Reference} from '../../imports'; | ||
| import {DirectiveMeta, InputMapping, InputOrOutput, MetadataReader} from '../../metadata'; | ||
| import {ClassDeclaration} from '../../reflection'; | ||
|
|
||
| import {ClassEntry, DirectiveEntry, EntryType, MemberEntry, MemberTags, MemberType, MethodEntry, PropertyEntry} from './entities'; | ||
| import {extractResolvedTypeString} from './type_extractor'; | ||
|
|
||
| /** A class member declaration that is *like* a property (including accessors) */ | ||
| type PropertyDeclarationLike = ts.PropertyDeclaration|ts.AccessorDeclaration; | ||
|
|
||
| /** Extractor to pull info for API reference documentation for a TypeScript class. */ | ||
| class ClassExtractor { | ||
| constructor( | ||
| protected declaration: ClassDeclaration&ts.ClassDeclaration, | ||
| protected reference: Reference, | ||
| protected typeChecker: ts.TypeChecker, | ||
| ) {} | ||
|
|
||
| /** Extract docs info specific to classes. */ | ||
| extract(): ClassEntry { | ||
| return { | ||
| name: this.declaration.name!.text, | ||
| entryType: EntryType.UndecoratedClass, | ||
| members: this.extractAllClassMembers(this.declaration), | ||
| description: extractJsDocDescription(this.declaration), | ||
| jsdocTags: extractJsDocTags(this.declaration), | ||
| rawComment: extractRawJsDoc(this.declaration), | ||
| }; | ||
| } | ||
|
|
||
| /** Extracts doc info for a class's members. */ | ||
| protected extractAllClassMembers(classDeclaration: ts.ClassDeclaration): MemberEntry[] { | ||
| const members: MemberEntry[] = []; | ||
|
|
||
| for (const member of classDeclaration.members) { | ||
| if (this.isMemberExcluded(member)) continue; | ||
|
|
||
| const memberEntry = this.extractClassMember(member); | ||
| if (memberEntry) { | ||
| members.push(memberEntry); | ||
| } | ||
| } | ||
|
|
||
| return members; | ||
| } | ||
|
|
||
| /** Extract docs for a class's members (methods and properties). */ | ||
| protected extractClassMember(memberDeclaration: ts.ClassElement): MemberEntry|undefined { | ||
| if (ts.isMethodDeclaration(memberDeclaration)) { | ||
| return this.extractMethod(memberDeclaration); | ||
| } else if (ts.isPropertyDeclaration(memberDeclaration)) { | ||
| return this.extractClassProperty(memberDeclaration); | ||
| } else if (ts.isAccessor(memberDeclaration)) { | ||
| return this.extractGetterSetter(memberDeclaration); | ||
| } | ||
|
|
||
| // We only expect methods, properties, and accessors. If we encounter something else, | ||
| // return undefined and let the rest of the program filter it out. | ||
| return undefined; | ||
| } | ||
|
|
||
| /** Extracts docs for a class method. */ | ||
| protected extractMethod(methodDeclaration: ts.MethodDeclaration): MethodEntry { | ||
| const functionExtractor = new FunctionExtractor(methodDeclaration, this.typeChecker); | ||
| return { | ||
| ...functionExtractor.extract(), | ||
| memberType: MemberType.Method, | ||
| memberTags: this.getMemberTags(methodDeclaration), | ||
| }; | ||
| } | ||
|
|
||
| /** Extracts doc info for a property declaration. */ | ||
| protected extractClassProperty(propertyDeclaration: PropertyDeclarationLike): PropertyEntry { | ||
| return { | ||
| name: propertyDeclaration.name.getText(), | ||
| type: extractResolvedTypeString(propertyDeclaration, this.typeChecker), | ||
| memberType: MemberType.Property, | ||
| memberTags: this.getMemberTags(propertyDeclaration), | ||
| description: extractJsDocDescription(propertyDeclaration), | ||
| jsdocTags: extractJsDocTags(propertyDeclaration), | ||
| }; | ||
| } | ||
|
|
||
| /** Extracts doc info for an accessor member (getter/setter). */ | ||
| protected extractGetterSetter(accessor: ts.AccessorDeclaration): PropertyEntry { | ||
| return { | ||
| ...this.extractClassProperty(accessor), | ||
| memberType: ts.isGetAccessor(accessor) ? MemberType.Getter : MemberType.Setter, | ||
| }; | ||
| } | ||
|
|
||
| /** Gets the tags for a member (protected, readonly, static, etc.) */ | ||
| protected getMemberTags(member: ts.MethodDeclaration|ts.PropertyDeclaration| | ||
| ts.AccessorDeclaration): MemberTags[] { | ||
| const tags: MemberTags[] = this.getMemberTagsFromModifiers(member.modifiers ?? []); | ||
|
|
||
| if (member.questionToken) { | ||
| tags.push(MemberTags.Optional); | ||
| } | ||
|
|
||
| return tags; | ||
| } | ||
|
|
||
| /** Get the tags for a member that come from the declaration modifiers. */ | ||
| private getMemberTagsFromModifiers(mods: Iterable<ts.ModifierLike>): MemberTags[] { | ||
| const tags: MemberTags[] = []; | ||
| for (const mod of mods) { | ||
| const tag = this.getTagForMemberModifier(mod); | ||
| if (tag) tags.push(tag); | ||
| } | ||
| return tags; | ||
| } | ||
|
|
||
| /** Gets the doc tag corresponding to a class member modifier (readonly, protected, etc.). */ | ||
| private getTagForMemberModifier(mod: ts.ModifierLike): MemberTags|undefined { | ||
| switch (mod.kind) { | ||
| case ts.SyntaxKind.StaticKeyword: | ||
| return MemberTags.Static; | ||
| case ts.SyntaxKind.ReadonlyKeyword: | ||
| return MemberTags.Readonly; | ||
| case ts.SyntaxKind.ProtectedKeyword: | ||
| return MemberTags.Protected; | ||
| default: | ||
| return undefined; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Gets whether a given class member should be excluded from public API docs. | ||
| * This is the case if: | ||
| * - The member does not have a name | ||
| * - The member is neither a method nor property | ||
| * - The member is protected | ||
| */ | ||
| private isMemberExcluded(member: ts.ClassElement): boolean { | ||
| return !member.name || !this.isDocumentableMember(member) || | ||
| !!member.modifiers?.some(mod => mod.kind === ts.SyntaxKind.PrivateKeyword); | ||
| } | ||
|
|
||
| /** Gets whether a class member is a method, property, or accessor. */ | ||
| private isDocumentableMember(member: ts.ClassElement): member is ts.MethodDeclaration | ||
| |ts.PropertyDeclaration { | ||
| return ts.isMethodDeclaration(member) || ts.isPropertyDeclaration(member) || | ||
| ts.isAccessor(member); | ||
| } | ||
| } | ||
|
|
||
| /** Extractor to pull info for API reference documentation for an Angular directive. */ | ||
| class DirectiveExtractor extends ClassExtractor { | ||
| constructor( | ||
| declaration: ClassDeclaration&ts.ClassDeclaration, | ||
| reference: Reference, | ||
| protected metadata: DirectiveMeta, | ||
| checker: ts.TypeChecker, | ||
| ) { | ||
| super(declaration, reference, checker); | ||
| } | ||
|
|
||
| /** Extract docs info for directives and components (including underlying class info). */ | ||
| override extract(): DirectiveEntry { | ||
| return { | ||
| ...super.extract(), | ||
| isStandalone: this.metadata.isStandalone, | ||
| selector: this.metadata.selector ?? '', | ||
| exportAs: this.metadata.exportAs ?? [], | ||
| entryType: this.metadata.isComponent ? EntryType.Component : EntryType.Directive, | ||
| }; | ||
| } | ||
|
|
||
| /** Extracts docs info for a directive property, including input/output metadata. */ | ||
| override extractClassProperty(propertyDeclaration: ts.PropertyDeclaration): PropertyEntry { | ||
| const entry = super.extractClassProperty(propertyDeclaration); | ||
|
|
||
| const inputMetadata = this.getInputMetadata(propertyDeclaration); | ||
| if (inputMetadata) { | ||
| entry.memberTags.push(MemberTags.Input); | ||
| entry.inputAlias = inputMetadata.bindingPropertyName; | ||
| } | ||
|
|
||
| const outputMetadata = this.getOutputMetadata(propertyDeclaration); | ||
| if (outputMetadata) { | ||
| entry.memberTags.push(MemberTags.Output); | ||
| entry.outputAlias = outputMetadata.bindingPropertyName; | ||
| } | ||
|
|
||
| return entry; | ||
| } | ||
|
|
||
| /** Gets the input metadata for a directive property. */ | ||
| private getInputMetadata(prop: ts.PropertyDeclaration): InputMapping|undefined { | ||
| const propName = prop.name.getText(); | ||
| return this.metadata.inputs?.getByClassPropertyName(propName) ?? undefined; | ||
| } | ||
|
|
||
| /** Gets the output metadata for a directive property. */ | ||
| private getOutputMetadata(prop: ts.PropertyDeclaration): InputOrOutput|undefined { | ||
| const propName = prop.name.getText(); | ||
| return this.metadata?.outputs?.getByClassPropertyName(propName) ?? undefined; | ||
| } | ||
| } | ||
|
|
||
| /** Extracts documentation info for a class, potentially including Angular-specific info. */ | ||
| export function extractClass( | ||
| classDeclaration: ClassDeclaration&ts.ClassDeclaration, metadataReader: MetadataReader, | ||
| typeChecker: ts.TypeChecker): ClassEntry { | ||
| const ref = new Reference(classDeclaration); | ||
| const metadata = metadataReader.getDirectiveMetadata(ref); | ||
| const extractor = metadata ? | ||
| new DirectiveExtractor(classDeclaration, ref, metadata, typeChecker) : | ||
| new ClassExtractor(classDeclaration, ref, typeChecker); | ||
|
|
||
| return extractor.extract(); | ||
| } |
45 changes: 45 additions & 0 deletions
45
packages/compiler-cli/src/ngtsc/docs/src/constant_extractor.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| /** | ||
| * @license | ||
| * Copyright Google LLC All Rights Reserved. | ||
| * | ||
| * Use of this source code is governed by an MIT-style license that can be | ||
| * found in the LICENSE file at https://angular.io/license | ||
| */ | ||
|
|
||
| import ts from 'typescript'; | ||
|
|
||
| import {ConstantEntry, EntryType} from './entities'; | ||
| import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc,} from './jsdoc_extractor'; | ||
|
|
||
| /** Extracts documentation entry for a constant. */ | ||
| export function extractConstant( | ||
| declaration: ts.VariableDeclaration, typeChecker: ts.TypeChecker): ConstantEntry { | ||
| // For constants specifically, we want to get the base type for any literal types. | ||
| // For example, TypeScript by default extracts `const PI = 3.14` as PI having a type of the | ||
| // literal `3.14`. We don't want this behavior for constants, since generally one wants the | ||
| // _value_ of the constant to be able to change between releases without changing the type. | ||
| // `VERSION` is a good example here; the version is always a `string`, but the actual value of | ||
| // the version string shouldn't matter to the type system. | ||
| const resolvedType = | ||
| typeChecker.getBaseTypeOfLiteralType(typeChecker.getTypeAtLocation(declaration)); | ||
|
|
||
| // In the TS AST, the leading comment for a variable declaration is actually | ||
| // on the ancestor `ts.VariableStatement` (since a single variable statement may | ||
| // contain multiple variable declarations). | ||
| const variableStatement = declaration.parent.parent; | ||
| const rawComment = extractRawJsDoc(declaration.parent.parent); | ||
|
|
||
| return { | ||
| name: declaration.name.getText(), | ||
| type: typeChecker.typeToString(resolvedType), | ||
| entryType: EntryType.Constant, | ||
| rawComment, | ||
| description: extractJsDocDescription(declaration), | ||
| jsdocTags: extractJsDocTags(declaration), | ||
| }; | ||
| } | ||
|
|
||
| /** Gets whether a given constant is an Angular-added const that should be ignored for docs. */ | ||
| export function isSyntheticAngularConstant(declaration: ts.VariableDeclaration) { | ||
| return declaration.name.getText() === 'USED_FOR_NG_TYPE_CHECKING'; | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't think of a
VariableDeclarationoutside of a variable list, statement- but would you want to assert this? I think that would make this code more robust and avoid future hard-to-debug scenarios?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type of
declaration.parent.parenthere ists.VariableStatement | ts.ForStatement | ts.ForOfStatement | ts.ForInStatement | ts.TryStatement. In the scenario that this code path runs for aforortrystatement, I think the current behavior (an empty string if the node doesn't have any JsDoc) is reasonable.I also expect to need to refine things once I start running it over the real sources.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. Just thought might make things easier to debug if we had an actual runtime assert here