feat(compiler-cli): explain why an expression cannot be used in AOT compilations (#37587)
During AOT compilation, the value of some expressions need to be known at compile time. The compiler has the ability to statically evaluate expressions the best it can, but there can be occurrences when an expression cannot be evaluated statically. For instance, the evaluation could depend on a dynamic value or syntax is used that the compiler does not understand. Alternatively, it is possible that an expression could be statically evaluated but the resulting value would be of an incorrect type. In these situations, it would be helpful if the compiler could explain why it is unable to evaluate an expression. To this extend, the static interpreter in Ivy keeps track of a trail of `DynamicValue`s which follow the path of nodes that were considered all the way to the node that causes an expression to be considered dynamic. Up until this commit, this rich trail of information was not surfaced to a developer so the compiler was of little help to explain why static evaluation failed, resulting in situations that are hard to debug and resolve. This commit adds much more insight to the diagnostic that is produced for static evaluation errors. For dynamic values, the trail of `DynamicValue` instances is presented to the user in a meaningful way. If a value is available but not of the correct type, the type of the resolved value is shown. Resolves FW-2155 PR Close #37587
This commit is contained in:
@ -11,6 +11,7 @@ ts_library(
|
||||
deps = [
|
||||
"//packages:types",
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli/src/ngtsc/diagnostics",
|
||||
"//packages/compiler-cli/src/ngtsc/imports",
|
||||
"//packages/compiler-cli/src/ngtsc/incremental:api",
|
||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||
|
@ -6,6 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export {describeResolvedType, traceDynamicValue} from './src/diagnostics';
|
||||
export {DynamicValue} from './src/dynamic';
|
||||
export {ForeignFunctionResolver, PartialEvaluator} from './src/interface';
|
||||
export {EnumValue, KnownFn, ResolvedValue, ResolvedValueArray, ResolvedValueMap} from './src/result';
|
||||
|
@ -0,0 +1,176 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {makeRelatedInformation} from '../../diagnostics';
|
||||
import {Reference} from '../../imports';
|
||||
import {DynamicValue, DynamicValueVisitor} from './dynamic';
|
||||
import {EnumValue, KnownFn, ResolvedModule, ResolvedValue} from './result';
|
||||
|
||||
/**
|
||||
* Derives a type representation from a resolved value to be reported in a diagnostic.
|
||||
*
|
||||
* @param value The resolved value for which a type representation should be derived.
|
||||
* @param maxDepth The maximum nesting depth of objects and arrays, defaults to 1 level.
|
||||
*/
|
||||
export function describeResolvedType(value: ResolvedValue, maxDepth: number = 1): string {
|
||||
if (value === null) {
|
||||
return 'null';
|
||||
} else if (value === undefined) {
|
||||
return 'undefined';
|
||||
} else if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'string') {
|
||||
return typeof value;
|
||||
} else if (value instanceof Map) {
|
||||
if (maxDepth === 0) {
|
||||
return 'object';
|
||||
}
|
||||
const entries = Array.from(value.entries()).map(([key, v]) => {
|
||||
return `${quoteKey(key)}: ${describeResolvedType(v, maxDepth - 1)}`;
|
||||
});
|
||||
return entries.length > 0 ? `{ ${entries.join('; ')} }` : '{}';
|
||||
} else if (value instanceof ResolvedModule) {
|
||||
return '(module)';
|
||||
} else if (value instanceof EnumValue) {
|
||||
return value.enumRef.debugName ?? '(anonymous)';
|
||||
} else if (value instanceof Reference) {
|
||||
return value.debugName ?? '(anonymous)';
|
||||
} else if (Array.isArray(value)) {
|
||||
if (maxDepth === 0) {
|
||||
return 'Array';
|
||||
}
|
||||
return `[${value.map(v => describeResolvedType(v, maxDepth - 1)).join(', ')}]`;
|
||||
} else if (value instanceof DynamicValue) {
|
||||
return '(not statically analyzable)';
|
||||
} else if (value instanceof KnownFn) {
|
||||
return 'Function';
|
||||
} else {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
function quoteKey(key: string): string {
|
||||
if (/^[a-z0-9_]+$/i.test(key)) {
|
||||
return key;
|
||||
} else {
|
||||
return `'${key.replace(/'/g, '\\\'')}'`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an array of related information diagnostics for a `DynamicValue` that describe the trace
|
||||
* of why an expression was evaluated as dynamic.
|
||||
*
|
||||
* @param node The node for which a `ts.Diagnostic` is to be created with the trace.
|
||||
* @param value The dynamic value for which a trace should be created.
|
||||
*/
|
||||
export function traceDynamicValue(
|
||||
node: ts.Node, value: DynamicValue): ts.DiagnosticRelatedInformation[] {
|
||||
return value.accept(new TraceDynamicValueVisitor(node));
|
||||
}
|
||||
|
||||
class TraceDynamicValueVisitor implements DynamicValueVisitor<ts.DiagnosticRelatedInformation[]> {
|
||||
private currentContainerNode: ts.Node|null = null;
|
||||
|
||||
constructor(private node: ts.Node) {}
|
||||
|
||||
visitDynamicInput(value: DynamicValue<DynamicValue>): ts.DiagnosticRelatedInformation[] {
|
||||
const trace = value.reason.accept(this);
|
||||
if (this.shouldTrace(value.node)) {
|
||||
const info =
|
||||
makeRelatedInformation(value.node, 'Unable to evaluate this expression statically.');
|
||||
trace.unshift(info);
|
||||
}
|
||||
return trace;
|
||||
}
|
||||
|
||||
visitDynamicString(value: DynamicValue): ts.DiagnosticRelatedInformation[] {
|
||||
return [makeRelatedInformation(
|
||||
value.node, 'A string value could not be determined statically.')];
|
||||
}
|
||||
|
||||
visitExternalReference(value: DynamicValue<Reference<ts.Declaration>>):
|
||||
ts.DiagnosticRelatedInformation[] {
|
||||
const name = value.reason.debugName;
|
||||
const description = name !== null ? `'${name}'` : 'an anonymous declaration';
|
||||
return [makeRelatedInformation(
|
||||
value.node,
|
||||
`A value for ${
|
||||
description} cannot be determined statically, as it is an external declaration.`)];
|
||||
}
|
||||
|
||||
visitInvalidExpressionType(value: DynamicValue): ts.DiagnosticRelatedInformation[] {
|
||||
return [makeRelatedInformation(value.node, 'Unable to evaluate an invalid expression.')];
|
||||
}
|
||||
|
||||
visitUnknown(value: DynamicValue): ts.DiagnosticRelatedInformation[] {
|
||||
return [makeRelatedInformation(value.node, 'Unable to evaluate statically.')];
|
||||
}
|
||||
|
||||
visitUnknownIdentifier(value: DynamicValue): ts.DiagnosticRelatedInformation[] {
|
||||
return [makeRelatedInformation(value.node, 'Unknown reference.')];
|
||||
}
|
||||
|
||||
visitUnsupportedSyntax(value: DynamicValue): ts.DiagnosticRelatedInformation[] {
|
||||
return [makeRelatedInformation(value.node, 'This syntax is not supported.')];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the dynamic value reported for the node should be traced, i.e. if it is not
|
||||
* part of the container for which the most recent trace was created.
|
||||
*/
|
||||
private shouldTrace(node: ts.Node): boolean {
|
||||
if (node === this.node) {
|
||||
// Do not include a dynamic value for the origin node, as the main diagnostic is already
|
||||
// reported on that node.
|
||||
return false;
|
||||
}
|
||||
|
||||
const container = getContainerNode(node);
|
||||
if (container === this.currentContainerNode) {
|
||||
// The node is part of the same container as the previous trace entry, so this dynamic value
|
||||
// should not become part of the trace.
|
||||
return false;
|
||||
}
|
||||
|
||||
this.currentContainerNode = container;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the closest parent node that is to be considered as container, which is used to reduce
|
||||
* the granularity of tracing the dynamic values to a single entry per container. Currently, full
|
||||
* statements and destructuring patterns are considered as container.
|
||||
*/
|
||||
function getContainerNode(node: ts.Node): ts.Node {
|
||||
let currentNode: ts.Node|undefined = node;
|
||||
while (currentNode !== undefined) {
|
||||
switch (currentNode.kind) {
|
||||
case ts.SyntaxKind.ExpressionStatement:
|
||||
case ts.SyntaxKind.VariableStatement:
|
||||
case ts.SyntaxKind.ReturnStatement:
|
||||
case ts.SyntaxKind.IfStatement:
|
||||
case ts.SyntaxKind.SwitchStatement:
|
||||
case ts.SyntaxKind.DoStatement:
|
||||
case ts.SyntaxKind.WhileStatement:
|
||||
case ts.SyntaxKind.ForStatement:
|
||||
case ts.SyntaxKind.ForInStatement:
|
||||
case ts.SyntaxKind.ForOfStatement:
|
||||
case ts.SyntaxKind.ContinueStatement:
|
||||
case ts.SyntaxKind.BreakStatement:
|
||||
case ts.SyntaxKind.ThrowStatement:
|
||||
case ts.SyntaxKind.ObjectBindingPattern:
|
||||
case ts.SyntaxKind.ArrayBindingPattern:
|
||||
return currentNode;
|
||||
}
|
||||
|
||||
currentNode = currentNode.parent;
|
||||
}
|
||||
return node.getSourceFile();
|
||||
}
|
@ -124,4 +124,34 @@ export class DynamicValue<R = unknown> {
|
||||
isFromUnknown(this: DynamicValue<R>): this is DynamicValue {
|
||||
return this.code === DynamicValueReason.UNKNOWN;
|
||||
}
|
||||
|
||||
accept<R>(visitor: DynamicValueVisitor<R>): R {
|
||||
switch (this.code) {
|
||||
case DynamicValueReason.DYNAMIC_INPUT:
|
||||
return visitor.visitDynamicInput(this as unknown as DynamicValue<DynamicValue>);
|
||||
case DynamicValueReason.DYNAMIC_STRING:
|
||||
return visitor.visitDynamicString(this);
|
||||
case DynamicValueReason.EXTERNAL_REFERENCE:
|
||||
return visitor.visitExternalReference(
|
||||
this as unknown as DynamicValue<Reference<ts.Declaration>>);
|
||||
case DynamicValueReason.UNSUPPORTED_SYNTAX:
|
||||
return visitor.visitUnsupportedSyntax(this);
|
||||
case DynamicValueReason.UNKNOWN_IDENTIFIER:
|
||||
return visitor.visitUnknownIdentifier(this);
|
||||
case DynamicValueReason.INVALID_EXPRESSION_TYPE:
|
||||
return visitor.visitInvalidExpressionType(this);
|
||||
case DynamicValueReason.UNKNOWN:
|
||||
return visitor.visitUnknown(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynamicValueVisitor<R> {
|
||||
visitDynamicInput(value: DynamicValue<DynamicValue>): R;
|
||||
visitDynamicString(value: DynamicValue): R;
|
||||
visitExternalReference(value: DynamicValue<Reference<ts.Declaration>>): R;
|
||||
visitUnsupportedSyntax(value: DynamicValue): R;
|
||||
visitUnknownIdentifier(value: DynamicValue): R;
|
||||
visitInvalidExpressionType(value: DynamicValue): R;
|
||||
visitUnknown(value: DynamicValue): R;
|
||||
}
|
||||
|
@ -0,0 +1,286 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {absoluteFrom as _} from '../../file_system';
|
||||
import {runInEachFileSystem} from '../../file_system/testing';
|
||||
import {Reference} from '../../imports';
|
||||
import {TypeScriptReflectionHost} from '../../reflection';
|
||||
import {getDeclaration, makeProgram} from '../../testing';
|
||||
|
||||
import {ObjectAssignBuiltinFn} from '../src/builtin';
|
||||
import {describeResolvedType, traceDynamicValue} from '../src/diagnostics';
|
||||
import {DynamicValue} from '../src/dynamic';
|
||||
import {PartialEvaluator} from '../src/interface';
|
||||
import {EnumValue, ResolvedModule} from '../src/result';
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
describe('partial evaluator', () => {
|
||||
describe('describeResolvedType()', () => {
|
||||
it('should describe primitives', () => {
|
||||
expect(describeResolvedType(0)).toBe('number');
|
||||
expect(describeResolvedType(true)).toBe('boolean');
|
||||
expect(describeResolvedType(false)).toBe('boolean');
|
||||
expect(describeResolvedType(null)).toBe('null');
|
||||
expect(describeResolvedType(undefined)).toBe('undefined');
|
||||
expect(describeResolvedType('text')).toBe('string');
|
||||
});
|
||||
|
||||
it('should describe objects limited to a single level', () => {
|
||||
expect(describeResolvedType(new Map())).toBe('{}');
|
||||
expect(describeResolvedType(new Map<string, any>([['a', 0], ['b', true]])))
|
||||
.toBe('{ a: number; b: boolean }');
|
||||
expect(describeResolvedType(new Map([['a', new Map()]]))).toBe('{ a: object }');
|
||||
expect(describeResolvedType(new Map([['a', [1, 2, 3]]]))).toBe('{ a: Array }');
|
||||
});
|
||||
|
||||
it('should describe arrays limited to a single level', () => {
|
||||
expect(describeResolvedType([])).toBe('[]');
|
||||
expect(describeResolvedType([1, 2, 3])).toBe('[number, number, number]');
|
||||
expect(describeResolvedType([[1, 2], [3, 4]])).toBe('[Array, Array]');
|
||||
expect(describeResolvedType([new Map([['a', 0]])])).toBe('[object]');
|
||||
});
|
||||
|
||||
it('should describe references', () => {
|
||||
const namedFn = ts.createFunctionDeclaration(
|
||||
/* decorators */ undefined,
|
||||
/* modifiers */ undefined,
|
||||
/* asteriskToken */ undefined,
|
||||
/* name */ 'test',
|
||||
/* typeParameters */ undefined,
|
||||
/* parameters */[],
|
||||
/* type */ undefined,
|
||||
/* body */ undefined,
|
||||
);
|
||||
expect(describeResolvedType(new Reference(namedFn))).toBe('test');
|
||||
|
||||
const anonymousFn = ts.createFunctionDeclaration(
|
||||
/* decorators */ undefined,
|
||||
/* modifiers */ undefined,
|
||||
/* asteriskToken */ undefined,
|
||||
/* name */ undefined,
|
||||
/* typeParameters */ undefined,
|
||||
/* parameters */[],
|
||||
/* type */ undefined,
|
||||
/* body */ undefined,
|
||||
);
|
||||
expect(describeResolvedType(new Reference(anonymousFn))).toBe('(anonymous)');
|
||||
});
|
||||
|
||||
it('should describe enum values', () => {
|
||||
const decl = ts.createEnumDeclaration(
|
||||
/* decorators */ undefined,
|
||||
/* modifiers */ undefined,
|
||||
/* name */ 'MyEnum',
|
||||
/* members */[ts.createEnumMember('member', ts.createNumericLiteral('1'))],
|
||||
);
|
||||
const ref = new Reference(decl);
|
||||
expect(describeResolvedType(new EnumValue(ref, 'member', 1))).toBe('MyEnum');
|
||||
});
|
||||
|
||||
it('should describe dynamic values', () => {
|
||||
const node = ts.createObjectLiteral();
|
||||
expect(describeResolvedType(DynamicValue.fromUnsupportedSyntax(node)))
|
||||
.toBe('(not statically analyzable)');
|
||||
});
|
||||
|
||||
it('should describe known functions', () => {
|
||||
expect(describeResolvedType(new ObjectAssignBuiltinFn())).toBe('Function');
|
||||
});
|
||||
|
||||
it('should describe external modules', () => {
|
||||
expect(describeResolvedType(new ResolvedModule(new Map(), () => undefined)))
|
||||
.toBe('(module)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('traceDynamicValue()', () => {
|
||||
it('should not include the origin node if points to a different dynamic node.', () => {
|
||||
// In the below expression, the read of "value" is evaluated to be dynamic, but it's also
|
||||
// the exact node for which the diagnostic is produced. Therefore, this node is not part
|
||||
// of the trace.
|
||||
const trace = traceExpression('const value = nonexistent;', 'value');
|
||||
|
||||
expect(trace.length).toBe(1);
|
||||
expect(trace[0].messageText).toBe(`Unknown reference.`);
|
||||
expect(trace[0].file!.fileName).toBe(_('/entry.ts'));
|
||||
expect(getSourceCode(trace[0])).toBe('nonexistent');
|
||||
});
|
||||
|
||||
it('should include the origin node if it is dynamic by itself', () => {
|
||||
const trace = traceExpression('', 'nonexistent;');
|
||||
|
||||
expect(trace.length).toBe(1);
|
||||
expect(trace[0].messageText).toBe(`Unknown reference.`);
|
||||
expect(trace[0].file!.fileName).toBe(_('/entry.ts'));
|
||||
expect(getSourceCode(trace[0])).toBe('nonexistent');
|
||||
});
|
||||
|
||||
it('should include a trace for a dynamic subexpression in the origin expression', () => {
|
||||
const trace = traceExpression('const value = nonexistent;', 'value.property');
|
||||
|
||||
expect(trace.length).toBe(2);
|
||||
expect(trace[0].messageText).toBe('Unable to evaluate this expression statically.');
|
||||
expect(trace[0].file!.fileName).toBe(_('/entry.ts'));
|
||||
expect(getSourceCode(trace[0])).toBe('value');
|
||||
|
||||
expect(trace[1].messageText).toBe('Unknown reference.');
|
||||
expect(trace[1].file!.fileName).toBe(_('/entry.ts'));
|
||||
expect(getSourceCode(trace[1])).toBe('nonexistent');
|
||||
});
|
||||
|
||||
it('should reduce the granularity to a single entry per statement', () => {
|
||||
// Dynamic values exist for each node that has been visited, but only the initial dynamic
|
||||
// value within a statement is included in the trace.
|
||||
const trace = traceExpression(
|
||||
`const firstChild = document.body.childNodes[0];
|
||||
const child = firstChild.firstChild;`,
|
||||
'child !== undefined');
|
||||
|
||||
expect(trace.length).toBe(4);
|
||||
expect(trace[0].messageText).toBe('Unable to evaluate this expression statically.');
|
||||
expect(trace[0].file!.fileName).toBe(_('/entry.ts'));
|
||||
expect(getSourceCode(trace[0])).toBe('child');
|
||||
|
||||
expect(trace[1].messageText).toBe('Unable to evaluate this expression statically.');
|
||||
expect(trace[1].file!.fileName).toBe(_('/entry.ts'));
|
||||
expect(getSourceCode(trace[1])).toBe('firstChild');
|
||||
|
||||
expect(trace[2].messageText).toBe('Unable to evaluate this expression statically.');
|
||||
expect(trace[2].file!.fileName).toBe(_('/entry.ts'));
|
||||
expect(getSourceCode(trace[2])).toBe('document.body');
|
||||
|
||||
expect(trace[3].messageText)
|
||||
.toBe(
|
||||
`A value for 'document' cannot be determined statically, as it is an external declaration.`);
|
||||
expect(trace[3].file!.fileName).toBe(_('/lib.d.ts'));
|
||||
expect(getSourceCode(trace[3])).toBe('document: any');
|
||||
});
|
||||
|
||||
it('should trace dynamic strings', () => {
|
||||
const trace = traceExpression('', '`${document}`');
|
||||
|
||||
expect(trace.length).toBe(1);
|
||||
expect(trace[0].messageText).toBe('A string value could not be determined statically.');
|
||||
expect(trace[0].file!.fileName).toBe(_('/entry.ts'));
|
||||
expect(getSourceCode(trace[0])).toBe('document');
|
||||
});
|
||||
|
||||
it('should trace invalid expression types', () => {
|
||||
const trace = traceExpression('', 'true()');
|
||||
|
||||
expect(trace.length).toBe(1);
|
||||
expect(trace[0].messageText).toBe('Unable to evaluate an invalid expression.');
|
||||
expect(trace[0].file!.fileName).toBe(_('/entry.ts'));
|
||||
expect(getSourceCode(trace[0])).toBe('true');
|
||||
});
|
||||
|
||||
it('should trace unknown syntax', () => {
|
||||
const trace = traceExpression('', `new String('test')`);
|
||||
|
||||
expect(trace.length).toBe(1);
|
||||
expect(trace[0].messageText).toBe('This syntax is not supported.');
|
||||
expect(trace[0].file!.fileName).toBe(_('/entry.ts'));
|
||||
expect(getSourceCode(trace[0])).toBe('new String(\'test\')');
|
||||
});
|
||||
|
||||
it('should trace complex function invocations', () => {
|
||||
const trace = traceExpression(
|
||||
`
|
||||
function complex() {
|
||||
console.log('test');
|
||||
return true;
|
||||
}`,
|
||||
'complex()');
|
||||
|
||||
expect(trace.length).toBe(1);
|
||||
expect(trace[0].messageText).toBe('Unable to evaluate statically.');
|
||||
expect(trace[0].file!.fileName).toBe(_('/entry.ts'));
|
||||
expect(getSourceCode(trace[0])).toBe('complex()');
|
||||
});
|
||||
|
||||
it('should trace object destructuring of external reference', () => {
|
||||
const trace = traceExpression('const {body: {firstChild}} = document;', 'firstChild');
|
||||
|
||||
expect(trace.length).toBe(2);
|
||||
expect(trace[0].messageText).toBe('Unable to evaluate this expression statically.');
|
||||
expect(trace[0].file!.fileName).toBe(_('/entry.ts'));
|
||||
expect(getSourceCode(trace[0])).toBe('body: {firstChild}');
|
||||
|
||||
expect(trace[1].messageText)
|
||||
.toBe(
|
||||
`A value for 'document' cannot be determined statically, as it is an external declaration.`);
|
||||
expect(trace[1].file!.fileName).toBe(_('/lib.d.ts'));
|
||||
expect(getSourceCode(trace[1])).toBe('document: any');
|
||||
});
|
||||
|
||||
it('should trace deep object destructuring of external reference', () => {
|
||||
const trace =
|
||||
traceExpression('const {doc: {body: {firstChild}}} = {doc: document};', 'firstChild');
|
||||
|
||||
expect(trace.length).toBe(2);
|
||||
expect(trace[0].messageText).toBe('Unable to evaluate this expression statically.');
|
||||
expect(trace[0].file!.fileName).toBe(_('/entry.ts'));
|
||||
expect(getSourceCode(trace[0])).toBe('body: {firstChild}');
|
||||
|
||||
expect(trace[1].messageText)
|
||||
.toBe(
|
||||
`A value for 'document' cannot be determined statically, as it is an external declaration.`);
|
||||
expect(trace[1].file!.fileName).toBe(_('/lib.d.ts'));
|
||||
expect(getSourceCode(trace[1])).toBe('document: any');
|
||||
});
|
||||
|
||||
it('should trace array destructuring of dynamic value', () => {
|
||||
const trace =
|
||||
traceExpression('const [firstChild] = document.body.childNodes;', 'firstChild');
|
||||
|
||||
expect(trace.length).toBe(3);
|
||||
expect(trace[0].messageText).toBe('Unable to evaluate this expression statically.');
|
||||
expect(trace[0].file!.fileName).toBe(_('/entry.ts'));
|
||||
expect(getSourceCode(trace[0])).toBe('firstChild');
|
||||
|
||||
expect(trace[1].messageText).toBe('Unable to evaluate this expression statically.');
|
||||
expect(trace[1].file!.fileName).toBe(_('/entry.ts'));
|
||||
expect(getSourceCode(trace[1])).toBe('document.body');
|
||||
|
||||
expect(trace[2].messageText)
|
||||
.toBe(
|
||||
`A value for 'document' cannot be determined statically, as it is an external declaration.`);
|
||||
expect(trace[2].file!.fileName).toBe(_('/lib.d.ts'));
|
||||
expect(getSourceCode(trace[2])).toBe('document: any');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getSourceCode(diag: ts.DiagnosticRelatedInformation): string {
|
||||
const text = diag.file!.text;
|
||||
return text.substr(diag.start!, diag.length!);
|
||||
}
|
||||
|
||||
function traceExpression(code: string, expr: string): ts.DiagnosticRelatedInformation[] {
|
||||
const {program} = makeProgram(
|
||||
[
|
||||
{name: _('/entry.ts'), contents: `${code}; const target$ = ${expr};`},
|
||||
{name: _('/lib.d.ts'), contents: `declare const document: any;`},
|
||||
],
|
||||
/* options */ undefined, /* host */ undefined, /* checkForErrors */ false);
|
||||
const checker = program.getTypeChecker();
|
||||
const decl = getDeclaration(program, _('/entry.ts'), 'target$', ts.isVariableDeclaration);
|
||||
const valueExpr = decl.initializer!;
|
||||
|
||||
const reflectionHost = new TypeScriptReflectionHost(checker);
|
||||
const evaluator = new PartialEvaluator(reflectionHost, checker, /* dependencyTracker */ null);
|
||||
|
||||
const value = evaluator.evaluate(valueExpr);
|
||||
if (!(value instanceof DynamicValue)) {
|
||||
throw new Error('Expected DynamicValue');
|
||||
}
|
||||
return traceDynamicValue(valueExpr, value);
|
||||
}
|
Reference in New Issue
Block a user