UNPKG

tsickle

Version:

Transpile TypeScript code to JavaScript with Closure annotations.

489 lines 24.2 kB
"use strict"; /** * @license * Copyright Google Inc. 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 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.decoratorDownlevelTransformer = exports.shouldLower = void 0; /** * @fileoverview Decorator downleveling support. tsickle can optionally convert decorator calls * into annotations. For example, a decorator application on a method: * class X { * @Foo(1, 2) * bar() { ... } * } * Will get converted to: * class X { * bar() { ... } * static propDecorators = { * bar: {type: Foo, args: [1, 2]} * } * } * Similarly for decorators on the class (property 'decorators') and decorators on the constructor * (property 'ctorParameters', including the types of all arguments of the constructor). * * This is used by, among other software, Angular in its "non-AoT" mode to inspect decorator * invocations. */ const ts = require("typescript"); const decorators_1 = require("./decorators"); const jsdoc = require("./jsdoc"); const transformer_util_1 = require("./transformer_util"); /** * Returns true if the given decorator should be downleveled. * * Decorators that have JSDoc on them including the `@Annotation` tag are downleveled and converted * into properties on the class by this pass. */ function shouldLower(decorator, typeChecker) { for (const d of (0, decorators_1.getDecoratorDeclarations)(decorator, typeChecker)) { // TODO(lucassloan): // Switch to the TS JSDoc parser in the future to avoid false positives here. // For example using '@Annotation' in a true comment. // However, a new TS API would be needed, track at // https://github.com/Microsoft/TypeScript/issues/7393. let commentNode = d; // Not handling PropertyAccess expressions here, because they are // filtered earlier. if (commentNode.kind === ts.SyntaxKind.VariableDeclaration) { if (!commentNode.parent) continue; commentNode = commentNode.parent; } // Go up one more level to VariableDeclarationStatement, where usually // the comment lives. If the declaration has an 'export', the // VDList.getFullText will not contain the comment. if (commentNode.kind === ts.SyntaxKind.VariableDeclarationList) { if (!commentNode.parent) continue; commentNode = commentNode.parent; } const range = (0, transformer_util_1.getAllLeadingComments)(commentNode); if (!range) continue; for (const { text } of range) { if (text.includes('@Annotation')) return true; } } return false; } exports.shouldLower = shouldLower; const DECORATOR_INVOCATION_JSDOC_TYPE = '!Array<{type: !Function, args: (undefined|!Array<?>)}>'; function addJSDocTypeAnnotation(node, jsdocType) { ts.setSyntheticLeadingComments(node, [ jsdoc.toSynthesizedComment([{ tagName: 'type', type: jsdocType, }]), ]); } /** * Extracts the type of the decorator (the function or expression invoked), as well as all the * arguments passed to the decorator. Returns an AST with the form: * * // For @decorator(arg1, arg2) * { type: decorator, args: [arg1, arg2] } */ function extractMetadataFromSingleDecorator(decorator, diagnostics) { const metadataProperties = []; const expr = decorator.expression; switch (expr.kind) { case ts.SyntaxKind.Identifier: // The decorator was a plain @Foo. metadataProperties.push(ts.factory.createPropertyAssignment('type', expr)); break; case ts.SyntaxKind.CallExpression: // The decorator was a call, like @Foo(bar). const call = expr; metadataProperties.push(ts.factory.createPropertyAssignment('type', call.expression)); if (call.arguments.length) { const args = []; for (const arg of call.arguments) { args.push(arg); } const argsArrayLiteral = ts.factory.createArrayLiteralExpression(ts.factory.createNodeArray(args, /* hasTrailingComma */ true)); metadataProperties.push(ts.factory.createPropertyAssignment('args', argsArrayLiteral)); } break; default: diagnostics.push({ file: decorator.getSourceFile(), start: decorator.getStart(), length: decorator.getEnd() - decorator.getStart(), messageText: `${ts.SyntaxKind[decorator.kind]} not implemented in gathering decorator metadata`, category: ts.DiagnosticCategory.Error, code: 0, }); break; } return ts.factory.createObjectLiteralExpression(metadataProperties); } /** * Takes a list of decorator metadata object ASTs and produces an AST for a * static class property of an array of those metadata objects. */ function createDecoratorClassProperty(decoratorList) { const modifier = ts.factory.createToken(ts.SyntaxKind.StaticKeyword); const initializer = ts.factory.createArrayLiteralExpression(ts.factory.createNodeArray(decoratorList, /* hasTrailingComma */ true), true); const prop = ts.factory.createPropertyDeclaration(undefined, [modifier], 'decorators', undefined, undefined, initializer); addJSDocTypeAnnotation(prop, DECORATOR_INVOCATION_JSDOC_TYPE); // NB: the .decorators property does not get a @nocollapse property. There is // no good reason why - it means .decorators is not runtime accessible if you // compile with collapse properties, whereas propDecorators is, which doesn't // follow any stringent logic. However this has been the case previously, and // adding it back in leads to substantial code size increases as Closure fails // to tree shake these props without @nocollapse. return prop; } /** * createCtorParametersClassProperty creates a static 'ctorParameters' property containing * downleveled decorator information. * * The property contains an arrow function that returns an array of object literals of the shape: * static ctorParameters = () => [{ * type: SomeClass|undefined, // the type of the param that's decorated, if it's a value. * decorators: [{ * type: DecoratorFn, // the type of the decorator that's invoked. * args: [ARGS], // the arguments passed to the decorator. * }] * }]; */ function createCtorParametersClassProperty(diagnostics, entityNameToExpression, ctorParameters) { const params = []; for (const ctorParam of ctorParameters) { if (!ctorParam.type && ctorParam.decorators.length === 0) { params.push(ts.factory.createNull()); continue; } const paramType = ctorParam.type ? typeReferenceToExpression(entityNameToExpression, ctorParam.type) : undefined; const members = [ts.factory.createPropertyAssignment('type', paramType || ts.factory.createIdentifier('undefined'))]; const decorators = []; for (const deco of ctorParam.decorators) { decorators.push(extractMetadataFromSingleDecorator(deco, diagnostics)); } if (decorators.length) { members.push(ts.factory.createPropertyAssignment('decorators', ts.factory.createArrayLiteralExpression(decorators))); } params.push(ts.factory.createObjectLiteralExpression(members)); } const initializer = ts.factory.createArrowFunction(undefined, undefined, [], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createArrayLiteralExpression(params, true)); const ctorProp = ts.factory.createPropertyDeclaration(undefined, [ts.factory.createToken(ts.SyntaxKind.StaticKeyword)], 'ctorParameters', undefined, undefined, initializer); ts.setSyntheticLeadingComments(ctorProp, [ jsdoc.toSynthesizedComment([ { tagName: 'type', type: lines(`function(): !Array<(null|{`, ` type: ?,`, ` decorators: (undefined|${DECORATOR_INVOCATION_JSDOC_TYPE}),`, `})>`), }, { tagName: 'nocollapse' } ]), ]); return ctorProp; } /** * createPropDecoratorsClassProperty creates a static 'propDecorators' property containing type * information for every property that has a decorator applied. * * static propDecorators: {[key: string]: {type: Function, args?: any[]}[]} = { * propA: [{type: MyDecorator, args: [1, 2]}, ...], * ... * }; */ function createPropDecoratorsClassProperty(diagnostics, properties) { // `static propDecorators: {[key: string]: ` + {type: Function, args?: any[]}[] + `} = {\n`); const entries = []; for (const [name, decorators] of properties.entries()) { entries.push(ts.factory.createPropertyAssignment(name, ts.factory.createArrayLiteralExpression(decorators.map(deco => extractMetadataFromSingleDecorator(deco, diagnostics))))); } const initializer = ts.factory.createObjectLiteralExpression(entries, true); const prop = ts.factory.createPropertyDeclaration(undefined, [ts.factory.createToken(ts.SyntaxKind.StaticKeyword)], 'propDecorators', undefined, undefined, initializer); addJSDocTypeAnnotation(prop, `!Object<string, ${DECORATOR_INVOCATION_JSDOC_TYPE}>`); return prop; } /** * Returns an expression representing the (potentially) value part for the given node. * * This is a partial re-implementation of TypeScript's serializeTypeReferenceNode. This is a * workaround for https://github.com/Microsoft/TypeScript/issues/17516 (serializeTypeReferenceNode * not being exposed). In practice this implementation is sufficient for Angular's use of type * metadata. */ function typeReferenceToExpression(entityNameToExpression, node) { let kind = node.kind; if (ts.isLiteralTypeNode(node)) { // Treat literal types like their base type (boolean, string, number). kind = node.literal.kind; } switch (kind) { case ts.SyntaxKind.FunctionType: case ts.SyntaxKind.ConstructorType: return ts.factory.createIdentifier('Function'); case ts.SyntaxKind.ArrayType: case ts.SyntaxKind.TupleType: return ts.factory.createIdentifier('Array'); case ts.SyntaxKind.TypePredicate: case ts.SyntaxKind.TrueKeyword: case ts.SyntaxKind.FalseKeyword: case ts.SyntaxKind.BooleanKeyword: return ts.factory.createIdentifier('Boolean'); case ts.SyntaxKind.StringLiteral: case ts.SyntaxKind.StringKeyword: return ts.factory.createIdentifier('String'); case ts.SyntaxKind.ObjectKeyword: return ts.factory.createIdentifier('Object'); case ts.SyntaxKind.NumberKeyword: case ts.SyntaxKind.NumericLiteral: return ts.factory.createIdentifier('Number'); case ts.SyntaxKind.TypeReference: const typeRef = node; // Ignore any generic types, just return the base type. return entityNameToExpression(typeRef.typeName); default: return undefined; } } /** * Transformer factory for the decorator downlevel transformer. See fileoverview for details. */ function decoratorDownlevelTransformer(typeChecker, diagnostics) { return (context) => { /** A map from symbols to the identifier of an import, reset per SourceFile. */ let importNamesBySymbol = new Map(); /** * Converts an EntityName (from a type annotation) to an expression (accessing a value). * * For a given ts.EntityName, this walks depth first to find the leftmost ts.Identifier, then * converts the path into property accesses. * * This generally works, but TypeScript's emit pipeline does not serialize identifiers that are * only used in a type location (such as identifiers in a TypeNode), even if the identifier * itself points to a value (e.g. a class). To avoid that problem, this method finds the symbol * representing the identifier (using typeChecker), then looks up where it was imported (using * importNamesBySymbol), and then uses the imported name instead of the identifier from the type * expression, if any. Otherwise it'll use the identifier unchanged. This makes sure the * identifier is not marked as stemming from a "type only" expression, causing it to be emitted * and causing the import to be retained. */ function entityNameToExpression(name) { const sym = typeChecker.getSymbolAtLocation(name); if (!sym) return undefined; // Check if the entity name references a symbol that is an actual value. If it is not, it // cannot be referenced by an expression, so return undefined. if (!(0, transformer_util_1.symbolIsValue)(typeChecker, sym)) return undefined; if (ts.isIdentifier(name)) { // If there's a known import name for this symbol, use it so that the import will be // retained and the value can be referenced. if (importNamesBySymbol.has(sym)) return importNamesBySymbol.get(sym); // Otherwise this will be a locally declared name, just return that. return name; } const ref = entityNameToExpression(name.left); if (!ref) return undefined; return ts.factory.createPropertyAccessExpression(ref, name.right); } /** * Transforms a class element. Returns a three tuple of name, transformed element, and * decorators found. Returns an undefined name if there are no decorators to lower on the * element, or the element has an exotic name. */ function transformClassElement(element) { var _a; element = ts.visitEachChild(element, visitor, context); const decoratorsToKeep = []; const toLower = []; for (const decorator of element.decorators || []) { if (!shouldLower(decorator, typeChecker)) { decoratorsToKeep.push(decorator); continue; } toLower.push(decorator); } if (!toLower.length) return [undefined, element, []]; if (!element.name || element.name.kind !== ts.SyntaxKind.Identifier) { // Method has a weird name, e.g. // [Symbol.foo]() {...} diagnostics.push({ file: element.getSourceFile(), start: element.getStart(), length: element.getEnd() - element.getStart(), messageText: `cannot process decorators on strangely named method`, category: ts.DiagnosticCategory.Error, code: 0, }); return [undefined, element, []]; } const name = element.name.text; let newNode; const decorators = decoratorsToKeep.length ? ts.setTextRange(ts.factory.createNodeArray(decoratorsToKeep), element.decorators) : undefined; switch (element.kind) { case ts.SyntaxKind.PropertyDeclaration: newNode = ts.factory.updatePropertyDeclaration(element, decorators, element.modifiers, element.name, (_a = element.questionToken) !== null && _a !== void 0 ? _a : element.exclamationToken, element.type, element.initializer); break; case ts.SyntaxKind.GetAccessor: newNode = ts.factory.updateGetAccessorDeclaration(element, decorators, element.modifiers, element.name, element.parameters, element.type, element.body); break; case ts.SyntaxKind.SetAccessor: newNode = ts.factory.updateSetAccessorDeclaration(element, decorators, element.modifiers, element.name, element.parameters, element.body); break; case ts.SyntaxKind.MethodDeclaration: newNode = ts.factory.updateMethodDeclaration(element, decorators, element.modifiers, element.asteriskToken, element.name, element.questionToken, element.typeParameters, element.parameters, element.type, element.body); break; default: throw new Error(`unexpected element: ${element}`); } return [name, newNode, toLower]; } /** * Transforms a constructor. Returns the transformed constructor and the list of parameter * information collected, consisting of decorators and optional type. */ function transformConstructor(ctor) { ctor = ts.visitEachChild(ctor, visitor, context); const newParameters = []; const oldParameters = ts.visitParameterList(ctor.parameters, visitor, context); const parametersInfo = []; for (const param of oldParameters) { const decoratorsToKeep = []; const paramInfo = { decorators: [], type: null }; for (const decorator of param.decorators || []) { if (!shouldLower(decorator, typeChecker)) { decoratorsToKeep.push(decorator); continue; } paramInfo.decorators.push(decorator); } if (param.type) { // param has a type provided, e.g. "foo: Bar". // The type will be emitted as a value expression in entityNameToExpression, which takes // care not to emit anything for types that cannot be expressed as a value (e.g. // interfaces). paramInfo.type = param.type; } parametersInfo.push(paramInfo); const newParam = ts.factory.updateParameterDeclaration(param, // Must pass 'undefined' to avoid emitting decorator metadata. decoratorsToKeep.length ? decoratorsToKeep : undefined, param.modifiers, param.dotDotDotToken, param.name, param.questionToken, param.type, param.initializer); newParameters.push(newParam); } const updated = ts.factory.updateConstructorDeclaration(ctor, ctor.decorators, ctor.modifiers, newParameters, ts.visitFunctionBody(ctor.body, visitor, context)); return [updated, parametersInfo]; } /** * Transforms a single class declaration: * - dispatches to strip decorators on members * - converts decorators on the class to annotations * - creates a ctorParameters property * - creates a propDecorators property */ function transformClassDeclaration(classDecl) { const newMembers = []; const decoratedProperties = new Map(); let classParameters = null; for (const member of classDecl.members) { switch (member.kind) { case ts.SyntaxKind.PropertyDeclaration: case ts.SyntaxKind.GetAccessor: case ts.SyntaxKind.SetAccessor: case ts.SyntaxKind.MethodDeclaration: { const [name, newMember, decorators] = transformClassElement(member); newMembers.push(newMember); if (name) decoratedProperties.set(name, decorators); continue; } case ts.SyntaxKind.Constructor: { const ctor = member; if (!ctor.body) break; const [newMember, parametersInfo] = transformConstructor(member); classParameters = parametersInfo; newMembers.push(newMember); continue; } default: break; } newMembers.push(ts.visitEachChild(member, visitor, context)); } const decorators = classDecl.decorators || []; const decoratorsToLower = []; const decoratorsToKeep = []; for (const decorator of decorators) { if (shouldLower(decorator, typeChecker)) { decoratorsToLower.push(extractMetadataFromSingleDecorator(decorator, diagnostics)); } else { decoratorsToKeep.push(decorator); } } if (decoratorsToLower.length) { newMembers.push(createDecoratorClassProperty(decoratorsToLower)); } if (classParameters) { if ((decoratorsToLower.length) || classParameters.some(p => !!p.decorators.length)) { // emit ctorParameters if the class was decoratored at all, or if any of its ctors // were classParameters newMembers.push(createCtorParametersClassProperty(diagnostics, entityNameToExpression, classParameters)); } } if (decoratedProperties.size) { newMembers.push(createPropDecoratorsClassProperty(diagnostics, decoratedProperties)); } const newDecorators = decoratorsToKeep.length ? ts.factory.createNodeArray(decoratorsToKeep) : undefined; return ts.factory.updateClassDeclaration(classDecl, newDecorators, classDecl.modifiers, classDecl.name, classDecl.typeParameters, classDecl.heritageClauses, ts.setTextRange(ts.factory.createNodeArray(newMembers, classDecl.members.hasTrailingComma), classDecl.members)); } function visitor(node) { switch (node.kind) { case ts.SyntaxKind.SourceFile: { importNamesBySymbol = new Map(); return ts.visitEachChild(node, visitor, context); } case ts.SyntaxKind.ImportDeclaration: { const impDecl = node; if (impDecl.importClause) { const importClause = impDecl.importClause; const names = []; if (importClause.name) { names.push(importClause.name); } if (importClause.namedBindings && importClause.namedBindings.kind === ts.SyntaxKind.NamedImports) { const namedImports = importClause.namedBindings; names.push(...namedImports.elements.map(e => e.name)); } for (const name of names) { const sym = typeChecker.getSymbolAtLocation(name); importNamesBySymbol.set(sym, name); } } return ts.visitEachChild(node, visitor, context); } case ts.SyntaxKind.ClassDeclaration: { return transformClassDeclaration(node); } default: return (0, transformer_util_1.visitEachChild)(node, visitor, context); } } return (sf) => visitor(sf); }; } exports.decoratorDownlevelTransformer = decoratorDownlevelTransformer; function lines(...s) { return s.join('\n'); } //# sourceMappingURL=decorator_downlevel_transformer.js.map