From 37a740c6590b7725053f8578d39e53ecba4f5727 Mon Sep 17 00:00:00 2001 From: Zach Arend Date: Mon, 12 Apr 2021 10:05:14 -0700 Subject: [PATCH] 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 --- .../partial_evaluator/src/diagnostics.ts | 4 ++ .../ngtsc/partial_evaluator/src/dynamic.ts | 27 +++++++++++++ .../partial_evaluator/src/interpreter.ts | 40 ++++++++++++++++++- .../partial_evaluator/test/evaluator_spec.ts | 24 +++++++++++ 4 files changed, 94 insertions(+), 1 deletion(-) diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/diagnostics.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/diagnostics.ts index e270cb213c..17af7e00f6 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/diagnostics.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/diagnostics.ts @@ -127,6 +127,10 @@ class TraceDynamicValueVisitor implements DynamicValueVisitor { 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 { return this.code === DynamicValueReason.COMPLEX_FUNCTION_CALL; } + isFromDynamicType(this: DynamicValue): this is DynamicValue { + return this.code === DynamicValueReason.DYNAMIC_TYPE; + } + isFromUnknown(this: DynamicValue): this is DynamicValue { return this.code === DynamicValueReason.UNKNOWN; } @@ -158,6 +182,8 @@ export class DynamicValue { case DynamicValueReason.COMPLEX_FUNCTION_CALL: return visitor.visitComplexFunctionCall( this as unknown as DynamicValue); + case DynamicValueReason.DYNAMIC_TYPE: + return visitor.visitDynamicType(this); case DynamicValueReason.UNKNOWN: return visitor.visitUnknown(this); } @@ -172,5 +198,6 @@ export interface DynamicValueVisitor { visitUnknownIdentifier(value: DynamicValue): R; visitInvalidExpressionType(value: DynamicValue): R; visitComplexFunctionCall(value: DynamicValue): R; + visitDynamicType(value: DynamicValue): R; visitUnknown(value: DynamicValue): R; } diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts index 5579e6ccf3..44868a6b52 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts @@ -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(node: T, context: Context): Reference { 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): diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts index e597f6d0f3..7582aaebcf 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts @@ -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; }'},