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:
parent
7ac58bec8a
commit
a23a0bc3a4
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue