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,
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue