refactor(compiler-cli): track visited source files in PartialEvaluator (#29539)
PR Close #29539
This commit is contained in:
parent
717aa7c6e4
commit
06859f1335
|
@ -18,11 +18,18 @@ export type ForeignFunctionResolver =
|
||||||
(node: Reference<ts.FunctionDeclaration|ts.MethodDeclaration|ts.FunctionExpression>,
|
(node: Reference<ts.FunctionDeclaration|ts.MethodDeclaration|ts.FunctionExpression>,
|
||||||
args: ReadonlyArray<ts.Expression>) => ts.Expression | null;
|
args: ReadonlyArray<ts.Expression>) => ts.Expression | null;
|
||||||
|
|
||||||
|
export type VisitedFilesCallback = (sf: ts.SourceFile) => void;
|
||||||
|
|
||||||
export class PartialEvaluator {
|
export class PartialEvaluator {
|
||||||
constructor(private host: ReflectionHost, private checker: ts.TypeChecker) {}
|
constructor(private host: ReflectionHost, private checker: ts.TypeChecker) {}
|
||||||
|
|
||||||
evaluate(expr: ts.Expression, foreignFunctionResolver?: ForeignFunctionResolver): ResolvedValue {
|
evaluate(
|
||||||
const interpreter = new StaticInterpreter(this.host, this.checker);
|
expr: ts.Expression, foreignFunctionResolver?: ForeignFunctionResolver,
|
||||||
|
visitedFilesCb?: VisitedFilesCallback): ResolvedValue {
|
||||||
|
const interpreter = new StaticInterpreter(this.host, this.checker, visitedFilesCb);
|
||||||
|
if (visitedFilesCb) {
|
||||||
|
visitedFilesCb(expr.getSourceFile());
|
||||||
|
}
|
||||||
return interpreter.visit(expr, {
|
return interpreter.visit(expr, {
|
||||||
absoluteModuleName: null,
|
absoluteModuleName: null,
|
||||||
resolutionContext: expr.getSourceFile().fileName,
|
resolutionContext: expr.getSourceFile().fileName,
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {Declaration, ReflectionHost} from '../../reflection';
|
||||||
|
|
||||||
import {ArraySliceBuiltinFn} from './builtin';
|
import {ArraySliceBuiltinFn} from './builtin';
|
||||||
import {DynamicValue} from './dynamic';
|
import {DynamicValue} from './dynamic';
|
||||||
import {ForeignFunctionResolver} from './interface';
|
import {ForeignFunctionResolver, VisitedFilesCallback} from './interface';
|
||||||
import {BuiltinFn, EnumValue, ResolvedValue, ResolvedValueArray, ResolvedValueMap} from './result';
|
import {BuiltinFn, EnumValue, ResolvedValue, ResolvedValueArray, ResolvedValueMap} from './result';
|
||||||
|
|
||||||
|
|
||||||
|
@ -79,7 +79,9 @@ interface Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StaticInterpreter {
|
export class StaticInterpreter {
|
||||||
constructor(private host: ReflectionHost, private checker: ts.TypeChecker) {}
|
constructor(
|
||||||
|
private host: ReflectionHost, private checker: ts.TypeChecker,
|
||||||
|
private visitedFilesCb?: VisitedFilesCallback) {}
|
||||||
|
|
||||||
visit(node: ts.Expression, context: Context): ResolvedValue {
|
visit(node: ts.Expression, context: Context): ResolvedValue {
|
||||||
return this.visitExpression(node, context);
|
return this.visitExpression(node, context);
|
||||||
|
@ -231,6 +233,9 @@ export class StaticInterpreter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private visitDeclaration(node: ts.Declaration, context: Context): ResolvedValue {
|
private visitDeclaration(node: ts.Declaration, context: Context): ResolvedValue {
|
||||||
|
if (this.visitedFilesCb) {
|
||||||
|
this.visitedFilesCb(node.getSourceFile());
|
||||||
|
}
|
||||||
if (this.host.isClass(node)) {
|
if (this.host.isClass(node)) {
|
||||||
return this.getReference(node, context);
|
return this.getReference(node, context);
|
||||||
} else if (ts.isVariableDeclaration(node)) {
|
} else if (ts.isVariableDeclaration(node)) {
|
||||||
|
|
|
@ -15,10 +15,6 @@ import {DynamicValue} from '../src/dynamic';
|
||||||
import {ForeignFunctionResolver, PartialEvaluator} from '../src/interface';
|
import {ForeignFunctionResolver, PartialEvaluator} from '../src/interface';
|
||||||
import {EnumValue, ResolvedValue} from '../src/result';
|
import {EnumValue, ResolvedValue} from '../src/result';
|
||||||
|
|
||||||
function makeSimpleProgram(contents: string): ts.Program {
|
|
||||||
return makeProgram([{name: 'entry.ts', contents}]).program;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeExpression(
|
function makeExpression(
|
||||||
code: string, expr: string, supportingFiles: {name: string, contents: string}[] = []): {
|
code: string, expr: string, supportingFiles: {name: string, contents: string}[] = []): {
|
||||||
expression: ts.Expression,
|
expression: ts.Expression,
|
||||||
|
@ -40,12 +36,16 @@ function makeExpression(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeEvaluator(checker: ts.TypeChecker): PartialEvaluator {
|
||||||
|
const reflectionHost = new TypeScriptReflectionHost(checker);
|
||||||
|
return new PartialEvaluator(reflectionHost, checker);
|
||||||
|
}
|
||||||
|
|
||||||
function evaluate<T extends ResolvedValue>(
|
function evaluate<T extends ResolvedValue>(
|
||||||
code: string, expr: string, supportingFiles: {name: string, contents: string}[] = [],
|
code: string, expr: string, supportingFiles: {name: string, contents: string}[] = [],
|
||||||
foreignFunctionResolver?: ForeignFunctionResolver): T {
|
foreignFunctionResolver?: ForeignFunctionResolver): T {
|
||||||
const {expression, checker, program, options, host} = makeExpression(code, expr, supportingFiles);
|
const {expression, checker} = makeExpression(code, expr, supportingFiles);
|
||||||
const reflectionHost = new TypeScriptReflectionHost(checker);
|
const evaluator = makeEvaluator(checker);
|
||||||
const evaluator = new PartialEvaluator(reflectionHost, checker);
|
|
||||||
return evaluator.evaluate(expression, foreignFunctionResolver) as T;
|
return evaluator.evaluate(expression, foreignFunctionResolver) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -334,6 +334,56 @@ describe('ngtsc metadata', () => {
|
||||||
}
|
}
|
||||||
expect(id.text).toEqual('Target');
|
expect(id.text).toEqual('Target');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('(visited file tracking)', () => {
|
||||||
|
it('should track each time a source file is visited', () => {
|
||||||
|
const visitedFilesSpy = jasmine.createSpy('visitedFilesCb');
|
||||||
|
const {expression, checker} =
|
||||||
|
makeExpression(`class A { static foo = 42; } function bar() { return A.foo; }`, 'bar()');
|
||||||
|
const evaluator = makeEvaluator(checker);
|
||||||
|
evaluator.evaluate(expression, undefined, visitedFilesSpy);
|
||||||
|
expect(visitedFilesSpy)
|
||||||
|
.toHaveBeenCalledTimes(3); // The initial expression, followed by two declaration visited
|
||||||
|
expect(visitedFilesSpy.calls.allArgs().map(args => args[0].fileName)).toEqual([
|
||||||
|
'/entry.ts', '/entry.ts', '/entry.ts'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should track imported source files', () => {
|
||||||
|
const visitedFilesSpy = jasmine.createSpy('visitedFilesCb');
|
||||||
|
const {expression, checker} = makeExpression(`import {Y} from './other'; const A = Y;`, 'A', [
|
||||||
|
{name: 'other.ts', contents: `export const Y = 'test';`},
|
||||||
|
{name: 'not-visited.ts', contents: `export const Z = 'nope';`}
|
||||||
|
]);
|
||||||
|
const evaluator = makeEvaluator(checker);
|
||||||
|
evaluator.evaluate(expression, undefined, visitedFilesSpy);
|
||||||
|
expect(visitedFilesSpy).toHaveBeenCalledTimes(3);
|
||||||
|
expect(visitedFilesSpy.calls.allArgs().map(args => args[0].fileName)).toEqual([
|
||||||
|
'/entry.ts', '/entry.ts', '/other.ts'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should track files passed through during re-exports', () => {
|
||||||
|
const visitedFilesSpy = jasmine.createSpy('visitedFilesCb');
|
||||||
|
const {expression, checker} =
|
||||||
|
makeExpression(`import * as mod from './direct-reexport';`, 'mod.value.property', [
|
||||||
|
{name: 'const.ts', contents: 'export const value = {property: "test"};'},
|
||||||
|
{name: 'def.ts', contents: `import {value} from './const'; export default value;`},
|
||||||
|
{name: 'indirect-reexport.ts', contents: `import value from './def'; export {value};`},
|
||||||
|
{name: 'direct-reexport.ts', contents: `export {value} from './indirect-reexport';`},
|
||||||
|
]);
|
||||||
|
const evaluator = makeEvaluator(checker);
|
||||||
|
evaluator.evaluate(expression, undefined, visitedFilesSpy);
|
||||||
|
expect(visitedFilesSpy).toHaveBeenCalledTimes(3);
|
||||||
|
expect(visitedFilesSpy.calls.allArgs().map(args => args[0].fileName)).toEqual([
|
||||||
|
'/entry.ts',
|
||||||
|
'/direct-reexport.ts',
|
||||||
|
// Not '/indirect-reexport.ts' or '/def.ts'.
|
||||||
|
// TS skips through them when finding the original symbol for `value`
|
||||||
|
'/const.ts',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function owningModuleOf(ref: Reference): string|null {
|
function owningModuleOf(ref: Reference): string|null {
|
||||||
|
|
Loading…
Reference in New Issue