diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/index.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/index.ts index 2d32afc308..f8465eba4e 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/index.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/index.ts @@ -6,5 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ +export {DynamicValue} from './src/dynamic'; export {ForeignFunctionResolver, PartialEvaluator} from './src/interface'; -export {BuiltinFn, DynamicValue, EnumValue, ResolvedValue, ResolvedValueArray, ResolvedValueMap, isDynamicValue} from './src/result'; +export {BuiltinFn, EnumValue, ResolvedValue, ResolvedValueArray, ResolvedValueMap} from './src/result'; diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/builtin.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/builtin.ts index 758853946e..51373de9ba 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/builtin.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/builtin.ts @@ -6,16 +6,19 @@ * found in the LICENSE file at https://angular.io/license */ -import {BuiltinFn, DYNAMIC_VALUE, ResolvedValue, ResolvedValueArray} from './result'; +import * as ts from 'typescript'; + +import {DynamicValue} from './dynamic'; +import {BuiltinFn, ResolvedValue, ResolvedValueArray} from './result'; export class ArraySliceBuiltinFn extends BuiltinFn { - constructor(private lhs: ResolvedValueArray) { super(); } + constructor(private node: ts.Node, private lhs: ResolvedValueArray) { super(); } evaluate(args: ResolvedValueArray): ResolvedValue { if (args.length === 0) { return this.lhs; } else { - return DYNAMIC_VALUE; + return DynamicValue.fromUnknown(this.node); } } } diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/dynamic.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/dynamic.ts new file mode 100644 index 0000000000..ddcaf246b2 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/dynamic.ts @@ -0,0 +1,110 @@ +/** + * @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 ts from 'typescript'; + +import {Reference} from '../../imports'; + +/** + * The reason why a value cannot be determined statically. + */ +export const enum DynamicValueReason { + /** + * A value could not be determined statically, because it contains a term that could not be + * determined statically. + * (E.g. a property assignment or call expression where the lhs is a `DynamicValue`, a template + * literal with a dynamic expression, an object literal with a spread assignment which could not + * be determined statically, etc.) + */ + DYNAMIC_INPUT, + + /** + * A string could not be statically evaluated. + * (E.g. a dynamically constructed object property name or a template literal expression that + * could not be statically resolved to a primitive value.) + */ + DYNAMIC_STRING, + + /** + * An external reference could not be resolved to a value which can be evaluated. + * (E.g. a call expression for a function declared in `.d.ts`.) + */ + EXTERNAL_REFERENCE, + + /** + * A type of `ts.Expression` that `StaticInterpreter` doesn't know how to evaluate. + */ + UNKNOWN_EXPRESSION_TYPE, + + /** + * A declaration of a `ts.Identifier` could not be found. + */ + UNKNOWN_IDENTIFIER, + + /** + * A value could not be determined statically for any reason other the above. + */ + UNKNOWN, +} + +/** + * Represents a value which cannot be determined statically. + */ +export class DynamicValue { + private constructor( + readonly node: ts.Node, readonly reason: R, private code: DynamicValueReason) {} + + static fromDynamicInput(node: ts.Node, input: DynamicValue): DynamicValue { + return new DynamicValue(node, input, DynamicValueReason.DYNAMIC_INPUT); + } + + static fromDynamicString(node: ts.Node): DynamicValue { + return new DynamicValue(node, {}, DynamicValueReason.DYNAMIC_STRING); + } + + static fromExternalReference(node: ts.Node, ref: Reference): + DynamicValue> { + return new DynamicValue(node, ref, DynamicValueReason.EXTERNAL_REFERENCE); + } + + static fromUnknownExpressionType(node: ts.Node): DynamicValue { + return new DynamicValue(node, {}, DynamicValueReason.UNKNOWN_EXPRESSION_TYPE); + } + + static fromUnknownIdentifier(node: ts.Identifier): DynamicValue { + return new DynamicValue(node, {}, DynamicValueReason.UNKNOWN_IDENTIFIER); + } + + static fromUnknown(node: ts.Node): DynamicValue { + return new DynamicValue(node, {}, DynamicValueReason.UNKNOWN); + } + + isFromDynamicInput(this: DynamicValue): this is DynamicValue { + return this.code === DynamicValueReason.DYNAMIC_INPUT; + } + + isFromDynamicString(this: DynamicValue): this is DynamicValue { + return this.code === DynamicValueReason.DYNAMIC_STRING; + } + + isFromExternalReference(this: DynamicValue): this is DynamicValue> { + return this.code === DynamicValueReason.EXTERNAL_REFERENCE; + } + + isFromUnknownExpressionType(this: DynamicValue): this is DynamicValue { + return this.code === DynamicValueReason.UNKNOWN_EXPRESSION_TYPE; + } + + isFromUnknownIdentifier(this: DynamicValue): this is DynamicValue { + return this.code === DynamicValueReason.UNKNOWN_IDENTIFIER; + } + + isFromUnknown(this: DynamicValue): this is DynamicValue { + return this.code === DynamicValueReason.UNKNOWN; + } +} diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts index d44736bc80..f38b1df8e2 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts @@ -12,7 +12,8 @@ import {AbsoluteReference, NodeReference, Reference, ReferenceResolver, Resolved import {Declaration, ReflectionHost} from '../../reflection'; import {ArraySliceBuiltinFn} from './builtin'; -import {BuiltinFn, DYNAMIC_VALUE, EnumValue, ResolvedValue, ResolvedValueArray, ResolvedValueMap, isDynamicValue} from './result'; +import {DynamicValue} from './dynamic'; +import {BuiltinFn, EnumValue, ResolvedValue, ResolvedValueArray, ResolvedValueMap} from './result'; /** @@ -126,7 +127,7 @@ export class StaticInterpreter { } else if (this.host.isClass(node)) { return this.visitDeclaration(node, context); } else { - return DYNAMIC_VALUE; + return DynamicValue.fromUnknownExpressionType(node); } } @@ -137,21 +138,15 @@ export class StaticInterpreter { const element = node.elements[i]; if (ts.isSpreadElement(element)) { const spread = this.visitExpression(element.expression, context); - if (isDynamicValue(spread)) { - return DYNAMIC_VALUE; - } - if (!Array.isArray(spread)) { + if (spread instanceof DynamicValue) { + array.push(DynamicValue.fromDynamicInput(element.expression, spread)); + } else if (!Array.isArray(spread)) { throw new Error(`Unexpected value in spread expression: ${spread}`); + } else { + array.push(...spread); } - - array.push(...spread); } else { - const result = this.visitExpression(element, context); - if (isDynamicValue(result)) { - return DYNAMIC_VALUE; - } - - array.push(result); + array.push(this.visitExpression(element, context)); } } return array; @@ -164,30 +159,28 @@ export class StaticInterpreter { const property = node.properties[i]; if (ts.isPropertyAssignment(property)) { const name = this.stringNameFromPropertyName(property.name, context); - // Check whether the name can be determined statically. if (name === undefined) { - return DYNAMIC_VALUE; + return DynamicValue.fromDynamicInput(node, DynamicValue.fromDynamicString(property.name)); } - map.set(name, this.visitExpression(property.initializer, context)); } else if (ts.isShorthandPropertyAssignment(property)) { const symbol = this.checker.getShorthandAssignmentValueSymbol(property); if (symbol === undefined || symbol.valueDeclaration === undefined) { - return DYNAMIC_VALUE; + map.set(property.name.text, DynamicValue.fromUnknown(property)); + } else { + map.set(property.name.text, this.visitDeclaration(symbol.valueDeclaration, context)); } - map.set(property.name.text, this.visitDeclaration(symbol.valueDeclaration, context)); } else if (ts.isSpreadAssignment(property)) { const spread = this.visitExpression(property.expression, context); - if (isDynamicValue(spread)) { - return DYNAMIC_VALUE; - } - if (!(spread instanceof Map)) { + if (spread instanceof DynamicValue) { + return DynamicValue.fromDynamicInput(node, spread); + } else if (!(spread instanceof Map)) { throw new Error(`Unexpected value in spread assignment: ${spread}`); } spread.forEach((value, key) => map.set(key, value)); } else { - return DYNAMIC_VALUE; + return DynamicValue.fromUnknown(node); } } return map; @@ -201,8 +194,10 @@ export class StaticInterpreter { if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || value == null) { pieces.push(`${value}`); + } else if (value instanceof DynamicValue) { + return DynamicValue.fromDynamicInput(node, value); } else { - return DYNAMIC_VALUE; + return DynamicValue.fromDynamicInput(node, DynamicValue.fromDynamicString(span.expression)); } pieces.push(span.literal.text); } @@ -212,7 +207,7 @@ export class StaticInterpreter { private visitIdentifier(node: ts.Identifier, context: Context): ResolvedValue { const decl = this.host.getDeclarationOfIdentifier(node); if (decl === null) { - return DYNAMIC_VALUE; + return DynamicValue.fromUnknownIdentifier(node); } const result = this.visitDeclaration(decl.node, {...context, ...joinModuleContext(context, node, decl)}); @@ -270,19 +265,19 @@ export class StaticInterpreter { if (node.argumentExpression === undefined) { throw new Error(`Expected argument in ElementAccessExpression`); } - if (isDynamicValue(lhs)) { - return DYNAMIC_VALUE; + if (lhs instanceof DynamicValue) { + return DynamicValue.fromDynamicInput(node, lhs); } const rhs = this.visitExpression(node.argumentExpression, context); - if (isDynamicValue(rhs)) { - return DYNAMIC_VALUE; + if (rhs instanceof DynamicValue) { + return DynamicValue.fromDynamicInput(node, rhs); } if (typeof rhs !== 'string' && typeof rhs !== 'number') { throw new Error( `ElementAccessExpression index should be string or number, got ${typeof rhs}: ${rhs}`); } - return this.accessHelper(lhs, rhs, context); + return this.accessHelper(node, lhs, rhs, context); } private visitPropertyAccessExpression(node: ts.PropertyAccessExpression, context: Context): @@ -290,17 +285,16 @@ export class StaticInterpreter { const lhs = this.visitExpression(node.expression, context); const rhs = node.name.text; // TODO: handle reference to class declaration. - if (isDynamicValue(lhs)) { - return DYNAMIC_VALUE; + if (lhs instanceof DynamicValue) { + return DynamicValue.fromDynamicInput(node, lhs); } - - return this.accessHelper(lhs, rhs, context); + return this.accessHelper(node, lhs, rhs, context); } private visitSourceFile(node: ts.SourceFile, context: Context): ResolvedValue { const declarations = this.host.getExportsOfModule(node); if (declarations === null) { - return DYNAMIC_VALUE; + return DynamicValue.fromUnknown(node); } const map = new Map(); declarations.forEach((decl, name) => { @@ -313,7 +307,9 @@ export class StaticInterpreter { return map; } - private accessHelper(lhs: ResolvedValue, rhs: string|number, context: Context): ResolvedValue { + private accessHelper( + node: ts.Expression, lhs: ResolvedValue, rhs: string|number, + context: Context): ResolvedValue { const strIndex = `${rhs}`; if (lhs instanceof Map) { if (lhs.has(strIndex)) { @@ -325,10 +321,10 @@ export class StaticInterpreter { if (rhs === 'length') { return lhs.length; } else if (rhs === 'slice') { - return new ArraySliceBuiltinFn(lhs); + return new ArraySliceBuiltinFn(node, lhs); } if (typeof rhs !== 'number' || !Number.isInteger(rhs)) { - return DYNAMIC_VALUE; + return DynamicValue.fromUnknown(node); } if (rhs < 0 || rhs >= lhs.length) { throw new Error(`Index out of bounds: ${rhs} vs ${lhs.length}`); @@ -355,14 +351,17 @@ export class StaticInterpreter { } return value; } + } else if (lhs instanceof DynamicValue) { + return DynamicValue.fromDynamicInput(node, lhs); + } else { + throw new Error(`Invalid dot property access: ${lhs} dot ${rhs}`); } - throw new Error(`Invalid dot property access: ${lhs} dot ${rhs}`); } private visitCallExpression(node: ts.CallExpression, context: Context): ResolvedValue { const lhs = this.visitExpression(node.expression, context); - if (isDynamicValue(lhs)) { - return DYNAMIC_VALUE; + if (lhs instanceof DynamicValue) { + return DynamicValue.fromDynamicInput(node, lhs); } // If the call refers to a builtin function, attempt to evaluate the function. @@ -387,8 +386,8 @@ export class StaticInterpreter { expr = context.foreignFunctionResolver(lhs, node.arguments); } if (expr === null) { - throw new Error( - `could not resolve foreign function declaration: ${node.getSourceFile().fileName} ${(lhs.node.name as ts.Identifier).text}`); + return DynamicValue.fromDynamicInput( + node, DynamicValue.fromExternalReference(node.expression, lhs)); } // If the function is declared in a different file, resolve the foreign function expression @@ -432,8 +431,8 @@ export class StaticInterpreter { private visitConditionalExpression(node: ts.ConditionalExpression, context: Context): ResolvedValue { const condition = this.visitExpression(node.condition, context); - if (isDynamicValue(condition)) { - return condition; + if (condition instanceof DynamicValue) { + return DynamicValue.fromDynamicInput(node, condition); } if (condition) { @@ -452,7 +451,11 @@ export class StaticInterpreter { const op = UNARY_OPERATORS.get(operatorKind) !; const value = this.visitExpression(node.operand, context); - return isDynamicValue(value) ? DYNAMIC_VALUE : op(value); + if (value instanceof DynamicValue) { + return DynamicValue.fromDynamicInput(node, value); + } else { + return op(value); + } } private visitBinaryExpression(node: ts.BinaryExpression, context: Context): ResolvedValue { @@ -470,8 +473,13 @@ export class StaticInterpreter { lhs = this.visitExpression(node.left, context); rhs = this.visitExpression(node.right, context); } - - return isDynamicValue(lhs) || isDynamicValue(rhs) ? DYNAMIC_VALUE : opRecord.op(lhs, rhs); + if (lhs instanceof DynamicValue) { + return DynamicValue.fromDynamicInput(node, lhs); + } else if (rhs instanceof DynamicValue) { + return DynamicValue.fromDynamicInput(node, rhs); + } else { + return opRecord.op(lhs, rhs); + } } private visitParenthesizedExpression(node: ts.ParenthesizedExpression, context: Context): @@ -500,13 +508,10 @@ function isFunctionOrMethodReference(ref: Reference): } function literal(value: ResolvedValue): any { - if (value === null || value === undefined || typeof value === 'string' || - typeof value === 'number' || typeof value === 'boolean') { + if (value instanceof DynamicValue || value === null || value === undefined || + typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { return value; } - if (isDynamicValue(value)) { - return DYNAMIC_VALUE; - } throw new Error(`Value ${value} is not literal and cannot be used in this context.`); } diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/result.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/result.ts index a2191e19a7..8f9dcdf56d 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/result.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/result.ts @@ -10,6 +10,8 @@ import * as ts from 'typescript'; import {Reference} from '../../imports'; +import {DynamicValue} from './dynamic'; + /** * A value resulting from static resolution. @@ -19,34 +21,7 @@ import {Reference} from '../../imports'; * available statically. */ export type ResolvedValue = number | boolean | string | null | undefined | Reference | EnumValue | - ResolvedValueArray | ResolvedValueMap | BuiltinFn | DynamicValue; - -/** - * Represents a value which cannot be determined statically. - * - * Use `isDynamicValue` to determine whether a `ResolvedValue` is a `DynamicValue`. - */ -export class DynamicValue { - /** - * This is needed so the "is DynamicValue" assertion of `isDynamicValue` actually has meaning. - * - * Otherwise, "is DynamicValue" is akin to "is {}" which doesn't trigger narrowing. - */ - private _isDynamic = true; -} - -/** - * An internal flyweight for `DynamicValue`. Eventually the dynamic value will carry information - * on the location of the node that could not be statically computed. - */ -export const DYNAMIC_VALUE: DynamicValue = new DynamicValue(); - -/** - * Used to test whether a `ResolvedValue` is a `DynamicValue`. - */ -export function isDynamicValue(value: any): value is DynamicValue { - return value === DYNAMIC_VALUE; -} + ResolvedValueArray | ResolvedValueMap | BuiltinFn | DynamicValue<{}>; /** * An array of `ResolvedValue`s.