2017-11-20 10:21:17 -08:00
|
|
|
/**
|
|
|
|
* @license
|
2020-05-19 12:08:49 -07:00
|
|
|
* Copyright Google LLC All Rights Reserved.
|
2017-11-20 10:21:17 -08:00
|
|
|
*
|
|
|
|
* 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';
|
2020-04-08 10:14:18 -07:00
|
|
|
import {error, OutputContext} from './util';
|
2017-11-20 10:21:17 -08:00
|
|
|
|
2018-01-11 15:37:56 -08:00
|
|
|
const CONSTANT_PREFIX = '_c';
|
|
|
|
|
2020-02-16 12:49:23 +01:00
|
|
|
/**
|
|
|
|
* `ConstantPool` tries to reuse literal factories when two or more literals are identical.
|
|
|
|
* We determine whether literals are identical by creating a key out of their AST using the
|
|
|
|
* `KeyVisitor`. This constant is used to replace dynamic expressions which can't be safely
|
|
|
|
* converted into a key. E.g. given an expression `{foo: bar()}`, since we don't know what
|
|
|
|
* the result of `bar` will be, we create a key that looks like `{foo: <unknown>}`. Note
|
|
|
|
* that we use a variable, rather than something like `null` in order to avoid collisions.
|
|
|
|
*/
|
|
|
|
const UNKNOWN_VALUE_KEY = o.variable('<unknown>');
|
|
|
|
|
2020-04-08 10:14:18 -07:00
|
|
|
export const enum DefinitionKind {
|
|
|
|
Injector,
|
|
|
|
Directive,
|
|
|
|
Component,
|
|
|
|
Pipe
|
|
|
|
}
|
2017-11-20 10:21:17 -08:00
|
|
|
|
2018-02-14 17:12:05 -08:00
|
|
|
/**
|
|
|
|
* Context to use when producing a key.
|
|
|
|
*
|
|
|
|
* This ensures we see the constant not the reference variable when producing
|
|
|
|
* a key.
|
|
|
|
*/
|
|
|
|
const KEY_CONTEXT = {};
|
|
|
|
|
2020-07-23 18:47:33 -07:00
|
|
|
/**
|
|
|
|
* Generally all primitive values are excluded from the `ConstantPool`, but there is an exclusion
|
|
|
|
* for strings that reach a certain length threshold. This constant defines the length threshold for
|
|
|
|
* strings.
|
|
|
|
*/
|
|
|
|
const POOL_INCLUSION_LENGTH_THRESHOLD_FOR_STRINGS = 50;
|
|
|
|
|
2017-11-20 10:21:17 -08:00
|
|
|
/**
|
|
|
|
* 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 {
|
2018-02-14 17:12:05 -08:00
|
|
|
private original: o.Expression;
|
2017-11-20 10:21:17 -08:00
|
|
|
|
2018-06-18 16:38:33 -07:00
|
|
|
// TODO(issue/24571): remove '!'.
|
2020-04-08 10:14:18 -07:00
|
|
|
shared!: boolean;
|
2017-11-20 10:21:17 -08:00
|
|
|
|
2018-02-14 17:12:05 -08:00
|
|
|
constructor(public resolved: o.Expression) {
|
|
|
|
super(resolved.type);
|
|
|
|
this.original = resolved;
|
|
|
|
}
|
|
|
|
|
2017-11-20 10:21:17 -08:00
|
|
|
visitExpression(visitor: o.ExpressionVisitor, context: any): any {
|
2018-02-14 17:12:05 -08:00
|
|
|
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);
|
|
|
|
}
|
2017-11-20 10:21:17 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
isEquivalent(e: o.Expression): boolean {
|
|
|
|
return e instanceof FixupExpression && this.resolved.isEquivalent(e.resolved);
|
|
|
|
}
|
|
|
|
|
2020-04-08 10:14:18 -07:00
|
|
|
isConstant() {
|
|
|
|
return true;
|
|
|
|
}
|
2018-02-14 17:12:05 -08:00
|
|
|
|
2017-11-20 10:21:17 -08:00
|
|
|
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 literals = new Map<string, FixupExpression>();
|
2018-02-14 17:12:05 -08:00
|
|
|
private literalFactories = new Map<string, o.Expression>();
|
2017-11-20 10:21:17 -08:00
|
|
|
private injectorDefinitions = new Map<any, FixupExpression>();
|
|
|
|
private directiveDefinitions = new Map<any, FixupExpression>();
|
2018-01-11 15:37:56 -08:00
|
|
|
private componentDefinitions = new Map<any, FixupExpression>();
|
2018-02-05 17:31:12 -08:00
|
|
|
private pipeDefinitions = new Map<any, FixupExpression>();
|
2017-11-20 10:21:17 -08:00
|
|
|
|
|
|
|
private nextNameIndex = 0;
|
|
|
|
|
2020-07-27 13:24:55 -07:00
|
|
|
constructor(private readonly isClosureCompilerEnabled: boolean = false) {}
|
|
|
|
|
2018-01-11 15:37:56 -08:00
|
|
|
getConstLiteral(literal: o.Expression, forceShared?: boolean): o.Expression {
|
2020-07-27 13:24:55 -07:00
|
|
|
if ((literal instanceof o.LiteralExpr && !isLongStringLiteral(literal)) ||
|
2020-07-23 18:47:33 -07:00
|
|
|
literal instanceof FixupExpression) {
|
2018-02-14 17:12:05 -08:00
|
|
|
// Do no put simple literals into the constant pool or try to produce a constant for a
|
|
|
|
// reference to a constant.
|
|
|
|
return literal;
|
|
|
|
}
|
2017-11-20 10:21:17 -08:00
|
|
|
const key = this.keyOf(literal);
|
|
|
|
let fixup = this.literals.get(key);
|
2018-01-11 15:37:56 -08:00
|
|
|
let newValue = false;
|
2017-11-20 10:21:17 -08:00
|
|
|
if (!fixup) {
|
|
|
|
fixup = new FixupExpression(literal);
|
|
|
|
this.literals.set(key, fixup);
|
2018-01-11 15:37:56 -08:00
|
|
|
newValue = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((!newValue && !fixup.shared) || (newValue && forceShared)) {
|
2017-11-20 10:21:17 -08:00
|
|
|
// Replace the expression with a variable
|
|
|
|
const name = this.freshName();
|
2020-07-27 13:24:55 -07:00
|
|
|
let definition: o.WriteVarExpr;
|
|
|
|
let usage: o.Expression;
|
|
|
|
if (this.isClosureCompilerEnabled && isLongStringLiteral(literal)) {
|
|
|
|
// For string literals, Closure will **always** inline the string at
|
|
|
|
// **all** usages, duplicating it each time. For large strings, this
|
|
|
|
// unnecessarily bloats bundle size. To work around this restriction, we
|
|
|
|
// wrap the string in a function, and call that function for each usage.
|
|
|
|
// This tricks Closure into using inline logic for functions instead of
|
|
|
|
// string literals. Function calls are only inlined if the body is small
|
|
|
|
// enough to be worth it. By doing this, very large strings will be
|
|
|
|
// shared across multiple usages, rather than duplicating the string at
|
|
|
|
// each usage site.
|
|
|
|
//
|
|
|
|
// const myStr = function() { return "very very very long string"; };
|
|
|
|
// const usage1 = myStr();
|
|
|
|
// const usage2 = myStr();
|
|
|
|
definition = o.variable(name).set(new o.FunctionExpr(
|
|
|
|
[], // Params.
|
|
|
|
[
|
|
|
|
// Statements.
|
|
|
|
new o.ReturnStatement(literal),
|
|
|
|
],
|
|
|
|
));
|
|
|
|
usage = o.variable(name).callFn([]);
|
|
|
|
} else {
|
|
|
|
// Just declare and use the variable directly, without a function call
|
|
|
|
// indirection. This saves a few bytes and avoids an unncessary call.
|
|
|
|
definition = o.variable(name).set(literal);
|
|
|
|
usage = o.variable(name);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.statements.push(definition.toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]));
|
|
|
|
fixup.fixup(usage);
|
2017-11-20 10:21:17 -08:00
|
|
|
}
|
2018-01-11 15:37:56 -08:00
|
|
|
|
2017-11-20 10:21:17 -08:00
|
|
|
return fixup;
|
|
|
|
}
|
|
|
|
|
2018-02-05 17:31:12 -08:00
|
|
|
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;
|
2017-11-20 10:21:17 -08:00
|
|
|
if (!fixup) {
|
2018-02-05 17:31:12 -08:00
|
|
|
const property = this.propertyNameOf(kind);
|
2017-11-20 10:21:17 -08:00
|
|
|
fixup = new FixupExpression(ctx.importExpr(type).prop(property));
|
2018-02-05 17:31:12 -08:00
|
|
|
definitions.set(type, fixup);
|
|
|
|
newValue = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((!newValue && !fixup.shared) || (newValue && forceShared)) {
|
2017-11-20 10:21:17 -08:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-02-14 17:12:05 -08:00
|
|
|
getLiteralFactory(literal: o.LiteralArrayExpr|o.LiteralMapExpr):
|
|
|
|
{literalFactory: o.Expression, literalFactoryArguments: o.Expression[]} {
|
2020-02-16 12:49:23 +01:00
|
|
|
// Create a pure function that builds an array of a mix of constant and variable expressions
|
2018-02-14 17:12:05 -08:00
|
|
|
if (literal instanceof o.LiteralArrayExpr) {
|
2020-02-16 12:49:23 +01:00
|
|
|
const argumentsForKey = literal.entries.map(e => e.isConstant() ? e : UNKNOWN_VALUE_KEY);
|
2018-02-14 17:12:05 -08:00
|
|
|
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,
|
2020-02-16 12:49:23 +01:00
|
|
|
value: e.value.isConstant() ? e.value : UNKNOWN_VALUE_KEY,
|
2018-02-14 17:12:05 -08:00
|
|
|
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 =
|
2020-04-08 10:14:18 -07:00
|
|
|
resultExpressions.filter(isVariable).map(e => new o.FnParam(e.name!, o.DYNAMIC_TYPE));
|
2018-02-14 17:12:05 -08:00
|
|
|
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};
|
|
|
|
}
|
|
|
|
|
2017-11-20 10:21:17 -08:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2020-04-08 10:14:18 -07:00
|
|
|
uniqueName(prefix: string): string {
|
|
|
|
return `${prefix}${this.nextNameIndex++}`;
|
|
|
|
}
|
2017-11-20 10:21:17 -08:00
|
|
|
|
2018-02-05 17:31:12 -08:00
|
|
|
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:
|
2019-10-10 14:57:15 -07:00
|
|
|
return 'ɵcmp';
|
2018-02-05 17:31:12 -08:00
|
|
|
case DefinitionKind.Directive:
|
2019-10-11 12:28:12 -07:00
|
|
|
return 'ɵdir';
|
2018-02-05 17:31:12 -08:00
|
|
|
case DefinitionKind.Injector:
|
2019-10-14 15:28:01 -07:00
|
|
|
return 'ɵinj';
|
2018-02-05 17:31:12 -08:00
|
|
|
case DefinitionKind.Pipe:
|
2019-10-11 19:19:59 -07:00
|
|
|
return 'ɵpipe';
|
2018-02-05 17:31:12 -08:00
|
|
|
}
|
|
|
|
error(`Unknown definition kind ${kind}`);
|
|
|
|
return '<unknown>';
|
|
|
|
}
|
|
|
|
|
2020-04-08 10:14:18 -07:00
|
|
|
private freshName(): string {
|
|
|
|
return this.uniqueName(CONSTANT_PREFIX);
|
|
|
|
}
|
2017-11-20 10:21:17 -08:00
|
|
|
|
|
|
|
private keyOf(expression: o.Expression) {
|
2018-02-14 17:12:05 -08:00
|
|
|
return expression.visitExpression(new KeyVisitor(), KEY_CONTEXT);
|
2017-11-20 10:21:17 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-22 15:03:06 -07:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2017-11-20 10:21:17 -08:00
|
|
|
class KeyVisitor implements o.ExpressionVisitor {
|
2018-01-11 15:37:56 -08:00
|
|
|
visitLiteralExpr(ast: o.LiteralExpr): string {
|
|
|
|
return `${typeof ast.value === 'string' ? '"' + ast.value + '"' : ast.value}`;
|
|
|
|
}
|
2018-03-22 15:03:06 -07:00
|
|
|
|
2018-02-14 17:12:05 -08:00
|
|
|
visitLiteralArrayExpr(ast: o.LiteralArrayExpr, context: object): string {
|
|
|
|
return `[${ast.entries.map(entry => entry.visitExpression(this, context)).join(',')}]`;
|
2017-11-20 10:21:17 -08:00
|
|
|
}
|
|
|
|
|
2018-02-14 17:12:05 -08:00
|
|
|
visitLiteralMapExpr(ast: o.LiteralMapExpr, context: object): string {
|
2018-03-22 15:03:06 -07:00
|
|
|
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)}`;
|
2018-01-11 15:37:56 -08:00
|
|
|
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}`;
|
2017-11-20 10:21:17 -08:00
|
|
|
}
|
|
|
|
|
2020-04-08 10:14:18 -07:00
|
|
|
visitReadVarExpr(node: o.ReadVarExpr) {
|
|
|
|
return `VAR:${node.name}`;
|
|
|
|
}
|
2018-05-10 15:58:27 -07:00
|
|
|
|
fix(ivy): use 'typeof' and 'never' for type metadata (#24862)
Previously ngtsc would use a tuple of class types for listing metadata
in .d.ts files. For example, an @NgModule's declarations might be
represented with the type:
[NgIf, NgForOf, NgClass]
If the module had no declarations, an empty tuple [] would be produced.
This has two problems.
1. If the class type has generic type parameters, TypeScript will
complain that they're not provided.
2. The empty tuple type is not actually legal.
This commit addresses both problems.
1. Class types are now represented using the `typeof` operator, so the
above declarations would be represented as:
[typeof NgIf, typeof NgForOf, typeof NgClass].
Since typeof operates on a value, it doesn't require generic type
arguments.
2. Instead of an empty tuple, `never` is used to indicate no metadata.
PR Close #24862
2018-07-17 13:34:20 -07:00
|
|
|
visitTypeofExpr(node: o.TypeofExpr, context: any): string {
|
|
|
|
return `TYPEOF:${node.expr.visitExpression(this, context)}`;
|
|
|
|
}
|
|
|
|
|
2018-04-06 09:53:10 -07:00
|
|
|
visitWrappedNodeExpr = invalid;
|
2017-11-20 10:21:17 -08:00
|
|
|
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;
|
2019-07-30 18:02:17 +01:00
|
|
|
visitLocalizedString = invalid;
|
2017-11-20 10:21:17 -08:00
|
|
|
}
|
|
|
|
|
2020-04-08 10:14:18 -07:00
|
|
|
function invalid<T>(this: o.ExpressionVisitor, arg: o.Expression|o.Statement): never {
|
2017-11-20 10:21:17 -08:00
|
|
|
throw new Error(
|
2018-02-14 10:54:00 -08:00
|
|
|
`Invalid state: Visitor ${this.constructor.name} doesn't handle ${arg.constructor.name}`);
|
2017-11-20 10:21:17 -08:00
|
|
|
}
|
2018-02-14 17:12:05 -08:00
|
|
|
|
|
|
|
function isVariable(e: o.Expression): e is o.ReadVarExpr {
|
|
|
|
return e instanceof o.ReadVarExpr;
|
2019-06-14 09:28:04 +02:00
|
|
|
}
|
2020-07-23 18:47:33 -07:00
|
|
|
|
2020-07-27 13:24:55 -07:00
|
|
|
function isLongStringLiteral(expr: o.Expression): boolean {
|
|
|
|
return expr instanceof o.LiteralExpr && typeof expr.value === 'string' &&
|
2020-07-23 18:51:55 -07:00
|
|
|
expr.value.length >= POOL_INCLUSION_LENGTH_THRESHOLD_FOR_STRINGS;
|
2020-07-27 13:24:55 -07:00
|
|
|
}
|