/** * @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 */ import * as o from './output/output_ast'; import {OutputContext, error} from './util'; const CONSTANT_PREFIX = '_c'; // Closure variables holding messages must be named `MSG_[A-Z0-9]+` const TRANSLATION_PREFIX = 'MSG_'; export const enum DefinitionKind {Injector, Directive, Component, Pipe} /** * Closure uses `goog.getMsg(message)` to lookup translations */ const GOOG_GET_MSG = 'goog.getMsg'; /** * Context to use when producing a key. * * This ensures we see the constant not the reference variable when producing * a key. */ const KEY_CONTEXT = {}; /** * A node that is a place-holder that allows the node to be replaced when the actual * node is known. * * This allows the constant pool to change an expression from a direct reference to * a constant to a shared constant. It returns a fix-up node that is later allowed to * change the referenced expression. */ class FixupExpression extends o.Expression { private original: o.Expression; shared: boolean; constructor(public resolved: o.Expression) { super(resolved.type); this.original = resolved; } visitExpression(visitor: o.ExpressionVisitor, context: any): any { if (context === KEY_CONTEXT) { // When producing a key we want to traverse the constant not the // variable used to refer to it. return this.original.visitExpression(visitor, context); } else { return this.resolved.visitExpression(visitor, context); } } isEquivalent(e: o.Expression): boolean { return e instanceof FixupExpression && this.resolved.isEquivalent(e.resolved); } isConstant() { return true; } fixup(expression: o.Expression) { this.resolved = expression; this.shared = true; } } /** * A constant pool allows a code emitter to share constant in an output context. * * The constant pool also supports sharing access to ivy definitions references. */ export class ConstantPool { statements: o.Statement[] = []; private translations = new Map(); private literals = new Map(); private literalFactories = new Map(); private injectorDefinitions = new Map(); private directiveDefinitions = new Map(); private componentDefinitions = new Map(); private pipeDefinitions = new Map(); private nextNameIndex = 0; getConstLiteral(literal: o.Expression, forceShared?: boolean): o.Expression { if (literal instanceof o.LiteralExpr || literal instanceof FixupExpression) { // Do no put simple literals into the constant pool or try to produce a constant for a // reference to a constant. return literal; } const key = this.keyOf(literal); let fixup = this.literals.get(key); let newValue = false; if (!fixup) { fixup = new FixupExpression(literal); this.literals.set(key, fixup); newValue = true; } if ((!newValue && !fixup.shared) || (newValue && forceShared)) { // Replace the expression with a variable const name = this.freshName(); this.statements.push( o.variable(name).set(literal).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final])); fixup.fixup(o.variable(name)); } return fixup; } // Generates closure specific code for translation. // // ``` // /** // * @desc description? // * @meaning meaning? // */ // const MSG_XYZ = goog.getMsg('message'); // ``` getTranslation(message: string, meta: {description?: string, meaning?: string}): o.Expression { // The identity of an i18n message depends on the message and its meaning const key = meta.meaning ? `${message}\u0000\u0000${meta.meaning}` : message; const exp = this.translations.get(key); if (exp) { return exp; } const docStmt = i18nMetaToDocStmt(meta); if (docStmt) { this.statements.push(docStmt); } // Call closure to get the translation const variable = o.variable(this.freshTranslationName()); const fnCall = o.variable(GOOG_GET_MSG).callFn([o.literal(message)]); const msgStmt = variable.set(fnCall).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]); this.statements.push(msgStmt); this.translations.set(key, variable); return variable; } getDefinition(type: any, kind: DefinitionKind, ctx: OutputContext, forceShared: boolean = false): o.Expression { const definitions = this.definitionsOf(kind); let fixup = definitions.get(type); let newValue = false; if (!fixup) { const property = this.propertyNameOf(kind); fixup = new FixupExpression(ctx.importExpr(type).prop(property)); definitions.set(type, fixup); newValue = true; } if ((!newValue && !fixup.shared) || (newValue && forceShared)) { const name = this.freshName(); this.statements.push( o.variable(name).set(fixup.resolved).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final])); fixup.fixup(o.variable(name)); } return fixup; } getLiteralFactory(literal: o.LiteralArrayExpr|o.LiteralMapExpr): {literalFactory: o.Expression, literalFactoryArguments: o.Expression[]} { // Create a pure function that builds an array of a mix of constant and variable expressions if (literal instanceof o.LiteralArrayExpr) { const argumentsForKey = literal.entries.map(e => e.isConstant() ? e : o.literal(null)); const key = this.keyOf(o.literalArr(argumentsForKey)); return this._getLiteralFactory(key, literal.entries, entries => o.literalArr(entries)); } else { const expressionForKey = o.literalMap( literal.entries.map(e => ({ key: e.key, value: e.value.isConstant() ? e.value : o.literal(null), quoted: e.quoted }))); const key = this.keyOf(expressionForKey); return this._getLiteralFactory( key, literal.entries.map(e => e.value), entries => o.literalMap(entries.map((value, index) => ({ key: literal.entries[index].key, value, quoted: literal.entries[index].quoted })))); } } private _getLiteralFactory( key: string, values: o.Expression[], resultMap: (parameters: o.Expression[]) => o.Expression): {literalFactory: o.Expression, literalFactoryArguments: o.Expression[]} { let literalFactory = this.literalFactories.get(key); const literalFactoryArguments = values.filter((e => !e.isConstant())); if (!literalFactory) { const resultExpressions = values.map( (e, index) => e.isConstant() ? this.getConstLiteral(e, true) : o.variable(`a${index}`)); const parameters = resultExpressions.filter(isVariable).map(e => new o.FnParam(e.name !, o.DYNAMIC_TYPE)); const pureFunctionDeclaration = o.fn(parameters, [new o.ReturnStatement(resultMap(resultExpressions))], o.INFERRED_TYPE); const name = this.freshName(); this.statements.push( o.variable(name).set(pureFunctionDeclaration).toDeclStmt(o.INFERRED_TYPE, [ o.StmtModifier.Final ])); literalFactory = o.variable(name); this.literalFactories.set(key, literalFactory); } return {literalFactory, literalFactoryArguments}; } /** * Produce a unique name. * * The name might be unique among different prefixes if any of the prefixes end in * a digit so the prefix should be a constant string (not based on user input) and * must not end in a digit. */ uniqueName(prefix: string): string { return `${prefix}${this.nextNameIndex++}`; } private definitionsOf(kind: DefinitionKind): Map { switch (kind) { case DefinitionKind.Component: return this.componentDefinitions; case DefinitionKind.Directive: return this.directiveDefinitions; case DefinitionKind.Injector: return this.injectorDefinitions; case DefinitionKind.Pipe: return this.pipeDefinitions; } error(`Unknown definition kind ${kind}`); return this.componentDefinitions; } public propertyNameOf(kind: DefinitionKind): string { switch (kind) { case DefinitionKind.Component: return 'ngComponentDef'; case DefinitionKind.Directive: return 'ngDirectiveDef'; case DefinitionKind.Injector: return 'ngInjectorDef'; case DefinitionKind.Pipe: return 'ngPipeDef'; } error(`Unknown definition kind ${kind}`); return ''; } private freshName(): string { return this.uniqueName(CONSTANT_PREFIX); } private freshTranslationName(): string { return this.uniqueName(TRANSLATION_PREFIX).toUpperCase(); } private keyOf(expression: o.Expression) { return expression.visitExpression(new KeyVisitor(), KEY_CONTEXT); } } /** * Visitor used to determine if 2 expressions are equivalent and can be shared in the * `ConstantPool`. * * When the id (string) generated by the visitor is equal, expressions are considered equivalent. */ class KeyVisitor implements o.ExpressionVisitor { visitLiteralExpr(ast: o.LiteralExpr): string { return `${typeof ast.value === 'string' ? '"' + ast.value + '"' : ast.value}`; } visitLiteralArrayExpr(ast: o.LiteralArrayExpr, context: object): string { return `[${ast.entries.map(entry => entry.visitExpression(this, context)).join(',')}]`; } visitLiteralMapExpr(ast: o.LiteralMapExpr, context: object): string { const mapKey = (entry: o.LiteralMapEntry) => { const quote = entry.quoted ? '"' : ''; return `${quote}${entry.key}${quote}`; }; const mapEntry = (entry: o.LiteralMapEntry) => `${mapKey(entry)}:${entry.value.visitExpression(this, context)}`; return `{${ast.entries.map(mapEntry).join(',')}`; } visitExternalExpr(ast: o.ExternalExpr): string { return ast.value.moduleName ? `EX:${ast.value.moduleName}:${ast.value.name}` : `EX:${ast.value.runtime.name}`; } visitReadVarExpr(node: o.ReadVarExpr) { return `VAR:${node.name}`; } visitWrappedNodeExpr = invalid; visitWriteVarExpr = invalid; visitWriteKeyExpr = invalid; visitWritePropExpr = invalid; visitInvokeMethodExpr = invalid; visitInvokeFunctionExpr = invalid; visitInstantiateExpr = invalid; visitConditionalExpr = invalid; visitNotExpr = invalid; visitAssertNotNullExpr = invalid; visitCastExpr = invalid; visitFunctionExpr = invalid; visitBinaryOperatorExpr = invalid; visitReadPropExpr = invalid; visitReadKeyExpr = invalid; visitCommaExpr = invalid; } function invalid(arg: o.Expression | o.Statement): never { throw new Error( `Invalid state: Visitor ${this.constructor.name} doesn't handle ${arg.constructor.name}`); } function isVariable(e: o.Expression): e is o.ReadVarExpr { return e instanceof o.ReadVarExpr; } // Converts i18n meta informations for a message (description, meaning) to a JsDoc statement // formatted as expected by the Closure compiler. function i18nMetaToDocStmt(meta: {description?: string, id?: string, meaning?: string}): o.JSDocCommentStmt|null { const tags: o.JSDocTag[] = []; if (meta.description) { tags.push({tagName: o.JSDocTagName.Desc, text: meta.description}); } if (meta.meaning) { tags.push({tagName: o.JSDocTagName.Meaning, text: meta.meaning}); } return tags.length == 0 ? null : new o.JSDocCommentStmt(tags); }