UNPKG

tsickle

Version:

Transpile TypeScript code to JavaScript with Closure annotations.

221 lines 11.1 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.enumTransformer = exports.getEnumType = void 0; /** * @fileoverview Transforms TypeScript enum declarations to Closure enum declarations, which * look like: * * /.. @enum {number} ./ * const Foo = {BAR: 0, BAZ: 1, ...}; * export {Foo}; // even if originally exported on one line. * * This declares an enum type for Closure Compiler (and Closure JS users of this TS code). * Splitting the enum into declaration and export is required so that local references to the * type resolve ("@type {Foo}"). */ const ts = require("typescript"); const transformer_util_1 = require("./transformer_util"); /** * isInUnsupportedNamespace returns true if any of node's ancestors is a * namespace (ModuleDeclaration) that is not a transformed declaration merging * namespace. */ function isInUnsupportedNamespace(node) { // Must use the original node because node might have already been transformed, with node.parent // no longer being set. let parent = ts.getOriginalNode(node).parent; while (parent) { if (parent.kind === ts.SyntaxKind.ModuleDeclaration) { return !(0, transformer_util_1.isTransformedDeclMergeNs)(parent); } parent = parent.parent; } return false; } /** * getEnumMemberType computes the type of an enum member by inspecting its initializer expression. */ function getEnumMemberType(typeChecker, member) { // Enum members without initialization have type 'number' if (!member.initializer) { return 'number'; } const type = typeChecker.getTypeAtLocation(member.initializer); // Note: checking against 'NumberLike' instead of just 'Number' means this code // handles both // MEMBER = 3, // TypeFlags.NumberLiteral // and // MEMBER = someFunction(), // TypeFlags.Number if (type.flags & ts.TypeFlags.NumberLike) { return 'number'; } // If the value is not a number, it must be a string. // TypeScript does not allow enum members to have any other type. return 'string'; } /** * getEnumType computes the Closure type of an enum, by iterating through the members and gathering * their types. */ function getEnumType(typeChecker, enumDecl) { let hasNumber = false; let hasString = false; for (const member of enumDecl.members) { const type = getEnumMemberType(typeChecker, member); if (type === 'string') { hasString = true; } else if (type === 'number') { hasNumber = true; } } if (hasNumber && hasString) { return '?'; // Closure's new type inference doesn't support enums of unions. } else if (hasNumber) { return 'number'; } else if (hasString) { return 'string'; } else { // Perhaps an empty enum? return '?'; } } exports.getEnumType = getEnumType; /** * Transformer factory for the enum transformer. See fileoverview for details. */ function enumTransformer(typeChecker) { return (context) => { function visitor(node) { if (!ts.isEnumDeclaration(node)) return ts.visitEachChild(node, visitor, context); // TODO(martinprobst): The enum transformer does not work for enums embedded in namespaces, // because TS does not support splitting export and declaration ("export {Foo};") in // namespaces. tsickle's emit for namespaces is unintelligible for Closure in any case, so // this is left to fix for another day. if (isInUnsupportedNamespace(node)) { return ts.visitEachChild(node, visitor, context); } // TypeScript does not emit any code for ambient enums, so early exit here to prevent the code // below from producing runtime values for an ambient structure. if ((0, transformer_util_1.isAmbient)(node)) return ts.visitEachChild(node, visitor, context); const isExported = (0, transformer_util_1.hasModifierFlag)(node, ts.ModifierFlags.Export); const enumType = getEnumType(typeChecker, node); const values = []; let enumIndex = 0; for (const member of node.members) { let enumValue; if (member.initializer) { const enumConstValue = typeChecker.getConstantValue(member); if (typeof enumConstValue === 'number') { enumIndex = enumConstValue + 1; enumValue = ts.factory.createNumericLiteral(enumConstValue); } else if (typeof enumConstValue === 'string') { // tsickle does not care about string enum values. However TypeScript expects compile // time constant enum values to be replaced with their constant expression, and e.g. // doesn't emit imports for modules referenced in them. Because tsickle replaces the // enum with an object literal, i.e. handles the enum transform, it must thus also do // the const value substitution for strings. enumValue = ts.factory.createStringLiteral(enumConstValue); } else { // Non-numeric enum value (string or an expression). // Emit this initializer expression as-is. // Note: if the member's initializer expression refers to another // value within the enum (e.g. something like // enum Foo { // Field1, // Field2 = Field1 + something(), // } // Then when we emit the initializer we produce invalid code because // on the Closure side the reference to Field1 has to be namespaced, // e.g. written "Foo.Field1 + something()". // Hopefully this doesn't come up often -- if the enum instead has // something like // Field2 = Field1 + 3, // then it's still a constant expression and we inline the constant // value in the above branch of this "if" statement. enumValue = visitor(member.initializer); } } else { enumValue = ts.factory.createNumericLiteral(enumIndex); enumIndex++; } values.push(ts.setOriginalNode(ts.setTextRange(ts.factory.createPropertyAssignment(member.name, enumValue), member), member)); } const varDecl = ts.factory.createVariableDeclaration(node.name, /* exclamationToken */ undefined, /* type */ undefined, ts.factory.createObjectLiteralExpression(ts.setTextRange(ts.factory.createNodeArray(values, true), node.members), true)); const varDeclStmt = ts.setOriginalNode(ts.setTextRange(ts.factory.createVariableStatement( /* modifiers */ undefined, ts.factory.createVariableDeclarationList([varDecl], /* create a const var */ ts.NodeFlags.Const)), node), node); const comment = { kind: ts.SyntaxKind.MultiLineCommentTrivia, text: `* @enum {${enumType}} `, hasTrailingNewLine: true, pos: -1, end: -1 }; ts.setSyntheticLeadingComments(varDeclStmt, [comment]); const name = (0, transformer_util_1.getIdentifierText)(node.name); const resultNodes = [varDeclStmt]; if (isExported) { // Create a separate export {...} statement, so that the enum name can be used in local // type annotations within the file. resultNodes.push(ts.factory.createExportDeclaration( /* decorators */ undefined, /* modifiers */ undefined, /* isTypeOnly */ false, ts.factory.createNamedExports([ts.factory.createExportSpecifier( /* isTypeOnly */ false, undefined, name)]))); } if ((0, transformer_util_1.hasModifierFlag)(node, ts.ModifierFlags.Const)) { // By TypeScript semantics, const enums disappear after TS compilation. // We still need to generate the runtime value above to make Closure Compiler's type system // happy and allow refering to enums from JS code, but we should at least not emit string // value mappings. return resultNodes; } // Emit the reverse mapping of foo[foo.BAR] = 'BAR'; lines for number enum members for (const member of node.members) { const memberName = member.name; const memberType = getEnumMemberType(typeChecker, member); // Enum members cannot be named with a private identifier, although it // is technically valid in the AST. if (memberType !== 'number' || ts.isPrivateIdentifier(memberName)) { continue; } // TypeScript enum members can have Identifier names or String names. // We need to emit slightly different code to support these two syntaxes: let nameExpr; let memberAccess; if (ts.isIdentifier(memberName)) { // Foo[Foo.ABC] = "ABC"; nameExpr = (0, transformer_util_1.createSingleQuoteStringLiteral)(memberName.text); // Make sure to create a clean, new identifier, so comments do not get emitted twice. const ident = ts.factory.createIdentifier((0, transformer_util_1.getIdentifierText)(memberName)); memberAccess = ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(name), ident); } else { // Foo[Foo["A B C"]] = "A B C"; or Foo[Foo[expression]] = expression; nameExpr = ts.isComputedPropertyName(memberName) ? memberName.expression : memberName; memberAccess = ts.factory.createElementAccessExpression(ts.factory.createIdentifier(name), nameExpr); } resultNodes.push(ts.factory.createExpressionStatement(ts.factory.createAssignment(ts.factory.createElementAccessExpression(ts.factory.createIdentifier(name), memberAccess), nameExpr))); } return resultNodes; } return (sf) => visitor(sf); }; } exports.enumTransformer = enumTransformer; //# sourceMappingURL=enum_transformer.js.map