|
| 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 | +} |
0 commit comments