feat(ivy): DynamicValue now indicates why the value is dynamic (#27697)
This commit changes the partial evaluation mechanism to propagate DynamicValue errors internally during evaluation, and not to "poison" entire data structures when a single value is dynamic. For example, previously if any entry in an array was dynamic, evaluating the entire array would return DynamicValue. Now, the array is returned with only the specific dynamic entry as DynamicValue. Instances of DynamicValue also report the node that was determined to be dynamic, as well as a potential reason for the dynamic-ness. These can be nested, so an expression `a + b` may have a DynamicValue that indicates the 'a' term was DynamicValue, which will itself contain a reason for the dynamic-ness. This work was undertaken for the implementation of listLazyRoutes(), which needs to partially evaluate provider arrays, parts of which are dynamic and parts of which contain useful information. PR Close #27697
This commit is contained in:
parent
070fca1591
commit
9e5016c845
@ -6,5 +6,6 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export {DynamicValue} from './src/dynamic';
|
||||||
export {ForeignFunctionResolver, PartialEvaluator} from './src/interface';
|
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';
|
||||||
|
@ -6,16 +6,19 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 {
|
export class ArraySliceBuiltinFn extends BuiltinFn {
|
||||||
constructor(private lhs: ResolvedValueArray) { super(); }
|
constructor(private node: ts.Node, private lhs: ResolvedValueArray) { super(); }
|
||||||
|
|
||||||
evaluate(args: ResolvedValueArray): ResolvedValue {
|
evaluate(args: ResolvedValueArray): ResolvedValue {
|
||||||
if (args.length === 0) {
|
if (args.length === 0) {
|
||||||
return this.lhs;
|
return this.lhs;
|
||||||
} else {
|
} else {
|
||||||
return DYNAMIC_VALUE;
|
return DynamicValue.fromUnknown(this.node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
110
packages/compiler-cli/src/ngtsc/partial_evaluator/src/dynamic.ts
Normal file
110
packages/compiler-cli/src/ngtsc/partial_evaluator/src/dynamic.ts
Normal file
@ -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<R = {}> {
|
||||||
|
private constructor(
|
||||||
|
readonly node: ts.Node, readonly reason: R, private code: DynamicValueReason) {}
|
||||||
|
|
||||||
|
static fromDynamicInput(node: ts.Node, input: DynamicValue): 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<ts.Declaration>):
|
||||||
|
DynamicValue<Reference<ts.Declaration>> {
|
||||||
|
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<R>): this is DynamicValue<DynamicValue> {
|
||||||
|
return this.code === DynamicValueReason.DYNAMIC_INPUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
isFromDynamicString(this: DynamicValue<R>): this is DynamicValue {
|
||||||
|
return this.code === DynamicValueReason.DYNAMIC_STRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
isFromExternalReference(this: DynamicValue<R>): this is DynamicValue<Reference<ts.Declaration>> {
|
||||||
|
return this.code === DynamicValueReason.EXTERNAL_REFERENCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
isFromUnknownExpressionType(this: DynamicValue<R>): this is DynamicValue {
|
||||||
|
return this.code === DynamicValueReason.UNKNOWN_EXPRESSION_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
isFromUnknownIdentifier(this: DynamicValue<R>): this is DynamicValue {
|
||||||
|
return this.code === DynamicValueReason.UNKNOWN_IDENTIFIER;
|
||||||
|
}
|
||||||
|
|
||||||
|
isFromUnknown(this: DynamicValue<R>): this is DynamicValue {
|
||||||
|
return this.code === DynamicValueReason.UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,8 @@ import {AbsoluteReference, NodeReference, Reference, ReferenceResolver, Resolved
|
|||||||
import {Declaration, ReflectionHost} from '../../reflection';
|
import {Declaration, ReflectionHost} from '../../reflection';
|
||||||
|
|
||||||
import {ArraySliceBuiltinFn} from './builtin';
|
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)) {
|
} else if (this.host.isClass(node)) {
|
||||||
return this.visitDeclaration(node, context);
|
return this.visitDeclaration(node, context);
|
||||||
} else {
|
} else {
|
||||||
return DYNAMIC_VALUE;
|
return DynamicValue.fromUnknownExpressionType(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,21 +138,15 @@ export class StaticInterpreter {
|
|||||||
const element = node.elements[i];
|
const element = node.elements[i];
|
||||||
if (ts.isSpreadElement(element)) {
|
if (ts.isSpreadElement(element)) {
|
||||||
const spread = this.visitExpression(element.expression, context);
|
const spread = this.visitExpression(element.expression, context);
|
||||||
if (isDynamicValue(spread)) {
|
if (spread instanceof DynamicValue) {
|
||||||
return DYNAMIC_VALUE;
|
array.push(DynamicValue.fromDynamicInput(element.expression, spread));
|
||||||
}
|
} else if (!Array.isArray(spread)) {
|
||||||
if (!Array.isArray(spread)) {
|
|
||||||
throw new Error(`Unexpected value in spread expression: ${spread}`);
|
throw new Error(`Unexpected value in spread expression: ${spread}`);
|
||||||
|
} else {
|
||||||
|
array.push(...spread);
|
||||||
}
|
}
|
||||||
|
|
||||||
array.push(...spread);
|
|
||||||
} else {
|
} else {
|
||||||
const result = this.visitExpression(element, context);
|
array.push(this.visitExpression(element, context));
|
||||||
if (isDynamicValue(result)) {
|
|
||||||
return DYNAMIC_VALUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
array.push(result);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return array;
|
return array;
|
||||||
@ -164,30 +159,28 @@ export class StaticInterpreter {
|
|||||||
const property = node.properties[i];
|
const property = node.properties[i];
|
||||||
if (ts.isPropertyAssignment(property)) {
|
if (ts.isPropertyAssignment(property)) {
|
||||||
const name = this.stringNameFromPropertyName(property.name, context);
|
const name = this.stringNameFromPropertyName(property.name, context);
|
||||||
|
|
||||||
// Check whether the name can be determined statically.
|
// Check whether the name can be determined statically.
|
||||||
if (name === undefined) {
|
if (name === undefined) {
|
||||||
return DYNAMIC_VALUE;
|
return DynamicValue.fromDynamicInput(node, DynamicValue.fromDynamicString(property.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
map.set(name, this.visitExpression(property.initializer, context));
|
map.set(name, this.visitExpression(property.initializer, context));
|
||||||
} else if (ts.isShorthandPropertyAssignment(property)) {
|
} else if (ts.isShorthandPropertyAssignment(property)) {
|
||||||
const symbol = this.checker.getShorthandAssignmentValueSymbol(property);
|
const symbol = this.checker.getShorthandAssignmentValueSymbol(property);
|
||||||
if (symbol === undefined || symbol.valueDeclaration === undefined) {
|
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)) {
|
} else if (ts.isSpreadAssignment(property)) {
|
||||||
const spread = this.visitExpression(property.expression, context);
|
const spread = this.visitExpression(property.expression, context);
|
||||||
if (isDynamicValue(spread)) {
|
if (spread instanceof DynamicValue) {
|
||||||
return DYNAMIC_VALUE;
|
return DynamicValue.fromDynamicInput(node, spread);
|
||||||
}
|
} else if (!(spread instanceof Map)) {
|
||||||
if (!(spread instanceof Map)) {
|
|
||||||
throw new Error(`Unexpected value in spread assignment: ${spread}`);
|
throw new Error(`Unexpected value in spread assignment: ${spread}`);
|
||||||
}
|
}
|
||||||
spread.forEach((value, key) => map.set(key, value));
|
spread.forEach((value, key) => map.set(key, value));
|
||||||
} else {
|
} else {
|
||||||
return DYNAMIC_VALUE;
|
return DynamicValue.fromUnknown(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
@ -201,8 +194,10 @@ export class StaticInterpreter {
|
|||||||
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ||
|
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ||
|
||||||
value == null) {
|
value == null) {
|
||||||
pieces.push(`${value}`);
|
pieces.push(`${value}`);
|
||||||
|
} else if (value instanceof DynamicValue) {
|
||||||
|
return DynamicValue.fromDynamicInput(node, value);
|
||||||
} else {
|
} else {
|
||||||
return DYNAMIC_VALUE;
|
return DynamicValue.fromDynamicInput(node, DynamicValue.fromDynamicString(span.expression));
|
||||||
}
|
}
|
||||||
pieces.push(span.literal.text);
|
pieces.push(span.literal.text);
|
||||||
}
|
}
|
||||||
@ -212,7 +207,7 @@ export class StaticInterpreter {
|
|||||||
private visitIdentifier(node: ts.Identifier, context: Context): ResolvedValue {
|
private visitIdentifier(node: ts.Identifier, context: Context): ResolvedValue {
|
||||||
const decl = this.host.getDeclarationOfIdentifier(node);
|
const decl = this.host.getDeclarationOfIdentifier(node);
|
||||||
if (decl === null) {
|
if (decl === null) {
|
||||||
return DYNAMIC_VALUE;
|
return DynamicValue.fromUnknownIdentifier(node);
|
||||||
}
|
}
|
||||||
const result =
|
const result =
|
||||||
this.visitDeclaration(decl.node, {...context, ...joinModuleContext(context, node, decl)});
|
this.visitDeclaration(decl.node, {...context, ...joinModuleContext(context, node, decl)});
|
||||||
@ -270,19 +265,19 @@ export class StaticInterpreter {
|
|||||||
if (node.argumentExpression === undefined) {
|
if (node.argumentExpression === undefined) {
|
||||||
throw new Error(`Expected argument in ElementAccessExpression`);
|
throw new Error(`Expected argument in ElementAccessExpression`);
|
||||||
}
|
}
|
||||||
if (isDynamicValue(lhs)) {
|
if (lhs instanceof DynamicValue) {
|
||||||
return DYNAMIC_VALUE;
|
return DynamicValue.fromDynamicInput(node, lhs);
|
||||||
}
|
}
|
||||||
const rhs = this.visitExpression(node.argumentExpression, context);
|
const rhs = this.visitExpression(node.argumentExpression, context);
|
||||||
if (isDynamicValue(rhs)) {
|
if (rhs instanceof DynamicValue) {
|
||||||
return DYNAMIC_VALUE;
|
return DynamicValue.fromDynamicInput(node, rhs);
|
||||||
}
|
}
|
||||||
if (typeof rhs !== 'string' && typeof rhs !== 'number') {
|
if (typeof rhs !== 'string' && typeof rhs !== 'number') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`ElementAccessExpression index should be string or number, got ${typeof rhs}: ${rhs}`);
|
`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):
|
private visitPropertyAccessExpression(node: ts.PropertyAccessExpression, context: Context):
|
||||||
@ -290,17 +285,16 @@ export class StaticInterpreter {
|
|||||||
const lhs = this.visitExpression(node.expression, context);
|
const lhs = this.visitExpression(node.expression, context);
|
||||||
const rhs = node.name.text;
|
const rhs = node.name.text;
|
||||||
// TODO: handle reference to class declaration.
|
// TODO: handle reference to class declaration.
|
||||||
if (isDynamicValue(lhs)) {
|
if (lhs instanceof DynamicValue) {
|
||||||
return DYNAMIC_VALUE;
|
return DynamicValue.fromDynamicInput(node, lhs);
|
||||||
}
|
}
|
||||||
|
return this.accessHelper(node, lhs, rhs, context);
|
||||||
return this.accessHelper(lhs, rhs, context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private visitSourceFile(node: ts.SourceFile, context: Context): ResolvedValue {
|
private visitSourceFile(node: ts.SourceFile, context: Context): ResolvedValue {
|
||||||
const declarations = this.host.getExportsOfModule(node);
|
const declarations = this.host.getExportsOfModule(node);
|
||||||
if (declarations === null) {
|
if (declarations === null) {
|
||||||
return DYNAMIC_VALUE;
|
return DynamicValue.fromUnknown(node);
|
||||||
}
|
}
|
||||||
const map = new Map<string, ResolvedValue>();
|
const map = new Map<string, ResolvedValue>();
|
||||||
declarations.forEach((decl, name) => {
|
declarations.forEach((decl, name) => {
|
||||||
@ -313,7 +307,9 @@ export class StaticInterpreter {
|
|||||||
return map;
|
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}`;
|
const strIndex = `${rhs}`;
|
||||||
if (lhs instanceof Map) {
|
if (lhs instanceof Map) {
|
||||||
if (lhs.has(strIndex)) {
|
if (lhs.has(strIndex)) {
|
||||||
@ -325,10 +321,10 @@ export class StaticInterpreter {
|
|||||||
if (rhs === 'length') {
|
if (rhs === 'length') {
|
||||||
return lhs.length;
|
return lhs.length;
|
||||||
} else if (rhs === 'slice') {
|
} else if (rhs === 'slice') {
|
||||||
return new ArraySliceBuiltinFn(lhs);
|
return new ArraySliceBuiltinFn(node, lhs);
|
||||||
}
|
}
|
||||||
if (typeof rhs !== 'number' || !Number.isInteger(rhs)) {
|
if (typeof rhs !== 'number' || !Number.isInteger(rhs)) {
|
||||||
return DYNAMIC_VALUE;
|
return DynamicValue.fromUnknown(node);
|
||||||
}
|
}
|
||||||
if (rhs < 0 || rhs >= lhs.length) {
|
if (rhs < 0 || rhs >= lhs.length) {
|
||||||
throw new Error(`Index out of bounds: ${rhs} vs ${lhs.length}`);
|
throw new Error(`Index out of bounds: ${rhs} vs ${lhs.length}`);
|
||||||
@ -355,14 +351,17 @@ export class StaticInterpreter {
|
|||||||
}
|
}
|
||||||
return value;
|
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 {
|
private visitCallExpression(node: ts.CallExpression, context: Context): ResolvedValue {
|
||||||
const lhs = this.visitExpression(node.expression, context);
|
const lhs = this.visitExpression(node.expression, context);
|
||||||
if (isDynamicValue(lhs)) {
|
if (lhs instanceof DynamicValue) {
|
||||||
return DYNAMIC_VALUE;
|
return DynamicValue.fromDynamicInput(node, lhs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the call refers to a builtin function, attempt to evaluate the function.
|
// 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);
|
expr = context.foreignFunctionResolver(lhs, node.arguments);
|
||||||
}
|
}
|
||||||
if (expr === null) {
|
if (expr === null) {
|
||||||
throw new Error(
|
return DynamicValue.fromDynamicInput(
|
||||||
`could not resolve foreign function declaration: ${node.getSourceFile().fileName} ${(lhs.node.name as ts.Identifier).text}`);
|
node, DynamicValue.fromExternalReference(node.expression, lhs));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the function is declared in a different file, resolve the foreign function expression
|
// 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):
|
private visitConditionalExpression(node: ts.ConditionalExpression, context: Context):
|
||||||
ResolvedValue {
|
ResolvedValue {
|
||||||
const condition = this.visitExpression(node.condition, context);
|
const condition = this.visitExpression(node.condition, context);
|
||||||
if (isDynamicValue(condition)) {
|
if (condition instanceof DynamicValue) {
|
||||||
return condition;
|
return DynamicValue.fromDynamicInput(node, condition);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (condition) {
|
if (condition) {
|
||||||
@ -452,7 +451,11 @@ export class StaticInterpreter {
|
|||||||
|
|
||||||
const op = UNARY_OPERATORS.get(operatorKind) !;
|
const op = UNARY_OPERATORS.get(operatorKind) !;
|
||||||
const value = this.visitExpression(node.operand, context);
|
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 {
|
private visitBinaryExpression(node: ts.BinaryExpression, context: Context): ResolvedValue {
|
||||||
@ -470,8 +473,13 @@ export class StaticInterpreter {
|
|||||||
lhs = this.visitExpression(node.left, context);
|
lhs = this.visitExpression(node.left, context);
|
||||||
rhs = this.visitExpression(node.right, context);
|
rhs = this.visitExpression(node.right, context);
|
||||||
}
|
}
|
||||||
|
if (lhs instanceof DynamicValue) {
|
||||||
return isDynamicValue(lhs) || isDynamicValue(rhs) ? DYNAMIC_VALUE : opRecord.op(lhs, rhs);
|
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):
|
private visitParenthesizedExpression(node: ts.ParenthesizedExpression, context: Context):
|
||||||
@ -500,13 +508,10 @@ function isFunctionOrMethodReference(ref: Reference<ts.Node>):
|
|||||||
}
|
}
|
||||||
|
|
||||||
function literal(value: ResolvedValue): any {
|
function literal(value: ResolvedValue): any {
|
||||||
if (value === null || value === undefined || typeof value === 'string' ||
|
if (value instanceof DynamicValue || value === null || value === undefined ||
|
||||||
typeof value === 'number' || typeof value === 'boolean') {
|
typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
if (isDynamicValue(value)) {
|
|
||||||
return DYNAMIC_VALUE;
|
|
||||||
}
|
|
||||||
throw new Error(`Value ${value} is not literal and cannot be used in this context.`);
|
throw new Error(`Value ${value} is not literal and cannot be used in this context.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@ import * as ts from 'typescript';
|
|||||||
|
|
||||||
import {Reference} from '../../imports';
|
import {Reference} from '../../imports';
|
||||||
|
|
||||||
|
import {DynamicValue} from './dynamic';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A value resulting from static resolution.
|
* A value resulting from static resolution.
|
||||||
@ -19,34 +21,7 @@ import {Reference} from '../../imports';
|
|||||||
* available statically.
|
* available statically.
|
||||||
*/
|
*/
|
||||||
export type ResolvedValue = number | boolean | string | null | undefined | Reference | EnumValue |
|
export type ResolvedValue = number | boolean | string | null | undefined | Reference | EnumValue |
|
||||||
ResolvedValueArray | ResolvedValueMap | BuiltinFn | DynamicValue;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of `ResolvedValue`s.
|
* An array of `ResolvedValue`s.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user