in src/enum_transformer.ts [96:225]
function visitor<T extends ts.Node>(node: T): T|ts.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 (isInNamespace(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 (isAmbient(node)) return ts.visitEachChild(node, visitor, context);
const isExported = hasModifierFlag(node, ts.ModifierFlags.Export);
const enumType = getEnumType(typeChecker, node);
const values: ts.PropertyAssignment[] = [];
let enumIndex = 0;
for (const member of node.members) {
let enumValue: ts.Expression;
if (member.initializer) {
const enumConstValue = typeChecker.getConstantValue(member);
if (typeof enumConstValue === 'number') {
enumIndex = enumConstValue + 1;
enumValue = ts.createLiteral(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.createLiteral(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) as ts.Expression;
}
} else {
enumValue = ts.createLiteral(enumIndex);
enumIndex++;
}
values.push(ts.setOriginalNode(
ts.setTextRange(ts.createPropertyAssignment(member.name, enumValue), member), member));
}
const varDecl = ts.createVariableDeclaration(
node.name, /* type */ undefined,
ts.createObjectLiteral(
ts.setTextRange(ts.createNodeArray(values, true), node.members), true));
const varDeclStmt = ts.setOriginalNode(
ts.setTextRange(
ts.createVariableStatement(
/* modifiers */ undefined,
ts.createVariableDeclarationList(
[varDecl],
/* create a const var */ ts.NodeFlags.Const)),
node),
node);
const comment: ts.SynthesizedComment = {
kind: ts.SyntaxKind.MultiLineCommentTrivia,
text: `* @enum {${enumType}} `,
hasTrailingNewLine: true,
pos: -1,
end: -1
};
ts.setSyntheticLeadingComments(varDeclStmt, [comment]);
const name = node.name.getText();
const resultNodes: ts.Node[] = [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.createExportDeclaration(
undefined, undefined,
ts.createNamedExports([ts.createExportSpecifier(
/* isTypeOnly */ false, undefined, name)])));
}
if (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: ts.Expression;
let memberAccess: ts.Expression;
if (ts.isIdentifier(memberName)) {
// Foo[Foo.ABC] = "ABC";
nameExpr = createSingleQuoteStringLiteral(memberName.text);
// Make sure to create a clean, new identifier, so comments do not get emitted twice.
const ident = ts.createIdentifier(getIdentifierText(memberName));
memberAccess = ts.createPropertyAccess(ts.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.createElementAccess(ts.createIdentifier(name), nameExpr);
}
resultNodes.push(ts.createStatement(ts.createAssignment(
ts.createElementAccess(ts.createIdentifier(name), memberAccess), nameExpr)));
}
return resultNodes;
}