In #34288, ngtsc was refactored to separate the result of the analysis and resolve phase for more granular incremental rebuilds. In this model, any errors in one phase transition the trait into an error state, which prevents it from being ran through subsequent phases. The ngcc compiler on the other hand did not adopt this strict error model, which would cause incomplete metadata—due to errors in earlier phases—to be offered for compilation that could result in a hard crash. This commit updates ngcc to take advantage of ngtsc's `TraitCompiler`, that internally manages all Ivy classes that are part of the compilation. This effectively replaces ngcc's own `AnalyzedFile` and `AnalyzedClass` types, together with all of the logic to drive the `DecoratorHandler`s. All of this is now handled in the `TraitCompiler`, benefiting from its explicit state transitions of `Trait`s so that the ngcc crash is a thing of the past. Fixes #34500 Resolves FW-1788 PR Close #34889
206 lines
8.6 KiB
TypeScript
206 lines
8.6 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google Inc. 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 {makeDiagnostic} from '../../../src/ngtsc/diagnostics';
|
|
import {absoluteFrom} from '../../../src/ngtsc/file_system';
|
|
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
|
import {ClassDeclaration, Decorator, isNamedClassDeclaration} from '../../../src/ngtsc/reflection';
|
|
import {getDeclaration} from '../../../src/ngtsc/testing';
|
|
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, TraitState} from '../../../src/ngtsc/transform';
|
|
import {loadTestFiles} from '../../../test/helpers';
|
|
import {DefaultMigrationHost} from '../../src/analysis/migration_host';
|
|
import {NgccTraitCompiler} from '../../src/analysis/ngcc_trait_compiler';
|
|
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
|
|
import {createComponentDecorator} from '../../src/migrations/utils';
|
|
import {EntryPointBundle} from '../../src/packages/entry_point_bundle';
|
|
import {MockLogger} from '../helpers/mock_logger';
|
|
import {makeTestEntryPointBundle} from '../helpers/utils';
|
|
|
|
runInEachFileSystem(() => {
|
|
describe('DefaultMigrationHost', () => {
|
|
let _: typeof absoluteFrom;
|
|
let mockMetadata: any = {};
|
|
let mockEvaluator: any = {};
|
|
let mockClazz: any;
|
|
let injectedDecorator: any = {name: 'InjectedDecorator'};
|
|
beforeEach(() => {
|
|
_ = absoluteFrom;
|
|
const mockSourceFile: any = {
|
|
fileName: _('/node_modules/some-package/entry-point/test-file.js'),
|
|
};
|
|
mockClazz = {
|
|
name: {text: 'MockClazz'},
|
|
getSourceFile: () => mockSourceFile,
|
|
getStart: () => 0,
|
|
getWidth: () => 0,
|
|
};
|
|
});
|
|
|
|
function createMigrationHost({entryPoint, handlers}: {
|
|
entryPoint: EntryPointBundle; handlers: DecoratorHandler<unknown, unknown, unknown>[]
|
|
}) {
|
|
const reflectionHost = new Esm2015ReflectionHost(new MockLogger(), false, entryPoint.src);
|
|
const compiler = new NgccTraitCompiler(handlers, reflectionHost);
|
|
const host = new DefaultMigrationHost(
|
|
reflectionHost, mockMetadata, mockEvaluator, compiler, entryPoint.entryPoint.path);
|
|
return {compiler, host};
|
|
}
|
|
|
|
describe('injectSyntheticDecorator()', () => {
|
|
it('should add the injected decorator into the compilation', () => {
|
|
const handler = new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.WEAK);
|
|
loadTestFiles([{name: _('/node_modules/test/index.js'), contents: ``}]);
|
|
const entryPoint =
|
|
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
|
|
const {host, compiler} = createMigrationHost({entryPoint, handlers: [handler]});
|
|
host.injectSyntheticDecorator(mockClazz, injectedDecorator);
|
|
|
|
const record = compiler.recordFor(mockClazz) !;
|
|
expect(record).toBeDefined();
|
|
expect(record.traits.length).toBe(1);
|
|
expect(record.traits[0].detected.decorator).toBe(injectedDecorator);
|
|
});
|
|
|
|
it('should mention the migration that failed in the diagnostics message', () => {
|
|
const handler = new DiagnosticProducingHandler();
|
|
loadTestFiles([{name: _('/node_modules/test/index.js'), contents: ``}]);
|
|
const entryPoint =
|
|
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
|
|
const {host, compiler} = createMigrationHost({entryPoint, handlers: [handler]});
|
|
const decorator = createComponentDecorator(mockClazz, {selector: 'comp', exportAs: null});
|
|
host.injectSyntheticDecorator(mockClazz, decorator);
|
|
|
|
const record = compiler.recordFor(mockClazz) !;
|
|
const migratedTrait = record.traits[0];
|
|
if (migratedTrait.state !== TraitState.ERRORED) {
|
|
return fail('Expected migrated class trait to be in an error state');
|
|
}
|
|
|
|
expect(migratedTrait.diagnostics.length).toBe(1);
|
|
expect(ts.flattenDiagnosticMessageText(migratedTrait.diagnostics[0].messageText, '\n'))
|
|
.toEqual(
|
|
`test diagnostic\n` +
|
|
` Occurs for @Component decorator inserted by an automatic migration\n` +
|
|
` @Component({ template: "", selector: "comp" })`);
|
|
});
|
|
});
|
|
|
|
|
|
|
|
describe('getAllDecorators', () => {
|
|
it('should include injected decorators', () => {
|
|
const directiveHandler = new DetectDecoratorHandler('Directive', HandlerPrecedence.WEAK);
|
|
const injectedHandler =
|
|
new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.WEAK);
|
|
loadTestFiles([{
|
|
name: _('/node_modules/test/index.js'),
|
|
contents: `
|
|
import {Directive} from '@angular/core';
|
|
|
|
export class MyClass {};
|
|
MyClass.decorators = [{ type: Directive }];
|
|
`
|
|
}]);
|
|
const entryPoint =
|
|
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
|
|
const {host, compiler} =
|
|
createMigrationHost({entryPoint, handlers: [directiveHandler, injectedHandler]});
|
|
const myClass = getDeclaration(
|
|
entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass',
|
|
isNamedClassDeclaration);
|
|
|
|
compiler.analyzeFile(entryPoint.src.file);
|
|
|
|
host.injectSyntheticDecorator(myClass, injectedDecorator);
|
|
|
|
const decorators = host.getAllDecorators(myClass) !;
|
|
expect(decorators.length).toBe(2);
|
|
expect(decorators[0].name).toBe('Directive');
|
|
expect(decorators[1].name).toBe('InjectedDecorator');
|
|
});
|
|
});
|
|
|
|
describe('isInScope', () => {
|
|
it('should be true for nodes within the entry-point', () => {
|
|
loadTestFiles([
|
|
{name: _('/node_modules/test/index.js'), contents: `export * from './internal';`},
|
|
{name: _('/node_modules/test/internal.js'), contents: `export class InternalClass {}`},
|
|
]);
|
|
const entryPoint =
|
|
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
|
|
const {host} = createMigrationHost({entryPoint, handlers: []});
|
|
const internalClass = getDeclaration(
|
|
entryPoint.src.program, _('/node_modules/test/internal.js'), 'InternalClass',
|
|
isNamedClassDeclaration);
|
|
|
|
expect(host.isInScope(internalClass)).toBe(true);
|
|
});
|
|
|
|
it('should be false for nodes outside the entry-point', () => {
|
|
loadTestFiles([
|
|
{name: _('/node_modules/external/index.js'), contents: `export class ExternalClass {}`},
|
|
{
|
|
name: _('/node_modules/test/index.js'),
|
|
contents: `
|
|
export {ExternalClass} from 'external';
|
|
export class InternalClass {}
|
|
`
|
|
},
|
|
]);
|
|
const entryPoint =
|
|
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
|
|
const {host} = createMigrationHost({entryPoint, handlers: []});
|
|
const externalClass = getDeclaration(
|
|
entryPoint.src.program, _('/node_modules/external/index.js'), 'ExternalClass',
|
|
isNamedClassDeclaration);
|
|
|
|
expect(host.isInScope(externalClass)).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
class DetectDecoratorHandler implements DecoratorHandler<unknown, unknown, unknown> {
|
|
readonly name = DetectDecoratorHandler.name;
|
|
|
|
constructor(private decorator: string, readonly precedence: HandlerPrecedence) {}
|
|
|
|
detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<unknown>|undefined {
|
|
if (decorators === null) {
|
|
return undefined;
|
|
}
|
|
const decorator = decorators.find(decorator => decorator.name === this.decorator);
|
|
if (decorator === undefined) {
|
|
return undefined;
|
|
}
|
|
return {trigger: node, decorator, metadata: {}};
|
|
}
|
|
|
|
analyze(node: ClassDeclaration): AnalysisOutput<unknown> { return {}; }
|
|
|
|
compile(node: ClassDeclaration): CompileResult|CompileResult[] { return []; }
|
|
}
|
|
|
|
class DiagnosticProducingHandler implements DecoratorHandler<unknown, unknown, unknown> {
|
|
readonly name = DiagnosticProducingHandler.name;
|
|
readonly precedence = HandlerPrecedence.PRIMARY;
|
|
|
|
detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<unknown>|undefined {
|
|
const decorator = decorators !== null ? decorators[0] : null;
|
|
return {trigger: node, decorator, metadata: {}};
|
|
}
|
|
|
|
analyze(node: ClassDeclaration): AnalysisOutput<any> {
|
|
return {diagnostics: [makeDiagnostic(9999, node, 'test diagnostic')]};
|
|
}
|
|
|
|
compile(node: ClassDeclaration): CompileResult|CompileResult[] { return []; }
|
|
}
|