349 lines
12 KiB
TypeScript
349 lines
12 KiB
TypeScript
/**
|
|
* @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;
|
|
|
|
// TODO(issue/24571): remove '!'.
|
|
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<string, o.Expression>();
|
|
private literals = new Map<string, FixupExpression>();
|
|
private literalFactories = new Map<string, o.Expression>();
|
|
private injectorDefinitions = new Map<any, FixupExpression>();
|
|
private directiveDefinitions = new Map<any, FixupExpression>();
|
|
private componentDefinitions = new Map<any, FixupExpression>();
|
|
private pipeDefinitions = new Map<any, FixupExpression>();
|
|
|
|
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}, suffix: 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(suffix));
|
|
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<any, FixupExpression> {
|
|
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 '<unknown>';
|
|
}
|
|
|
|
private freshName(): string { return this.uniqueName(CONSTANT_PREFIX); }
|
|
|
|
private freshTranslationName(suffix: string): string {
|
|
return this.uniqueName(TRANSLATION_PREFIX + suffix).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}`; }
|
|
|
|
visitTypeofExpr(node: o.TypeofExpr, context: any): string {
|
|
return `TYPEOF:${node.expr.visitExpression(this, context)}`;
|
|
}
|
|
|
|
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<T>(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);
|
|
}
|