|
| 1 | +/** |
| 2 | + * @license |
| 3 | + * Copyright Google Inc. 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 | +import * as ts from 'typescript'; |
| 9 | + |
| 10 | +export function downlevelConstructorParameters( |
| 11 | + getTypeChecker: () => ts.TypeChecker, |
| 12 | +): ts.TransformerFactory<ts.SourceFile> { |
| 13 | + return (context: ts.TransformationContext) => { |
| 14 | + const transformer = decoratorDownlevelTransformer(getTypeChecker(), []); |
| 15 | + |
| 16 | + return transformer(context); |
| 17 | + }; |
| 18 | +} |
| 19 | + |
| 20 | +// The following is sourced from tsickle with local modifications |
| 21 | +// Only the creation of `ctorParameters` is retained |
| 22 | +// tslint:disable-next-line:max-line-length |
| 23 | +// https://github.com/angular/tsickle/blob/0ceb7d6bc47f6945a6c4c09689f1388eb48f5c07/src/decorator_downlevel_transformer.ts |
| 24 | +// |
| 25 | + |
| 26 | +/** |
| 27 | + * Extracts the type of the decorator (the function or expression invoked), as well as all the |
| 28 | + * arguments passed to the decorator. Returns an AST with the form: |
| 29 | + * |
| 30 | + * // For @decorator(arg1, arg2) |
| 31 | + * { type: decorator, args: [arg1, arg2] } |
| 32 | + */ |
| 33 | +function extractMetadataFromSingleDecorator( |
| 34 | + decorator: ts.Decorator, |
| 35 | + diagnostics: ts.Diagnostic[], |
| 36 | +): ts.ObjectLiteralExpression { |
| 37 | + const metadataProperties: ts.ObjectLiteralElementLike[] = []; |
| 38 | + const expr = decorator.expression; |
| 39 | + switch (expr.kind) { |
| 40 | + case ts.SyntaxKind.Identifier: |
| 41 | + // The decorator was a plain @Foo. |
| 42 | + metadataProperties.push(ts.createPropertyAssignment('type', expr)); |
| 43 | + break; |
| 44 | + case ts.SyntaxKind.CallExpression: |
| 45 | + // The decorator was a call, like @Foo(bar). |
| 46 | + const call = expr as ts.CallExpression; |
| 47 | + metadataProperties.push(ts.createPropertyAssignment('type', call.expression)); |
| 48 | + if (call.arguments.length) { |
| 49 | + const args: ts.Expression[] = []; |
| 50 | + for (const arg of call.arguments) { |
| 51 | + args.push(arg); |
| 52 | + } |
| 53 | + const argsArrayLiteral = ts.createArrayLiteral(args); |
| 54 | + argsArrayLiteral.elements.hasTrailingComma = true; |
| 55 | + metadataProperties.push(ts.createPropertyAssignment('args', argsArrayLiteral)); |
| 56 | + } |
| 57 | + break; |
| 58 | + default: |
| 59 | + diagnostics.push({ |
| 60 | + file: decorator.getSourceFile(), |
| 61 | + start: decorator.getStart(), |
| 62 | + length: decorator.getEnd() - decorator.getStart(), |
| 63 | + messageText: `${ |
| 64 | + ts.SyntaxKind[decorator.kind] |
| 65 | + } not implemented in gathering decorator metadata`, |
| 66 | + category: ts.DiagnosticCategory.Error, |
| 67 | + code: 0, |
| 68 | + }); |
| 69 | + break; |
| 70 | + } |
| 71 | + |
| 72 | + return ts.createObjectLiteral(metadataProperties); |
| 73 | +} |
| 74 | + |
| 75 | +/** |
| 76 | + * createCtorParametersClassProperty creates a static 'ctorParameters' property containing |
| 77 | + * downleveled decorator information. |
| 78 | + * |
| 79 | + * The property contains an arrow function that returns an array of object literals of the shape: |
| 80 | + * static ctorParameters = () => [{ |
| 81 | + * type: SomeClass|undefined, // the type of the param that's decorated, if it's a value. |
| 82 | + * decorators: [{ |
| 83 | + * type: DecoratorFn, // the type of the decorator that's invoked. |
| 84 | + * args: [ARGS], // the arguments passed to the decorator. |
| 85 | + * }] |
| 86 | + * }]; |
| 87 | + */ |
| 88 | +function createCtorParametersClassProperty( |
| 89 | + diagnostics: ts.Diagnostic[], |
| 90 | + entityNameToExpression: (n: ts.EntityName) => ts.Expression | undefined, |
| 91 | + ctorParameters: ParameterDecorationInfo[], |
| 92 | +): ts.PropertyDeclaration { |
| 93 | + const params: ts.Expression[] = []; |
| 94 | + |
| 95 | + for (const ctorParam of ctorParameters) { |
| 96 | + if (!ctorParam.type && ctorParam.decorators.length === 0) { |
| 97 | + params.push(ts.createNull()); |
| 98 | + continue; |
| 99 | + } |
| 100 | + |
| 101 | + const paramType = ctorParam.type |
| 102 | + ? typeReferenceToExpression(entityNameToExpression, ctorParam.type) |
| 103 | + : undefined; |
| 104 | + const members = [ |
| 105 | + ts.createPropertyAssignment('type', paramType || ts.createIdentifier('undefined')), |
| 106 | + ]; |
| 107 | + |
| 108 | + const decorators: ts.ObjectLiteralExpression[] = []; |
| 109 | + for (const deco of ctorParam.decorators) { |
| 110 | + decorators.push(extractMetadataFromSingleDecorator(deco, diagnostics)); |
| 111 | + } |
| 112 | + if (decorators.length) { |
| 113 | + members.push(ts.createPropertyAssignment('decorators', ts.createArrayLiteral(decorators))); |
| 114 | + } |
| 115 | + params.push(ts.createObjectLiteral(members)); |
| 116 | + } |
| 117 | + |
| 118 | + const initializer = ts.createArrowFunction( |
| 119 | + undefined, |
| 120 | + undefined, |
| 121 | + [], |
| 122 | + undefined, |
| 123 | + ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken), |
| 124 | + ts.createArrayLiteral(params, true), |
| 125 | + ); |
| 126 | + |
| 127 | + const ctorProp = ts.createProperty( |
| 128 | + undefined, |
| 129 | + [ts.createToken(ts.SyntaxKind.StaticKeyword)], |
| 130 | + 'ctorParameters', |
| 131 | + undefined, |
| 132 | + undefined, |
| 133 | + initializer, |
| 134 | + ); |
| 135 | + |
| 136 | + return ctorProp; |
| 137 | +} |
| 138 | + |
| 139 | +/** |
| 140 | + * Returns an expression representing the (potentially) value part for the given node. |
| 141 | + * |
| 142 | + * This is a partial re-implementation of TypeScript's serializeTypeReferenceNode. This is a |
| 143 | + * workaround for https://github.com/Microsoft/TypeScript/issues/17516 (serializeTypeReferenceNode |
| 144 | + * not being exposed). In practice this implementation is sufficient for Angular's use of type |
| 145 | + * metadata. |
| 146 | + */ |
| 147 | +function typeReferenceToExpression( |
| 148 | + entityNameToExpression: (n: ts.EntityName) => ts.Expression | undefined, |
| 149 | + node: ts.TypeNode, |
| 150 | +): ts.Expression | undefined { |
| 151 | + let kind = node.kind; |
| 152 | + if (ts.isLiteralTypeNode(node)) { |
| 153 | + // Treat literal types like their base type (boolean, string, number). |
| 154 | + kind = node.literal.kind; |
| 155 | + } |
| 156 | + switch (kind) { |
| 157 | + case ts.SyntaxKind.FunctionType: |
| 158 | + case ts.SyntaxKind.ConstructorType: |
| 159 | + return ts.createIdentifier('Function'); |
| 160 | + case ts.SyntaxKind.ArrayType: |
| 161 | + case ts.SyntaxKind.TupleType: |
| 162 | + return ts.createIdentifier('Array'); |
| 163 | + case ts.SyntaxKind.TypePredicate: |
| 164 | + case ts.SyntaxKind.TrueKeyword: |
| 165 | + case ts.SyntaxKind.FalseKeyword: |
| 166 | + case ts.SyntaxKind.BooleanKeyword: |
| 167 | + return ts.createIdentifier('Boolean'); |
| 168 | + case ts.SyntaxKind.StringLiteral: |
| 169 | + case ts.SyntaxKind.StringKeyword: |
| 170 | + return ts.createIdentifier('String'); |
| 171 | + case ts.SyntaxKind.ObjectKeyword: |
| 172 | + return ts.createIdentifier('Object'); |
| 173 | + case ts.SyntaxKind.NumberKeyword: |
| 174 | + case ts.SyntaxKind.NumericLiteral: |
| 175 | + return ts.createIdentifier('Number'); |
| 176 | + case ts.SyntaxKind.TypeReference: |
| 177 | + const typeRef = node as ts.TypeReferenceNode; |
| 178 | + |
| 179 | + // Ignore any generic types, just return the base type. |
| 180 | + return entityNameToExpression(typeRef.typeName); |
| 181 | + default: |
| 182 | + return undefined; |
| 183 | + } |
| 184 | +} |
| 185 | + |
| 186 | +/** ParameterDecorationInfo describes the information for a single constructor parameter. */ |
| 187 | +interface ParameterDecorationInfo { |
| 188 | + /** |
| 189 | + * The type declaration for the parameter. Only set if the type is a value (e.g. a class, not an |
| 190 | + * interface). |
| 191 | + */ |
| 192 | + type: ts.TypeNode | null; |
| 193 | + /** The list of decorators found on the parameter, null if none. */ |
| 194 | + decorators: ts.Decorator[]; |
| 195 | +} |
| 196 | + |
| 197 | +/** |
| 198 | + * Transformer factory for the decorator downlevel transformer. See fileoverview for details. |
| 199 | + */ |
| 200 | +export function decoratorDownlevelTransformer( |
| 201 | + typeChecker: ts.TypeChecker, |
| 202 | + diagnostics: ts.Diagnostic[], |
| 203 | +): (context: ts.TransformationContext) => ts.Transformer<ts.SourceFile> { |
| 204 | + return (context: ts.TransformationContext) => { |
| 205 | + /** A map from symbols to the identifier of an import, reset per SourceFile. */ |
| 206 | + let importNamesBySymbol = new Map<ts.Symbol, ts.Identifier>(); |
| 207 | + |
| 208 | + /** |
| 209 | + * Converts an EntityName (from a type annotation) to an expression (accessing a value). |
| 210 | + * |
| 211 | + * For a given ts.EntityName, this walks depth first to find the leftmost ts.Identifier, then |
| 212 | + * converts the path into property accesses. |
| 213 | + * |
| 214 | + * This generally works, but TypeScript's emit pipeline does not serialize identifiers that are |
| 215 | + * only used in a type location (such as identifiers in a TypeNode), even if the identifier |
| 216 | + * itself points to a value (e.g. a class). To avoid that problem, this method finds the symbol |
| 217 | + * representing the identifier (using typeChecker), then looks up where it was imported (using |
| 218 | + * importNamesBySymbol), and then uses the imported name instead of the identifier from the type |
| 219 | + * expression, if any. Otherwise it'll use the identifier unchanged. This makes sure the |
| 220 | + * identifier is not marked as stemming from a "type only" expression, causing it to be emitted |
| 221 | + * and causing the import to be retained. |
| 222 | + */ |
| 223 | + function entityNameToExpression(name: ts.EntityName): ts.Expression | undefined { |
| 224 | + const sym = typeChecker.getSymbolAtLocation(name); |
| 225 | + if (!sym) { |
| 226 | + return undefined; |
| 227 | + } |
| 228 | + // Check if the entity name references a symbol that is an actual value. If it is not, it |
| 229 | + // cannot be referenced by an expression, so return undefined. |
| 230 | + let symToCheck = sym; |
| 231 | + if (symToCheck.flags & ts.SymbolFlags.Alias) { |
| 232 | + symToCheck = typeChecker.getAliasedSymbol(symToCheck); |
| 233 | + } |
| 234 | + if (!(symToCheck.flags & ts.SymbolFlags.Value)) { |
| 235 | + return undefined; |
| 236 | + } |
| 237 | + |
| 238 | + if (ts.isIdentifier(name)) { |
| 239 | + // If there's a known import name for this symbol, use it so that the import will be |
| 240 | + // retained and the value can be referenced. |
| 241 | + const value = importNamesBySymbol.get(sym); |
| 242 | + if (value) { |
| 243 | + return value; |
| 244 | + } |
| 245 | + |
| 246 | + // Otherwise this will be a locally declared name, just return that. |
| 247 | + return name; |
| 248 | + } |
| 249 | + const ref = entityNameToExpression(name.left); |
| 250 | + if (!ref) { |
| 251 | + return undefined; |
| 252 | + } |
| 253 | + |
| 254 | + return ts.createPropertyAccess(ref, name.right); |
| 255 | + } |
| 256 | + |
| 257 | + /** |
| 258 | + * Transforms a constructor. Returns the transformed constructor and the list of parameter |
| 259 | + * information collected, consisting of decorators and optional type. |
| 260 | + */ |
| 261 | + function transformConstructor( |
| 262 | + ctor: ts.ConstructorDeclaration, |
| 263 | + ): [ts.ConstructorDeclaration, ParameterDecorationInfo[]] { |
| 264 | + ctor = ts.visitEachChild(ctor, visitor, context); |
| 265 | + |
| 266 | + const parametersInfo: ParameterDecorationInfo[] = []; |
| 267 | + for (const param of ctor.parameters) { |
| 268 | + const paramInfo: ParameterDecorationInfo = { decorators: [], type: null }; |
| 269 | + |
| 270 | + for (const decorator of param.decorators || []) { |
| 271 | + paramInfo.decorators.push(decorator); |
| 272 | + } |
| 273 | + if (param.type) { |
| 274 | + // param has a type provided, e.g. "foo: Bar". |
| 275 | + // The type will be emitted as a value expression in entityNameToExpression, which takes |
| 276 | + // care not to emit anything for types that cannot be expressed as a value (e.g. |
| 277 | + // interfaces). |
| 278 | + paramInfo.type = param.type; |
| 279 | + } |
| 280 | + parametersInfo.push(paramInfo); |
| 281 | + } |
| 282 | + |
| 283 | + return [ctor, parametersInfo]; |
| 284 | + } |
| 285 | + |
| 286 | + /** |
| 287 | + * Transforms a single class declaration: |
| 288 | + * - creates a ctorParameters property |
| 289 | + */ |
| 290 | + function transformClassDeclaration(classDecl: ts.ClassDeclaration): ts.ClassDeclaration { |
| 291 | + if (!classDecl.decorators || classDecl.decorators.length === 0) { |
| 292 | + return classDecl; |
| 293 | + } |
| 294 | + |
| 295 | + const newMembers: ts.ClassElement[] = []; |
| 296 | + let classParameters: ParameterDecorationInfo[] | null = null; |
| 297 | + |
| 298 | + for (const member of classDecl.members) { |
| 299 | + switch (member.kind) { |
| 300 | + case ts.SyntaxKind.Constructor: { |
| 301 | + const ctor = member as ts.ConstructorDeclaration; |
| 302 | + if (!ctor.body) { |
| 303 | + break; |
| 304 | + } |
| 305 | + |
| 306 | + const [newMember, parametersInfo] = transformConstructor( |
| 307 | + member as ts.ConstructorDeclaration, |
| 308 | + ); |
| 309 | + classParameters = parametersInfo; |
| 310 | + newMembers.push(newMember); |
| 311 | + continue; |
| 312 | + } |
| 313 | + default: |
| 314 | + break; |
| 315 | + } |
| 316 | + newMembers.push(ts.visitEachChild(member, visitor, context)); |
| 317 | + } |
| 318 | + |
| 319 | + const newClassDeclaration = ts.getMutableClone(classDecl); |
| 320 | + |
| 321 | + if (classParameters) { |
| 322 | + newMembers.push( |
| 323 | + createCtorParametersClassProperty(diagnostics, entityNameToExpression, classParameters), |
| 324 | + ); |
| 325 | + } |
| 326 | + |
| 327 | + newClassDeclaration.members = ts.setTextRange( |
| 328 | + ts.createNodeArray(newMembers, newClassDeclaration.members.hasTrailingComma), |
| 329 | + classDecl.members, |
| 330 | + ); |
| 331 | + |
| 332 | + return newClassDeclaration; |
| 333 | + } |
| 334 | + |
| 335 | + function visitor(node: ts.Node): ts.Node { |
| 336 | + switch (node.kind) { |
| 337 | + case ts.SyntaxKind.SourceFile: { |
| 338 | + importNamesBySymbol = new Map<ts.Symbol, ts.Identifier>(); |
| 339 | + |
| 340 | + return ts.visitEachChild(node, visitor, context); |
| 341 | + } |
| 342 | + case ts.SyntaxKind.ImportDeclaration: { |
| 343 | + const impDecl = node as ts.ImportDeclaration; |
| 344 | + if (impDecl.importClause) { |
| 345 | + const importClause = impDecl.importClause; |
| 346 | + const names = []; |
| 347 | + if (importClause.name) { |
| 348 | + names.push(importClause.name); |
| 349 | + } |
| 350 | + if ( |
| 351 | + importClause.namedBindings && |
| 352 | + importClause.namedBindings.kind === ts.SyntaxKind.NamedImports |
| 353 | + ) { |
| 354 | + const namedImports = importClause.namedBindings as ts.NamedImports; |
| 355 | + names.push(...namedImports.elements.map(e => e.name)); |
| 356 | + } |
| 357 | + for (const name of names) { |
| 358 | + const sym = typeChecker.getSymbolAtLocation(name); |
| 359 | + if (sym) { |
| 360 | + importNamesBySymbol.set(sym, name); |
| 361 | + } |
| 362 | + } |
| 363 | + } |
| 364 | + |
| 365 | + return ts.visitEachChild(node, visitor, context); |
| 366 | + } |
| 367 | + case ts.SyntaxKind.ClassDeclaration: { |
| 368 | + return transformClassDeclaration(node as ts.ClassDeclaration); |
| 369 | + } |
| 370 | + default: |
| 371 | + return ts.visitEachChild(node, visitor, context); |
| 372 | + } |
| 373 | + } |
| 374 | + |
| 375 | + return (sf: ts.SourceFile) => visitor(sf) as ts.SourceFile; |
| 376 | + }; |
| 377 | +} |
0 commit comments