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:
parent
45d24d28a6
commit
37a740c659
|
@ -127,6 +127,10 @@ class TraceDynamicValueVisitor implements DynamicValueVisitor<ts.DiagnosticRelat
|
||||||
return [makeRelatedInformation(value.node, 'Unknown reference.')];
|
return [makeRelatedInformation(value.node, 'Unknown reference.')];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
visitDynamicType(value: DynamicValue): ts.DiagnosticRelatedInformation[] {
|
||||||
|
return [makeRelatedInformation(value.node, 'Dynamic type.')];
|
||||||
|
}
|
||||||
|
|
||||||
visitUnsupportedSyntax(value: DynamicValue): ts.DiagnosticRelatedInformation[] {
|
visitUnsupportedSyntax(value: DynamicValue): ts.DiagnosticRelatedInformation[] {
|
||||||
return [makeRelatedInformation(value.node, 'This syntax is not supported.')];
|
return [makeRelatedInformation(value.node, 'This syntax is not supported.')];
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,22 @@ export const enum DynamicValueReason {
|
||||||
*/
|
*/
|
||||||
COMPLEX_FUNCTION_CALL,
|
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.
|
* 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);
|
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 {
|
static fromUnknown(node: ts.Node): DynamicValue {
|
||||||
return new DynamicValue(node, undefined, DynamicValueReason.UNKNOWN);
|
return new DynamicValue(node, undefined, DynamicValueReason.UNKNOWN);
|
||||||
}
|
}
|
||||||
|
@ -136,6 +156,10 @@ export class DynamicValue<R = unknown> {
|
||||||
return this.code === DynamicValueReason.COMPLEX_FUNCTION_CALL;
|
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 {
|
isFromUnknown(this: DynamicValue<R>): this is DynamicValue {
|
||||||
return this.code === DynamicValueReason.UNKNOWN;
|
return this.code === DynamicValueReason.UNKNOWN;
|
||||||
}
|
}
|
||||||
|
@ -158,6 +182,8 @@ export class DynamicValue<R = unknown> {
|
||||||
case DynamicValueReason.COMPLEX_FUNCTION_CALL:
|
case DynamicValueReason.COMPLEX_FUNCTION_CALL:
|
||||||
return visitor.visitComplexFunctionCall(
|
return visitor.visitComplexFunctionCall(
|
||||||
this as unknown as DynamicValue<FunctionDefinition>);
|
this as unknown as DynamicValue<FunctionDefinition>);
|
||||||
|
case DynamicValueReason.DYNAMIC_TYPE:
|
||||||
|
return visitor.visitDynamicType(this);
|
||||||
case DynamicValueReason.UNKNOWN:
|
case DynamicValueReason.UNKNOWN:
|
||||||
return visitor.visitUnknown(this);
|
return visitor.visitUnknown(this);
|
||||||
}
|
}
|
||||||
|
@ -172,5 +198,6 @@ export interface DynamicValueVisitor<R> {
|
||||||
visitUnknownIdentifier(value: DynamicValue): R;
|
visitUnknownIdentifier(value: DynamicValue): R;
|
||||||
visitInvalidExpressionType(value: DynamicValue): R;
|
visitInvalidExpressionType(value: DynamicValue): R;
|
||||||
visitComplexFunctionCall(value: DynamicValue<FunctionDefinition>): R;
|
visitComplexFunctionCall(value: DynamicValue<FunctionDefinition>): R;
|
||||||
|
visitDynamicType(value: DynamicValue): R;
|
||||||
visitUnknown(value: DynamicValue): R;
|
visitUnknown(value: DynamicValue): R;
|
||||||
}
|
}
|
||||||
|
|
|
@ -276,12 +276,28 @@ export class StaticInterpreter {
|
||||||
return this.getReference(node, context);
|
return this.getReference(node, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private visitVariableDeclaration(node: ts.VariableDeclaration, context: Context): ResolvedValue {
|
private visitVariableDeclaration(node: ts.VariableDeclaration, context: Context): ResolvedValue {
|
||||||
const value = this.host.getVariableValue(node);
|
const value = this.host.getVariableValue(node);
|
||||||
if (value !== null) {
|
if (value !== null) {
|
||||||
return this.visitExpression(value, context);
|
return this.visitExpression(value, context);
|
||||||
} else if (isVariableDeclarationDeclared(node)) {
|
} 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);
|
return this.getReference(node, context);
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -681,6 +697,28 @@ export class StaticInterpreter {
|
||||||
private getReference<T extends DeclarationNode>(node: T, context: Context): Reference<T> {
|
private getReference<T extends DeclarationNode>(node: T, context: Context): Reference<T> {
|
||||||
return new Reference(node, owningModule(context));
|
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>):
|
function isFunctionOrMethodReference(ref: Reference<ts.Node>):
|
||||||
|
|
|
@ -357,6 +357,30 @@ runInEachFileSystem(() => {
|
||||||
expect(value.reason.node.getText()).toEqual('window: any');
|
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', () => {
|
it('imports work', () => {
|
||||||
const {program} = makeProgram([
|
const {program} = makeProgram([
|
||||||
{name: _('/second.ts'), contents: 'export function foo(bar) { return bar; }'},
|
{name: _('/second.ts'), contents: 'export function foo(bar) { return bar; }'},
|
||||||
|
|
Loading…
Reference in New Issue