UNPKG

tsickle

Version:

Transpile TypeScript code to JavaScript with Closure annotations.

239 lines 10.3 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.transformDecoratorJsdoc = exports.transformDecoratorsOutputForClosurePropertyRenaming = exports.hasExportingDecorator = exports.getDecoratorDeclarations = void 0; const ts = require("typescript"); const jsdoc = require("./jsdoc"); const transformer_util_1 = require("./transformer_util"); /** * Returns the declarations for the given decorator. */ function getDecoratorDeclarations(decorator, typeChecker) { // Walk down the expression to find the identifier of the decorator function. let node = decorator; while (node.kind !== ts.SyntaxKind.Identifier) { if (node.kind === ts.SyntaxKind.Decorator || node.kind === ts.SyntaxKind.CallExpression) { node = node.expression; } else { // We do not know how to handle this type of decorator. return []; } } let decSym = typeChecker.getSymbolAtLocation(node); if (!decSym) return []; if (decSym.flags & ts.SymbolFlags.Alias) { decSym = typeChecker.getAliasedSymbol(decSym); } return decSym.getDeclarations() || []; } exports.getDecoratorDeclarations = getDecoratorDeclarations; /** * Returns true if node has an exporting decorator (i.e., a decorator with @ExportDecoratedItems * in its JSDoc). */ function hasExportingDecorator(node, typeChecker) { return node.decorators && node.decorators.some(decorator => isExportingDecorator(decorator, typeChecker)); } exports.hasExportingDecorator = hasExportingDecorator; /** * Returns true if the given decorator has an @ExportDecoratedItems directive in its JSDoc. */ function isExportingDecorator(decorator, typeChecker) { return getDecoratorDeclarations(decorator, typeChecker).some(declaration => { const range = (0, transformer_util_1.getAllLeadingComments)(declaration); if (!range) { return false; } for (const { text } of range) { if (/@ExportDecoratedItems\b/.test(text)) { return true; } } return false; }); } /** * A transform pass that adds goog.reflect.objectProperty calls to the property * name string literals that are emitted as part of TypeScript's default * decorator output. * * goog.reflect.objectProperty is a special function that is recognized by * Closure Compiler. It is called like goog.reflect.objectProperty('prop', obj) * and it is compiled to a string literal that's the property named 'prop' on * the obj value. * * This way, runtime decorators can use the property names (e.g. to register * the property as a getter/setter pair) while still being compatible with * Closure Compiler's property renaming. * * Transforms: * * tslib_1.__decorate([ * decorator, * tslib_1.__metadata("design:type", Object) * ], Foo.prototype, "prop", void 0); * * Into: * * tslib_1.__decorate([ * decorator, * tslib_1.__metadata("design:type", Object) * ], Foo.prototype, * __googReflect.objectProperty("prop", Foo.prototype), void 0); */ function transformDecoratorsOutputForClosurePropertyRenaming(diagnostics) { return (context) => { const result = (sourceFile) => { let nodeNeedingGoogReflect = undefined; const visitor = (node) => { const replacementNode = rewriteDecorator(node); if (replacementNode) { nodeNeedingGoogReflect = node; return replacementNode; } return ts.visitEachChild(node, visitor, context); }; let updatedSourceFile = ts.visitNode(sourceFile, visitor); if (nodeNeedingGoogReflect !== undefined) { const statements = [...updatedSourceFile.statements]; const googModuleIndex = statements.findIndex(isGoogModuleStatement); if (googModuleIndex === -1) { (0, transformer_util_1.reportDiagnostic)(diagnostics, nodeNeedingGoogReflect, 'Internal tsickle error: could not find goog.module statement to import __tsickle_googReflect for decorator compilation.'); return sourceFile; } const googRequireReflectObjectProperty = ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration('__tsickle_googReflect', /* exclamationToken */ undefined, /* type */ undefined, ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('goog'), 'require'), undefined, [ts.factory.createStringLiteral('goog.reflect')]))], ts.NodeFlags.Const)); // The boilerplate we produce has a goog.module line, then two related // lines dealing with the `module` variable. Insert our goog.require // after that to avoid visually breaking up the module info, and to be // with the rest of the goog.require statements. statements.splice(googModuleIndex + 3, 0, googRequireReflectObjectProperty); updatedSourceFile = ts.factory.updateSourceFile(updatedSourceFile, ts.setTextRange(ts.factory.createNodeArray(statements), updatedSourceFile.statements), updatedSourceFile.isDeclarationFile, updatedSourceFile.referencedFiles, updatedSourceFile.typeReferenceDirectives, updatedSourceFile.hasNoDefaultLib, updatedSourceFile.libReferenceDirectives); } return updatedSourceFile; }; return result; }; } exports.transformDecoratorsOutputForClosurePropertyRenaming = transformDecoratorsOutputForClosurePropertyRenaming; /** * If `node` is a call to the tslib __decorate function, this returns a modified * call with the string argument replaced with * `__tsickle_googReflect.objectProperty('prop', TheClass.prototype)`. * * Returns undefined if no modification is necessary. */ function rewriteDecorator(node) { if (!ts.isCallExpression(node)) { return; } const identifier = node.expression; if (!ts.isIdentifier(identifier) || identifier.text !== '__decorate') { return; } const args = [...node.arguments]; if (args.length !== 4) { // Some decorators, like class decorators, have fewer arguments, and don't // need help to be renaming-safe. return; } const untypedFieldNameLiteral = args[2]; if (!ts.isStringLiteral(untypedFieldNameLiteral)) { // This is allowed, for example: // // const prop = Symbol(); // class Foo { // @decorate [prop] = 'val'; // } // // Nothing for us to do in that case. return; } const fieldNameLiteral = untypedFieldNameLiteral; args[2] = ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('__tsickle_googReflect'), 'objectProperty'), undefined, [ts.factory.createStringLiteral(fieldNameLiteral.text), args[1]]); return ts.factory.updateCallExpression(node, node.expression, node.typeArguments, args); } function isGoogModuleStatement(statement) { if (!ts.isExpressionStatement(statement)) { return false; } const expr = statement.expression; if (!ts.isCallExpression(expr)) { return false; } if (!ts.isPropertyAccessExpression(expr.expression)) { return false; } const goog = expr.expression.expression; if (!ts.isIdentifier(goog)) { return false; } return goog.text === 'goog' && expr.expression.name.text === 'module'; } const TAGS_CONFLICTING_WITH_DECORATE = new Set(['template', 'abstract']); /** * Removes problematic annotations from JsDoc comments. */ function sanitizeDecorateComments(comments) { const sanitized = []; for (const comment of comments) { const parsedComment = jsdoc.parse(comment); if (parsedComment && parsedComment.tags.length !== 0) { const filteredTags = parsedComment.tags.filter(t => !(TAGS_CONFLICTING_WITH_DECORATE.has(t.tagName))); if (filteredTags.length !== 0) { sanitized.push(jsdoc.toSynthesizedComment(filteredTags)); } } } return sanitized; } /** * A transformation pass that removes the annotations contained in * TAGS_CONFLICTING_WITH_DECORATE from toplevel statements of the form `ident = * tslib_1.__decorate(...)`. * * These call statements are generated for decorated classes and other * declarations. The leading comments of the declarations are cloned to the * `__decorate()` calls and may contain annotations that are not allowed in this * context and result in JSCompiler errors. */ function transformDecoratorJsdoc() { return () => { const transformer = (sourceFile) => { for (const stmt of sourceFile.statements) { // Only need to iterate over top-level statements in the source // file. if (!ts.isExpressionStatement(stmt)) continue; const comments = ts.getSyntheticLeadingComments(stmt); if (!comments || comments.length === 0) continue; const expr = stmt.expression; if (!ts.isBinaryExpression(expr)) continue; if (expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken) continue; const rhs = expr.right; if (!ts.isCallExpression(rhs)) continue; if (ts.isIdentifier(rhs.expression) && (rhs.expression.text === '__decorate')) { ts.setSyntheticLeadingComments(stmt, sanitizeDecorateComments(comments)); } } return sourceFile; }; return transformer; }; } exports.transformDecoratorJsdoc = transformDecoratorJsdoc; //# sourceMappingURL=decorators.js.map