feat(ivy): support tracking the provenance of DynamicValue (#29033)

DynamicValues are generated whenever a partially evaluated expression is
unable to be resolved statically. They contain a reference to the ts.Node
which wasn't resolvable.

They can also be nested. For example, the expression 'a + b' is resolvable
only if 'a' and 'b' are themselves resolvable. If either 'a' or 'b' resolve
to a DynamicValue, the whole expression must also resolve to a DynamicValue.

Previously, if 'a' resolved to a DynamicValue, the entire expression might
have been resolved to the same DynamicValue. This correctly indicated that
the expression wasn't resolvable, but didn't return a reference to the
shallow node that couldn't be resolved (the expression 'a + b'), only a
reference to the deep node that couldn't be resolved ('a').

In certain situations, it's very useful to know the shallow unresolvable
node (for example, to use it verbatim in the output). To support this,
the partial evaluator is updated to always wrap DynamicValue to point to
each unresolvable expression as it's processed, ensuring the receiver can
determine exactly which expression node failed to resolve.

PR Close #29033
This commit is contained in:
Alex Rickabaugh 2019-02-27 15:32:38 -08:00 committed by Andrew Kushnir
parent 7ac58bec8a
commit a23a0bc3a4
4 changed files with 70 additions and 23 deletions

View File

@ -46,6 +46,13 @@ export const enum DynamicValueReason {
*/
UNKNOWN_IDENTIFIER,
/**
* A value could be resolved, but is not an acceptable type for the operation being performed.
*
* For example, attempting to call a non-callable expression.
*/
INVALID_EXPRESSION_TYPE,
/**
* A value could not be determined statically for any reason other the above.
*/
@ -55,7 +62,7 @@ export const enum DynamicValueReason {
/**
* Represents a value which cannot be determined statically.
*/
export class DynamicValue<R = {}> {
export class DynamicValue<R = unknown> {
private constructor(
readonly node: ts.Node, readonly reason: R, private code: DynamicValueReason) {}
@ -64,7 +71,7 @@ export class DynamicValue<R = {}> {
}
static fromDynamicString(node: ts.Node): DynamicValue {
return new DynamicValue(node, {}, DynamicValueReason.DYNAMIC_STRING);
return new DynamicValue(node, undefined, DynamicValueReason.DYNAMIC_STRING);
}
static fromExternalReference(node: ts.Node, ref: Reference<ts.Declaration>):
@ -73,15 +80,19 @@ export class DynamicValue<R = {}> {
}
static fromUnknownExpressionType(node: ts.Node): DynamicValue {
return new DynamicValue(node, {}, DynamicValueReason.UNKNOWN_EXPRESSION_TYPE);
return new DynamicValue(node, undefined, DynamicValueReason.UNKNOWN_EXPRESSION_TYPE);
}
static fromUnknownIdentifier(node: ts.Identifier): DynamicValue {
return new DynamicValue(node, {}, DynamicValueReason.UNKNOWN_IDENTIFIER);
return new DynamicValue(node, undefined, DynamicValueReason.UNKNOWN_IDENTIFIER);
}
static fromInvalidExpressionType(node: ts.Node, value: unknown): DynamicValue<unknown> {
return new DynamicValue(node, value, DynamicValueReason.INVALID_EXPRESSION_TYPE);
}
static fromUnknown(node: ts.Node): DynamicValue {
return new DynamicValue(node, {}, DynamicValueReason.UNKNOWN);
return new DynamicValue(node, undefined, DynamicValueReason.UNKNOWN);
}
isFromDynamicInput(this: DynamicValue<R>): this is DynamicValue<DynamicValue> {
@ -104,6 +115,10 @@ export class DynamicValue<R = {}> {
return this.code === DynamicValueReason.UNKNOWN_IDENTIFIER;
}
isFromInvalidExpressionType(this: DynamicValue<R>): this is DynamicValue<unknown> {
return this.code === DynamicValueReason.INVALID_EXPRESSION_TYPE;
}
isFromUnknown(this: DynamicValue<R>): this is DynamicValue {
return this.code === DynamicValueReason.UNKNOWN;
}

View File

@ -86,6 +86,7 @@ export class StaticInterpreter {
}
private visitExpression(node: ts.Expression, context: Context): ResolvedValue {
let result: ResolvedValue;
if (node.kind === ts.SyntaxKind.TrueKeyword) {
return true;
} else if (node.kind === ts.SyntaxKind.FalseKeyword) {
@ -95,38 +96,42 @@ export class StaticInterpreter {
} else if (ts.isNoSubstitutionTemplateLiteral(node)) {
return node.text;
} else if (ts.isTemplateExpression(node)) {
return this.visitTemplateExpression(node, context);
result = this.visitTemplateExpression(node, context);
} else if (ts.isNumericLiteral(node)) {
return parseFloat(node.text);
} else if (ts.isObjectLiteralExpression(node)) {
return this.visitObjectLiteralExpression(node, context);
result = this.visitObjectLiteralExpression(node, context);
} else if (ts.isIdentifier(node)) {
return this.visitIdentifier(node, context);
result = this.visitIdentifier(node, context);
} else if (ts.isPropertyAccessExpression(node)) {
return this.visitPropertyAccessExpression(node, context);
result = this.visitPropertyAccessExpression(node, context);
} else if (ts.isCallExpression(node)) {
return this.visitCallExpression(node, context);
result = this.visitCallExpression(node, context);
} else if (ts.isConditionalExpression(node)) {
return this.visitConditionalExpression(node, context);
result = this.visitConditionalExpression(node, context);
} else if (ts.isPrefixUnaryExpression(node)) {
return this.visitPrefixUnaryExpression(node, context);
result = this.visitPrefixUnaryExpression(node, context);
} else if (ts.isBinaryExpression(node)) {
return this.visitBinaryExpression(node, context);
result = this.visitBinaryExpression(node, context);
} else if (ts.isArrayLiteralExpression(node)) {
return this.visitArrayLiteralExpression(node, context);
result = this.visitArrayLiteralExpression(node, context);
} else if (ts.isParenthesizedExpression(node)) {
return this.visitParenthesizedExpression(node, context);
result = this.visitParenthesizedExpression(node, context);
} else if (ts.isElementAccessExpression(node)) {
return this.visitElementAccessExpression(node, context);
result = this.visitElementAccessExpression(node, context);
} else if (ts.isAsExpression(node)) {
return this.visitExpression(node.expression, context);
result = this.visitExpression(node.expression, context);
} else if (ts.isNonNullExpression(node)) {
return this.visitExpression(node.expression, context);
result = this.visitExpression(node.expression, context);
} else if (this.host.isClass(node)) {
return this.visitDeclaration(node, context);
result = this.visitDeclaration(node, context);
} else {
return DynamicValue.fromUnknownExpressionType(node);
}
if (result instanceof DynamicValue && result.node !== node) {
return DynamicValue.fromDynamicInput(node, result);
}
return result;
}
private visitArrayLiteralExpression(node: ts.ArrayLiteralExpression, context: Context):
@ -211,6 +216,8 @@ export class StaticInterpreter {
this.visitDeclaration(decl.node, {...context, ...joinModuleContext(context, node, decl)});
if (result instanceof Reference) {
result.addIdentifier(node);
} else if (result instanceof DynamicValue) {
return DynamicValue.fromDynamicInput(node, result);
}
return result;
}
@ -365,10 +372,9 @@ export class StaticInterpreter {
}
if (!(lhs instanceof Reference)) {
throw new Error(`attempting to call something that is not a function: ${lhs}`);
return DynamicValue.fromInvalidExpressionType(node.expression, lhs);
} else if (!isFunctionOrMethodReference(lhs)) {
throw new Error(
`calling something that is not a function declaration? ${ts.SyntaxKind[lhs.node.kind]} (${node.getText()})`);
return DynamicValue.fromInvalidExpressionType(node.expression, lhs);
}
const fn = this.host.getDefinitionOfFunction(lhs.node);

View File

@ -21,7 +21,7 @@ import {DynamicValue} from './dynamic';
* available statically.
*/
export type ResolvedValue = number | boolean | string | null | undefined | Reference | EnumValue |
ResolvedValueArray | ResolvedValueMap | BuiltinFn | DynamicValue<{}>;
ResolvedValueArray | ResolvedValueMap | BuiltinFn | DynamicValue<unknown>;
/**
* An array of `ResolvedValue`s.

View File

@ -11,6 +11,7 @@ import * as ts from 'typescript';
import {Reference} from '../../imports';
import {TypeScriptReflectionHost} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {DynamicValue} from '../src/dynamic';
import {PartialEvaluator} from '../src/interface';
import {EnumValue, ResolvedValue} from '../src/result';
@ -281,6 +282,31 @@ describe('ngtsc metadata', () => {
const resolved = evaluator.evaluate(prop.name);
expect(resolved).toBe(42);
});
it('should resolve dynamic values in object literals', () => {
const {program} = makeProgram([
{name: 'decl.d.ts', contents: 'export declare const fn: any;'},
{
name: 'entry.ts',
contents: `import {fn} from './decl'; const prop = fn.foo(); const target$ = {value: prop};`
},
]);
const checker = program.getTypeChecker();
const reflectionHost = new TypeScriptReflectionHost(checker);
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
const expr = result.initializer !as ts.ObjectLiteralExpression;
const evaluator = new PartialEvaluator(reflectionHost, checker);
const resolved = evaluator.evaluate(expr);
if (!(resolved instanceof Map)) {
return fail('Should have resolved to a Map');
}
const value = resolved.get('value') !;
if (!(value instanceof DynamicValue)) {
return fail(`Should have resolved 'value' to a DynamicValue`);
}
const prop = expr.properties[0] as ts.PropertyAssignment;
expect(value.node).toBe(prop.initializer);
});
});
function owningModuleOf(ref: Reference): string|null {