fix(compiler-cli): add support for partially evaluating types (#41661)

Add support to the partial evaluator for evaluating literal types and
tuples.

resolves #41338

PR Close #41661
This commit is contained in:
Zach Arend 2021-04-12 10:05:14 -07:00 committed by Jessica Janiuk
parent 45d24d28a6
commit 37a740c659
4 changed files with 94 additions and 1 deletions

View File

@ -127,6 +127,10 @@ class TraceDynamicValueVisitor implements DynamicValueVisitor<ts.DiagnosticRelat
return [makeRelatedInformation(value.node, 'Unknown reference.')];
}
visitDynamicType(value: DynamicValue): ts.DiagnosticRelatedInformation[] {
return [makeRelatedInformation(value.node, 'Dynamic type.')];
}
visitUnsupportedSyntax(value: DynamicValue): ts.DiagnosticRelatedInformation[] {
return [makeRelatedInformation(value.node, 'This syntax is not supported.')];
}

View File

@ -61,6 +61,22 @@ export const enum DynamicValueReason {
*/
COMPLEX_FUNCTION_CALL,
/**
* A value that could not be determined because it contains type information that cannot be
* statically evaluated. This happens when producing a value from type information, but the value
* of the given type cannot be determined statically.
*
* E.g. evaluating a tuple.
*
* `declare const foo: [string];`
*
* Evaluating `foo` gives a DynamicValue wrapped in an array with a reason of DYNAMIC_TYPE. This
* is because the static evaluator has a `string` type for the first element of this tuple, and
* the value of that string cannot be determined statically. The type `string` permits it to be
* 'foo', 'bar' or any arbitrary string, so we evaluate it to a DynamicValue.
*/
DYNAMIC_TYPE,
/**
* A value could not be determined statically for any reason other the above.
*/
@ -104,6 +120,10 @@ export class DynamicValue<R = unknown> {
return new DynamicValue(node, fn, DynamicValueReason.COMPLEX_FUNCTION_CALL);
}
static fromDynamicType(node: ts.TypeNode): DynamicValue {
return new DynamicValue(node, undefined, DynamicValueReason.DYNAMIC_TYPE);
}
static fromUnknown(node: ts.Node): DynamicValue {
return new DynamicValue(node, undefined, DynamicValueReason.UNKNOWN);
}
@ -136,6 +156,10 @@ export class DynamicValue<R = unknown> {
return this.code === DynamicValueReason.COMPLEX_FUNCTION_CALL;
}
isFromDynamicType(this: DynamicValue<R>): this is DynamicValue {
return this.code === DynamicValueReason.DYNAMIC_TYPE;
}
isFromUnknown(this: DynamicValue<R>): this is DynamicValue {
return this.code === DynamicValueReason.UNKNOWN;
}
@ -158,6 +182,8 @@ export class DynamicValue<R = unknown> {
case DynamicValueReason.COMPLEX_FUNCTION_CALL:
return visitor.visitComplexFunctionCall(
this as unknown as DynamicValue<FunctionDefinition>);
case DynamicValueReason.DYNAMIC_TYPE:
return visitor.visitDynamicType(this);
case DynamicValueReason.UNKNOWN:
return visitor.visitUnknown(this);
}
@ -172,5 +198,6 @@ export interface DynamicValueVisitor<R> {
visitUnknownIdentifier(value: DynamicValue): R;
visitInvalidExpressionType(value: DynamicValue): R;
visitComplexFunctionCall(value: DynamicValue<FunctionDefinition>): R;
visitDynamicType(value: DynamicValue): R;
visitUnknown(value: DynamicValue): R;
}

View File

@ -276,12 +276,28 @@ export class StaticInterpreter {
return this.getReference(node, context);
}
}
private visitVariableDeclaration(node: ts.VariableDeclaration, context: Context): ResolvedValue {
const value = this.host.getVariableValue(node);
if (value !== null) {
return this.visitExpression(value, context);
} else if (isVariableDeclarationDeclared(node)) {
// If the declaration has a literal type that can be statically reduced to a value, resolve to
// that value. If not, the historical behavior for variable declarations is to return a
// `Reference` to the variable, as the consumer could use it in a context where knowing its
// static value is not necessary.
//
// Arguably, since the value cannot be statically determined, we should return a
// `DynamicValue`. This returns a `Reference` because it's the same behavior as before
// `visitType` was introduced.
//
// TODO(zarend): investigate switching to a `DynamicValue` and verify this won't break any
// use cases, especially in ngcc
if (node.type !== undefined) {
const evaluatedType = this.visitType(node.type, context);
if (!(evaluatedType instanceof DynamicValue)) {
return evaluatedType;
}
}
return this.getReference(node, context);
} else {
return undefined;
@ -681,6 +697,28 @@ export class StaticInterpreter {
private getReference<T extends DeclarationNode>(node: T, context: Context): Reference<T> {
return new Reference(node, owningModule(context));
}
private visitType(node: ts.TypeNode, context: Context): ResolvedValue {
if (ts.isLiteralTypeNode(node)) {
return this.visitExpression(node.literal, context);
} else if (ts.isTupleTypeNode(node)) {
return this.visitTupleType(node, context);
} else if (ts.isNamedTupleMember(node)) {
return this.visitType(node.type, context);
}
return DynamicValue.fromDynamicType(node);
}
private visitTupleType(node: ts.TupleTypeNode, context: Context): ResolvedValueArray {
const res: ResolvedValueArray = [];
for (const elem of node.elements) {
res.push(this.visitType(elem, context));
}
return res;
}
}
function isFunctionOrMethodReference(ref: Reference<ts.Node>):

View File

@ -357,6 +357,30 @@ runInEachFileSystem(() => {
expect(value.reason.node.getText()).toEqual('window: any');
});
it('supports declarations of primitive constant types', () => {
expect(evaluate(`declare const x: 'foo';`, `x`)).toEqual('foo');
expect(evaluate(`declare const x: 42;`, `x`)).toEqual(42);
expect(evaluate(`declare const x: null;`, `x`)).toEqual(null);
expect(evaluate(`declare const x: true;`, `x`)).toEqual(true);
});
it('supports declarations of tuples', () => {
expect(evaluate(`declare const x: ['foo', 42, null, true];`, `x`)).toEqual([
'foo', 42, null, true
]);
expect(evaluate(`declare const x: ['bar'];`, `[...x]`)).toEqual(['bar']);
});
it('evaluates tuple elements it cannot understand to DynamicValue', () => {
const value = evaluate(`declare const x: ['foo', string];`, `x`) as [string, DynamicValue];
expect(Array.isArray(value)).toBeTrue();
expect(value[0]).toEqual('foo');
expect(value[1] instanceof DynamicValue).toBeTrue();
expect(value[1].isFromDynamicType()).toBe(true);
});
it('imports work', () => {
const {program} = makeProgram([
{name: _('/second.ts'), contents: 'export function foo(bar) { return bar; }'},