refactor(compiler-cli): more accurate reporting of complex function call (#37587)

This commit introduces a dedicated `DynamicValue` kind to indicate that a value
cannot be evaluated statically as the function body is not just a single return
statement. This allows more accurate reporting of why a function call failed
to be evaluated, i.e. we now include a reference to the function declaration
and have a tailor-made diagnostic message.

PR Close #37587
This commit is contained in:
JoostK 2020-06-15 17:55:04 +02:00 committed by Andrew Kushnir
parent 712f1bd0b7
commit ce879fc416
5 changed files with 47 additions and 6 deletions

View File

@ -10,6 +10,7 @@ import * as ts from 'typescript';
import {makeRelatedInformation} from '../../diagnostics';
import {Reference} from '../../imports';
import {FunctionDefinition} from '../../reflection';
import {DynamicValue, DynamicValueVisitor} from './dynamic';
import {EnumValue, KnownFn, ResolvedModule, ResolvedValue} from './result';
@ -104,6 +105,16 @@ class TraceDynamicValueVisitor implements DynamicValueVisitor<ts.DiagnosticRelat
description} cannot be determined statically, as it is an external declaration.`)];
}
visitComplexFunctionCall(value: DynamicValue<FunctionDefinition>):
ts.DiagnosticRelatedInformation[] {
return [
makeRelatedInformation(
value.node,
'Unable to evaluate function call of complex function. A function must have exactly one return statement.'),
makeRelatedInformation(value.reason.node, 'Function is declared here.')
];
}
visitInvalidExpressionType(value: DynamicValue): ts.DiagnosticRelatedInformation[] {
return [makeRelatedInformation(value.node, 'Unable to evaluate an invalid expression.')];
}

View File

@ -9,6 +9,7 @@
import * as ts from 'typescript';
import {Reference} from '../../imports';
import {FunctionDefinition} from '../../reflection';
/**
* The reason why a value cannot be determined statically.
@ -55,6 +56,11 @@ export const enum DynamicValueReason {
*/
INVALID_EXPRESSION_TYPE,
/**
* A function call could not be evaluated as the function's body is not a single return statement.
*/
COMPLEX_FUNCTION_CALL,
/**
* A value could not be determined statically for any reason other the above.
*/
@ -93,6 +99,11 @@ export class DynamicValue<R = unknown> {
return new DynamicValue(node, value, DynamicValueReason.INVALID_EXPRESSION_TYPE);
}
static fromComplexFunctionCall(node: ts.Node, fn: FunctionDefinition):
DynamicValue<FunctionDefinition> {
return new DynamicValue(node, fn, DynamicValueReason.COMPLEX_FUNCTION_CALL);
}
static fromUnknown(node: ts.Node): DynamicValue {
return new DynamicValue(node, undefined, DynamicValueReason.UNKNOWN);
}
@ -121,6 +132,10 @@ export class DynamicValue<R = unknown> {
return this.code === DynamicValueReason.INVALID_EXPRESSION_TYPE;
}
isFromComplexFunctionCall(this: DynamicValue<R>): this is DynamicValue<FunctionDefinition> {
return this.code === DynamicValueReason.COMPLEX_FUNCTION_CALL;
}
isFromUnknown(this: DynamicValue<R>): this is DynamicValue {
return this.code === DynamicValueReason.UNKNOWN;
}
@ -140,6 +155,9 @@ export class DynamicValue<R = unknown> {
return visitor.visitUnknownIdentifier(this);
case DynamicValueReason.INVALID_EXPRESSION_TYPE:
return visitor.visitInvalidExpressionType(this);
case DynamicValueReason.COMPLEX_FUNCTION_CALL:
return visitor.visitComplexFunctionCall(
this as unknown as DynamicValue<FunctionDefinition>);
case DynamicValueReason.UNKNOWN:
return visitor.visitUnknown(this);
}
@ -153,5 +171,6 @@ export interface DynamicValueVisitor<R> {
visitUnsupportedSyntax(value: DynamicValue): R;
visitUnknownIdentifier(value: DynamicValue): R;
visitInvalidExpressionType(value: DynamicValue): R;
visitComplexFunctionCall(value: DynamicValue<FunctionDefinition>): R;
visitUnknown(value: DynamicValue): R;
}

View File

@ -490,8 +490,10 @@ export class StaticInterpreter {
private visitFunctionBody(node: ts.CallExpression, fn: FunctionDefinition, context: Context):
ResolvedValue {
if (fn.body === null || fn.body.length !== 1 || !ts.isReturnStatement(fn.body[0])) {
if (fn.body === null) {
return DynamicValue.fromUnknown(node);
} else if (fn.body.length !== 1 || !ts.isReturnStatement(fn.body[0])) {
return DynamicValue.fromComplexFunctionCall(node, fn);
}
const ret = fn.body[0] as ts.ReturnStatement;

View File

@ -199,10 +199,16 @@ runInEachFileSystem(() => {
}`,
'complex()');
expect(trace.length).toBe(1);
expect(trace[0].messageText).toBe('Unable to evaluate statically.');
expect(trace.length).toBe(2);
expect(trace[0].messageText)
.toBe(
'Unable to evaluate function call of complex function. A function must have exactly one return statement.');
expect(trace[0].file!.fileName).toBe(_('/entry.ts'));
expect(getSourceCode(trace[0])).toBe('complex()');
expect(trace[1].messageText).toBe('Function is declared here.');
expect(trace[1].file!.fileName).toBe(_('/entry.ts'));
expect(getSourceCode(trace[1])).toContain(`console.log('test');`);
});
it('should trace object destructuring of external reference', () => {

View File

@ -622,15 +622,18 @@ runInEachFileSystem(() => {
expect(id.text).toEqual('Target');
});
it('should resolve functions with more than one statement to an unknown value', () => {
it('should resolve functions with more than one statement to a complex function call', () => {
const value = evaluate(`function foo(bar) { const b = bar; return b; }`, 'foo("test")');
if (!(value instanceof DynamicValue)) {
return fail(`Should have resolved to a DynamicValue`);
}
expect(value.isFromUnknown()).toBe(true);
if (!value.isFromComplexFunctionCall()) {
return fail('Expected DynamicValue to be from complex function call');
}
expect((value.node as ts.CallExpression).expression.getText()).toBe('foo');
expect((value.reason.node as ts.FunctionDeclaration).getText())
.toContain('const b = bar; return b;');
});
describe('(with imported TypeScript helpers)', () => {