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, 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. * 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. * Represents a value which cannot be determined statically.
*/ */
export class DynamicValue<R = {}> { export class DynamicValue<R = unknown> {
private constructor( private constructor(
readonly node: ts.Node, readonly reason: R, private code: DynamicValueReason) {} readonly node: ts.Node, readonly reason: R, private code: DynamicValueReason) {}
@ -64,7 +71,7 @@ export class DynamicValue<R = {}> {
} }
static fromDynamicString(node: ts.Node): DynamicValue { 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>): static fromExternalReference(node: ts.Node, ref: Reference<ts.Declaration>):
@ -73,15 +80,19 @@ export class DynamicValue<R = {}> {
} }
static fromUnknownExpressionType(node: ts.Node): DynamicValue { 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 { 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 { 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> { isFromDynamicInput(this: DynamicValue<R>): this is DynamicValue<DynamicValue> {
@ -104,6 +115,10 @@ export class DynamicValue<R = {}> {
return this.code === DynamicValueReason.UNKNOWN_IDENTIFIER; 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 { isFromUnknown(this: DynamicValue<R>): this is DynamicValue {
return this.code === DynamicValueReason.UNKNOWN; return this.code === DynamicValueReason.UNKNOWN;
} }

View File

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

View File

@ -21,7 +21,7 @@ import {DynamicValue} from './dynamic';
* 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<unknown>;
/** /**
* An array of `ResolvedValue`s. * An array of `ResolvedValue`s.

View File

@ -11,6 +11,7 @@ import * as ts from 'typescript';
import {Reference} from '../../imports'; import {Reference} from '../../imports';
import {TypeScriptReflectionHost} from '../../reflection'; import {TypeScriptReflectionHost} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {DynamicValue} from '../src/dynamic';
import {PartialEvaluator} from '../src/interface'; import {PartialEvaluator} from '../src/interface';
import {EnumValue, ResolvedValue} from '../src/result'; import {EnumValue, ResolvedValue} from '../src/result';
@ -281,6 +282,31 @@ describe('ngtsc metadata', () => {
const resolved = evaluator.evaluate(prop.name); const resolved = evaluator.evaluate(prop.name);
expect(resolved).toBe(42); 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 { function owningModuleOf(ref: Reference): string|null {