diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts index 9a6ed425a1a6c..de364a789c9e0 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts @@ -253,8 +253,8 @@ export function unwrapForwardRef(node: ts.Expression, reflector: ReflectionHost) * @returns an unwrapped argument if `ref` pointed to forwardRef, or null otherwise */ export function forwardRefResolver( - ref: Reference, - args: ts.Expression[]): ts.Expression|null { + ref: Reference, + args: ReadonlyArray): ts.Expression|null { if (!isAngularCoreReference(ref, 'forwardRef') || args.length !== 1) { return null; } @@ -266,8 +266,8 @@ export function forwardRefResolver( * @param resolvers Resolvers to be combined. */ export function combineResolvers(resolvers: ForeignFunctionResolver[]): ForeignFunctionResolver { - return (ref: Reference, - args: ts.Expression[]): ts.Expression | + return (ref: Reference, + args: ReadonlyArray): ts.Expression | null => { for (const resolver of resolvers) { const resolved = resolver(ref, args); diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interface.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interface.ts index 28617d1ca1572..8f2ea707429ce 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interface.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interface.ts @@ -18,11 +18,18 @@ export type ForeignFunctionResolver = (node: Reference, args: ReadonlyArray) => ts.Expression | null; +export type VisitedFilesCallback = (sf: ts.SourceFile) => void; + export class PartialEvaluator { constructor(private host: ReflectionHost, private checker: ts.TypeChecker) {} - evaluate(expr: ts.Expression, foreignFunctionResolver?: ForeignFunctionResolver): ResolvedValue { - const interpreter = new StaticInterpreter(this.host, this.checker); + evaluate( + 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, { absoluteModuleName: null, resolutionContext: expr.getSourceFile().fileName, 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 138de061ba869..5b4cb6afc9691 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts @@ -14,7 +14,7 @@ import {Declaration, ReflectionHost} from '../../reflection'; import {ArraySliceBuiltinFn} from './builtin'; import {DynamicValue} from './dynamic'; -import {ForeignFunctionResolver} from './interface'; +import {ForeignFunctionResolver, VisitedFilesCallback} from './interface'; import {BuiltinFn, EnumValue, ResolvedValue, ResolvedValueArray, ResolvedValueMap} from './result'; @@ -79,7 +79,9 @@ interface Context { } 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 { return this.visitExpression(node, context); @@ -231,6 +233,9 @@ export class StaticInterpreter { } private visitDeclaration(node: ts.Declaration, context: Context): ResolvedValue { + if (this.visitedFilesCb) { + this.visitedFilesCb(node.getSourceFile()); + } if (this.host.isClass(node)) { return this.getReference(node, context); } else if (ts.isVariableDeclaration(node)) { 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 65c270dc9bf38..da3b76b563d2e 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 @@ -15,10 +15,6 @@ import {DynamicValue} from '../src/dynamic'; import {ForeignFunctionResolver, PartialEvaluator} from '../src/interface'; import {EnumValue, ResolvedValue} from '../src/result'; -function makeSimpleProgram(contents: string): ts.Program { - return makeProgram([{name: 'entry.ts', contents}]).program; -} - function makeExpression( code: string, expr: string, supportingFiles: {name: string, contents: string}[] = []): { 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( code: string, expr: string, supportingFiles: {name: string, contents: string}[] = [], foreignFunctionResolver?: ForeignFunctionResolver): T { - const {expression, checker, program, options, host} = makeExpression(code, expr, supportingFiles); - const reflectionHost = new TypeScriptReflectionHost(checker); - const evaluator = new PartialEvaluator(reflectionHost, checker); + const {expression, checker} = makeExpression(code, expr, supportingFiles); + const evaluator = makeEvaluator(checker); return evaluator.evaluate(expression, foreignFunctionResolver) as T; } @@ -334,6 +334,56 @@ describe('ngtsc metadata', () => { } 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 {