|
| 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 {EntryType, FunctionWithOverloads, InitializerApiFunctionEntry, JsDocTagEntry} from './entities'; |
| 12 | +import {extractAllParams} from './function_extractor'; |
| 13 | +import {extractGenerics} from './generics_extractor'; |
| 14 | +import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from './jsdoc_extractor'; |
| 15 | + |
| 16 | +/** JSDoc used to recognize an initializer API function. */ |
| 17 | +const initializerApiTag = 'initializerApiFunction'; |
| 18 | + |
| 19 | +/** |
| 20 | + * Checks whether the given node corresponds to an initializer API function. |
| 21 | + * |
| 22 | + * An initializer API function is a function declaration or variable declaration |
| 23 | + * that is explicitly annotated with `@initializerApiFunction`. |
| 24 | + * |
| 25 | + * Note: The node may be a function overload signature that is automatically |
| 26 | + * resolved to its implementation to detect the JSDoc tag. |
| 27 | + */ |
| 28 | +export function isInitializerApiFunction(node: ts.Node, typeChecker: ts.TypeChecker): |
| 29 | + node is ts.VariableDeclaration|ts.FunctionDeclaration { |
| 30 | + // If this is matching an overload signature, resolve to the implementation |
| 31 | + // as it would hold the `@initializerApiFunction` tag. |
| 32 | + if (ts.isFunctionDeclaration(node) && node.name !== undefined && node.body === undefined) { |
| 33 | + const implementation = findImplementationOfFunction(node, typeChecker); |
| 34 | + if (implementation !== undefined) { |
| 35 | + node = implementation; |
| 36 | + } |
| 37 | + } |
| 38 | + |
| 39 | + if (!ts.isFunctionDeclaration(node) && !ts.isVariableDeclaration(node)) { |
| 40 | + return false; |
| 41 | + } |
| 42 | + |
| 43 | + let tagContainer = ts.isFunctionDeclaration(node) ? node : getContainerVariableStatement(node); |
| 44 | + if (tagContainer === null) { |
| 45 | + return false; |
| 46 | + } |
| 47 | + const tags = ts.getJSDocTags(tagContainer); |
| 48 | + return tags.some(t => t.tagName.text === initializerApiTag); |
| 49 | +} |
| 50 | + |
| 51 | +/** |
| 52 | + * Extracts the given node as initializer API function and returns |
| 53 | + * a docs entry that can be rendered to represent the API function. |
| 54 | + */ |
| 55 | +export function extractInitializerApiFunction( |
| 56 | + node: ts.VariableDeclaration|ts.FunctionDeclaration, |
| 57 | + typeChecker: ts.TypeChecker): InitializerApiFunctionEntry { |
| 58 | + if (node.name === undefined || !ts.isIdentifier(node.name)) { |
| 59 | + throw new Error(`Initializer API: Expected literal variable name.`); |
| 60 | + } |
| 61 | + |
| 62 | + const container = ts.isFunctionDeclaration(node) ? node : getContainerVariableStatement(node); |
| 63 | + if (container === null) { |
| 64 | + throw new Error('Initializer API: Could not find container AST node of variable.'); |
| 65 | + } |
| 66 | + |
| 67 | + const name = node.name.text; |
| 68 | + const type = typeChecker.getTypeAtLocation(node); |
| 69 | + |
| 70 | + // Top-level call signatures. E.g. `input()`, `input<ReadT>(initialValue: ReadT)`. etc. |
| 71 | + const callFunction: FunctionWithOverloads = |
| 72 | + extractFunctionWithOverloads(name, type.getCallSignatures(), typeChecker); |
| 73 | + // Sub-functions like `input.required()`. |
| 74 | + const subFunctions: FunctionWithOverloads[] = []; |
| 75 | + |
| 76 | + for (const property of type.getProperties()) { |
| 77 | + const subName = property.getName(); |
| 78 | + const subDecl = property.getDeclarations()?.[0]; |
| 79 | + if (subDecl === undefined || !ts.isPropertySignature(subDecl)) { |
| 80 | + throw new Error( |
| 81 | + `Initializer API: Could not resolve declaration of sub-property: ${name}.${subName}`); |
| 82 | + } |
| 83 | + |
| 84 | + const subType = typeChecker.getTypeAtLocation(subDecl); |
| 85 | + subFunctions.push( |
| 86 | + extractFunctionWithOverloads(subName, subType.getCallSignatures(), typeChecker)); |
| 87 | + } |
| 88 | + |
| 89 | + let jsdocTags: JsDocTagEntry[]; |
| 90 | + let description: string; |
| 91 | + let rawComment: string; |
| 92 | + |
| 93 | + // Extract container API documentation. |
| 94 | + // The container description describes the overall function, while |
| 95 | + // we allow the individual top-level call signatures to represent |
| 96 | + // their individual overloads. |
| 97 | + if (ts.isFunctionDeclaration(node)) { |
| 98 | + const implementation = findImplementationOfFunction(node, typeChecker); |
| 99 | + if (implementation === undefined) { |
| 100 | + throw new Error(`Initializer API: Could not find implementation of function: ${name}`); |
| 101 | + } |
| 102 | + |
| 103 | + callFunction.implementation = { |
| 104 | + name, |
| 105 | + entryType: EntryType.Function, |
| 106 | + isNewType: false, |
| 107 | + description: extractJsDocDescription(implementation), |
| 108 | + generics: extractGenerics(implementation), |
| 109 | + jsdocTags: extractJsDocTags(implementation), |
| 110 | + params: extractAllParams(implementation.parameters, typeChecker), |
| 111 | + rawComment: extractRawJsDoc(implementation), |
| 112 | + returnType: typeChecker.typeToString(typeChecker.getReturnTypeOfSignature( |
| 113 | + typeChecker.getSignatureFromDeclaration(implementation)!)), |
| 114 | + }; |
| 115 | + |
| 116 | + jsdocTags = callFunction.implementation.jsdocTags; |
| 117 | + description = callFunction.implementation.description; |
| 118 | + rawComment = callFunction.implementation.description; |
| 119 | + } else { |
| 120 | + jsdocTags = extractJsDocTags(container); |
| 121 | + description = extractJsDocDescription(container); |
| 122 | + rawComment = extractRawJsDoc(container); |
| 123 | + } |
| 124 | + |
| 125 | + // Extract additional docs metadata from the initializer API JSDoc tag. |
| 126 | + const metadataTag = jsdocTags.find(t => t.name === initializerApiTag); |
| 127 | + if (metadataTag === undefined) { |
| 128 | + throw new Error( |
| 129 | + 'Initializer API: Detected initializer API function does ' + |
| 130 | + `not have "@initializerApiFunction" tag: ${name}`); |
| 131 | + } |
| 132 | + |
| 133 | + let parsedMetadata: InitializerApiFunctionEntry['__docsMetadata__'] = undefined; |
| 134 | + if (metadataTag.comment.trim() !== '') { |
| 135 | + try { |
| 136 | + parsedMetadata = JSON.parse(metadataTag.comment) as typeof parsedMetadata; |
| 137 | + } catch (e: unknown) { |
| 138 | + throw new Error(`Could not parse initializer API function metadata: ${e}`); |
| 139 | + } |
| 140 | + } |
| 141 | + |
| 142 | + return { |
| 143 | + entryType: EntryType.InitializerApiFunction, |
| 144 | + name, |
| 145 | + description, |
| 146 | + jsdocTags, |
| 147 | + rawComment, |
| 148 | + callFunction, |
| 149 | + subFunctions, |
| 150 | + __docsMetadata__: parsedMetadata, |
| 151 | + }; |
| 152 | +} |
| 153 | + |
| 154 | +/** |
| 155 | + * Gets the container node of the given variable declaration. |
| 156 | + * |
| 157 | + * A variable declaration may be annotated with e.g. `@initializerApiFunction`, |
| 158 | + * but the JSDoc tag is not attached to the node, but to the containing variable |
| 159 | + * statement. |
| 160 | + */ |
| 161 | +function getContainerVariableStatement(node: ts.VariableDeclaration): ts.VariableStatement|null { |
| 162 | + if (!ts.isVariableDeclarationList(node.parent)) { |
| 163 | + return null; |
| 164 | + } |
| 165 | + if (!ts.isVariableStatement(node.parent.parent)) { |
| 166 | + return null; |
| 167 | + } |
| 168 | + return node.parent.parent; |
| 169 | +} |
| 170 | + |
| 171 | +/** Filters the list signatures to valid initializer API signatures. */ |
| 172 | +function filterSignatureDeclarations(signatures: readonly ts.Signature[]) { |
| 173 | + const result: Array<ts.FunctionDeclaration|ts.CallSignatureDeclaration> = []; |
| 174 | + for (const signature of signatures) { |
| 175 | + const decl = signature.getDeclaration(); |
| 176 | + if (ts.isFunctionDeclaration(decl) || ts.isCallSignatureDeclaration(decl)) { |
| 177 | + result.push(decl); |
| 178 | + } |
| 179 | + } |
| 180 | + return result; |
| 181 | +} |
| 182 | + |
| 183 | +/** |
| 184 | + * Extracts all given signatures and returns them as a function with |
| 185 | + * overloads. |
| 186 | + * |
| 187 | + * The implementation of the function may be attached later, or may |
| 188 | + * be non-existent. E.g. initializer APIs declared using an interface |
| 189 | + * with call signatures do not have an associated implementation function |
| 190 | + * that is statically retrievable. The constant holds the overall API description. |
| 191 | + */ |
| 192 | +function extractFunctionWithOverloads( |
| 193 | + name: string, signatures: readonly ts.Signature[], |
| 194 | + typeChecker: ts.TypeChecker): FunctionWithOverloads { |
| 195 | + return { |
| 196 | + name, |
| 197 | + signatures: |
| 198 | + filterSignatureDeclarations(signatures) |
| 199 | + .map(s => ({ |
| 200 | + name, |
| 201 | + entryType: EntryType.Function, |
| 202 | + description: extractJsDocDescription(s), |
| 203 | + generics: extractGenerics(s), |
| 204 | + isNewType: false, |
| 205 | + jsdocTags: extractJsDocTags(s), |
| 206 | + params: extractAllParams(s.parameters, typeChecker), |
| 207 | + rawComment: extractRawJsDoc(s), |
| 208 | + returnType: typeChecker.typeToString(typeChecker.getReturnTypeOfSignature( |
| 209 | + typeChecker.getSignatureFromDeclaration(s)!)), |
| 210 | + })), |
| 211 | + // Implementation may be populated later. |
| 212 | + implementation: null, |
| 213 | + }; |
| 214 | +} |
| 215 | + |
| 216 | +/** Finds the implementation of the given function declaration overload signature. */ |
| 217 | +function findImplementationOfFunction( |
| 218 | + node: ts.FunctionDeclaration, typeChecker: ts.TypeChecker): ts.FunctionDeclaration|undefined { |
| 219 | + if (node.body !== undefined || node.name === undefined) { |
| 220 | + return node; |
| 221 | + } |
| 222 | + |
| 223 | + const symbol = typeChecker.getSymbolAtLocation(node.name); |
| 224 | + return symbol?.declarations?.find( |
| 225 | + (s): s is ts.FunctionDeclaration => ts.isFunctionDeclaration(s) && s.body !== undefined); |
| 226 | +} |
0 commit comments