fix(ivy): run annotations handlers' `resolve()` in `ngcc` (#28963)
The `resolve` phase (run after all handlers have analyzed) was
introduced in 7d954dffd
, but `ngcc` was not updated to run the handlers'
`resolve()` methods. As a result, certain operations (such as listing
directives used in component templates) would not be performed by
`ngcc`.
This commit fixes it by running the `resolve()` methods once analysis
has been completed.
PR Close #28963
This commit is contained in:
parent
e79f57a6b8
commit
ce4da3f8e5
|
@ -115,6 +115,7 @@ export class DecorationAnalyzer {
|
||||||
const analysedFiles = this.program.getSourceFiles()
|
const analysedFiles = this.program.getSourceFiles()
|
||||||
.map(sourceFile => this.analyzeFile(sourceFile))
|
.map(sourceFile => this.analyzeFile(sourceFile))
|
||||||
.filter(isDefined);
|
.filter(isDefined);
|
||||||
|
analysedFiles.forEach(analysedFile => this.resolveFile(analysedFile));
|
||||||
const compiledFiles = analysedFiles.map(analysedFile => this.compileFile(analysedFile));
|
const compiledFiles = analysedFiles.map(analysedFile => this.compileFile(analysedFile));
|
||||||
compiledFiles.forEach(
|
compiledFiles.forEach(
|
||||||
compiledFile => decorationAnalyses.set(compiledFile.sourceFile, compiledFile));
|
compiledFile => decorationAnalyses.set(compiledFile.sourceFile, compiledFile));
|
||||||
|
@ -202,6 +203,16 @@ export class DecorationAnalyzer {
|
||||||
}
|
}
|
||||||
return compilations;
|
return compilations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected resolveFile(analyzedFile: AnalyzedFile): void {
|
||||||
|
analyzedFile.analyzedClasses.forEach(({declaration, matches}) => {
|
||||||
|
matches.forEach(({handler, analysis}) => {
|
||||||
|
if ((handler.resolve !== undefined) && analysis) {
|
||||||
|
handler.resolve(declaration, analysis);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMatchingHandler<A, M>(handler: Partial<MatchingHandler<A, M>>):
|
function isMatchingHandler<A, M>(handler: Partial<MatchingHandler<A, M>>):
|
||||||
|
|
|
@ -10,7 +10,7 @@ import * as ts from 'typescript';
|
||||||
import {AbsoluteFsPath} from '../../../ngtsc/path';
|
import {AbsoluteFsPath} from '../../../ngtsc/path';
|
||||||
import {Decorator} from '../../../ngtsc/reflection';
|
import {Decorator} from '../../../ngtsc/reflection';
|
||||||
import {DecoratorHandler, DetectResult} from '../../../ngtsc/transform';
|
import {DecoratorHandler, DetectResult} from '../../../ngtsc/transform';
|
||||||
import {DecorationAnalyses, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
|
import {CompiledClass, DecorationAnalyses, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
|
||||||
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
|
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
|
||||||
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
|
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
|
||||||
import {makeTestBundleProgram} from '../helpers/utils';
|
import {makeTestBundleProgram} from '../helpers/utils';
|
||||||
|
@ -70,46 +70,65 @@ const INTERNAL_COMPONENT_PROGRAM = [
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
function createTestHandler() {
|
type DecoratorHandlerWithResolve = DecoratorHandler<any, any>& {
|
||||||
const handler = jasmine.createSpyObj<DecoratorHandler<any, any>>('TestDecoratorHandler', [
|
resolve: NonNullable<DecoratorHandler<any, any>['resolve']>;
|
||||||
'detect',
|
};
|
||||||
'analyze',
|
|
||||||
'compile',
|
|
||||||
]);
|
|
||||||
// Only detect the Component and Directive decorators
|
|
||||||
handler.detect.and.callFake(
|
|
||||||
(node: ts.Declaration, decorators: Decorator[]): DetectResult<any>| undefined => {
|
|
||||||
if (!decorators) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const metadata = decorators.find(d => d.name === 'Component' || d.name === 'Directive');
|
|
||||||
if (metadata === undefined) {
|
|
||||||
return undefined;
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
metadata,
|
|
||||||
trigger: metadata.node,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// The "test" analysis is an object with the name of the decorator being analyzed
|
|
||||||
handler.analyze.and.callFake((decl: ts.Declaration, dec: Decorator) => {
|
|
||||||
expect(handler.compile).not.toHaveBeenCalled();
|
|
||||||
return {analysis: {decoratorName: dec.name}, diagnostics: undefined};
|
|
||||||
});
|
|
||||||
// The "test" compilation result is just the name of the decorator being compiled
|
|
||||||
handler.compile.and.callFake((decl: ts.Declaration, analysis: any) => ({analysis}));
|
|
||||||
return handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('DecorationAnalyzer', () => {
|
describe('DecorationAnalyzer', () => {
|
||||||
describe('analyzeProgram()', () => {
|
describe('analyzeProgram()', () => {
|
||||||
|
let logs: string[];
|
||||||
let program: ts.Program;
|
let program: ts.Program;
|
||||||
let testHandler: jasmine.SpyObj<DecoratorHandler<any, any>>;
|
let testHandler: jasmine.SpyObj<DecoratorHandlerWithResolve>;
|
||||||
let result: DecorationAnalyses;
|
let result: DecorationAnalyses;
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
|
const createTestHandler = () => {
|
||||||
|
const handler = jasmine.createSpyObj<DecoratorHandlerWithResolve>('TestDecoratorHandler', [
|
||||||
|
'detect',
|
||||||
|
'analyze',
|
||||||
|
'resolve',
|
||||||
|
'compile',
|
||||||
|
]);
|
||||||
|
// Only detect the Component and Directive decorators
|
||||||
|
handler.detect.and.callFake(
|
||||||
|
(node: ts.Declaration, decorators: Decorator[]): DetectResult<any>| undefined => {
|
||||||
|
logs.push(`detect: ${(node as any).name.text}@${decorators.map(d => d.name)}`);
|
||||||
|
if (!decorators) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const metadata = decorators.find(d => d.name === 'Component' || d.name === 'Directive');
|
||||||
|
if (metadata === undefined) {
|
||||||
|
return undefined;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
metadata,
|
||||||
|
trigger: metadata.node,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// The "test" analysis is an object with the name of the decorator being analyzed
|
||||||
|
handler.analyze.and.callFake((decl: ts.Declaration, dec: Decorator) => {
|
||||||
|
logs.push(`analyze: ${(decl as any).name.text}@${dec.name}`);
|
||||||
|
return {analysis: {decoratorName: dec.name}, diagnostics: undefined};
|
||||||
|
});
|
||||||
|
// The "test" resolution is just setting `resolved: true` on the analysis
|
||||||
|
handler.resolve.and.callFake((decl: ts.Declaration, analysis: any) => {
|
||||||
|
logs.push(`resolve: ${(decl as any).name.text}@${analysis.decoratorName}`);
|
||||||
|
analysis.resolved = true;
|
||||||
|
});
|
||||||
|
// The "test" compilation result is just the name of the decorator being compiled
|
||||||
|
// (suffixed with `(compiled)`)
|
||||||
|
handler.compile.and.callFake((decl: ts.Declaration, analysis: any) => {
|
||||||
|
logs.push(
|
||||||
|
`compile: ${(decl as any).name.text}@${analysis.decoratorName} (resolved: ${analysis.resolved})`);
|
||||||
|
return `@${analysis.decoratorName} (compiled)`;
|
||||||
|
});
|
||||||
|
return handler;
|
||||||
|
};
|
||||||
|
|
||||||
const setUpAndAnalyzeProgram = (...progArgs: Parameters<typeof makeTestBundleProgram>) => {
|
const setUpAndAnalyzeProgram = (...progArgs: Parameters<typeof makeTestBundleProgram>) => {
|
||||||
|
logs = [];
|
||||||
|
|
||||||
const {options, host, ...bundle} = makeTestBundleProgram(...progArgs);
|
const {options, host, ...bundle} = makeTestBundleProgram(...progArgs);
|
||||||
program = bundle.program;
|
program = bundle.program;
|
||||||
|
|
||||||
|
@ -148,28 +167,39 @@ describe('DecorationAnalyzer', () => {
|
||||||
const file1 = program.getSourceFile(TEST_PROGRAM[0].name) !;
|
const file1 = program.getSourceFile(TEST_PROGRAM[0].name) !;
|
||||||
const compiledFile1 = result.get(file1) !;
|
const compiledFile1 = result.get(file1) !;
|
||||||
expect(compiledFile1.compiledClasses.length).toEqual(2);
|
expect(compiledFile1.compiledClasses.length).toEqual(2);
|
||||||
expect(compiledFile1.compiledClasses[0].name).toEqual('MyComponent');
|
expect(compiledFile1.compiledClasses[0]).toEqual(jasmine.objectContaining({
|
||||||
expect(compiledFile1.compiledClasses[1].name).toEqual('MyDirective');
|
name: 'MyComponent', compilation: ['@Component (compiled)'],
|
||||||
|
} as unknown as CompiledClass));
|
||||||
|
expect(compiledFile1.compiledClasses[1]).toEqual(jasmine.objectContaining({
|
||||||
|
name: 'MyDirective', compilation: ['@Directive (compiled)'],
|
||||||
|
} as unknown as CompiledClass));
|
||||||
|
|
||||||
const file2 = program.getSourceFile(TEST_PROGRAM[1].name) !;
|
const file2 = program.getSourceFile(TEST_PROGRAM[1].name) !;
|
||||||
const compiledFile2 = result.get(file2) !;
|
const compiledFile2 = result.get(file2) !;
|
||||||
expect(compiledFile2.compiledClasses.length).toEqual(1);
|
expect(compiledFile2.compiledClasses.length).toEqual(1);
|
||||||
expect(compiledFile2.compiledClasses[0].name).toEqual('MyOtherComponent');
|
expect(compiledFile2.compiledClasses[0]).toEqual(jasmine.objectContaining({
|
||||||
|
name: 'MyOtherComponent', compilation: ['@Component (compiled)'],
|
||||||
|
} as unknown as CompiledClass));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should analyze and compile the classes that are detected', () => {
|
it('should analyze, resolve and compile the classes that are detected', () => {
|
||||||
expect(testHandler.analyze).toHaveBeenCalledTimes(3);
|
expect(logs).toEqual([
|
||||||
expect(testHandler.analyze.calls.allArgs().map(args => args[1])).toEqual([
|
// First detect and (potentially) analyze.
|
||||||
jasmine.objectContaining({name: 'Component'}),
|
'detect: MyComponent@Component',
|
||||||
jasmine.objectContaining({name: 'Directive'}),
|
'analyze: MyComponent@Component',
|
||||||
jasmine.objectContaining({name: 'Component'}),
|
'detect: MyDirective@Directive',
|
||||||
]);
|
'analyze: MyDirective@Directive',
|
||||||
|
'detect: MyService@Injectable',
|
||||||
expect(testHandler.compile).toHaveBeenCalledTimes(3);
|
'detect: MyOtherComponent@Component',
|
||||||
expect(testHandler.compile.calls.allArgs().map(args => args[1])).toEqual([
|
'analyze: MyOtherComponent@Component',
|
||||||
{decoratorName: 'Component'},
|
// The resolve.
|
||||||
{decoratorName: 'Directive'},
|
'resolve: MyComponent@Component',
|
||||||
{decoratorName: 'Component'},
|
'resolve: MyDirective@Directive',
|
||||||
|
'resolve: MyOtherComponent@Component',
|
||||||
|
// Finally compile.
|
||||||
|
'compile: MyComponent@Component (resolved: true)',
|
||||||
|
'compile: MyDirective@Directive (resolved: true)',
|
||||||
|
'compile: MyOtherComponent@Component (resolved: true)',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue