fix(ngcc): do not attempt compilation when analysis fails (#34889)
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
This commit is contained in:
@ -44,6 +44,7 @@ runInEachFileSystem(() => {
|
||||
const handler = jasmine.createSpyObj<DecoratorHandlerWithResolve>('TestDecoratorHandler', [
|
||||
'detect',
|
||||
'analyze',
|
||||
'register',
|
||||
'resolve',
|
||||
'compile',
|
||||
]);
|
||||
@ -67,6 +68,7 @@ runInEachFileSystem(() => {
|
||||
} else {
|
||||
return {
|
||||
metadata,
|
||||
decorator: metadata,
|
||||
trigger: metadata.node,
|
||||
};
|
||||
}
|
||||
@ -117,7 +119,9 @@ runInEachFileSystem(() => {
|
||||
getFileSystem(), bundle, reflectionHost, referencesRegistry,
|
||||
(error) => diagnosticLogs.push(error));
|
||||
testHandler = createTestHandler(options);
|
||||
analyzer.handlers = [testHandler];
|
||||
|
||||
// Replace the default handlers with the test handler in the original array of handlers
|
||||
analyzer.handlers.splice(0, analyzer.handlers.length, testHandler);
|
||||
migrationLogs = [];
|
||||
const migration1 = new MockMigration('migration1', migrationLogs);
|
||||
const migration2 = new MockMigration('migration2', migrationLogs);
|
||||
@ -372,25 +376,49 @@ runInEachFileSystem(() => {
|
||||
expect(diagnosticLogs[1]).toEqual(jasmine.objectContaining({code: -996666}));
|
||||
});
|
||||
|
||||
it('should report analyze and resolve diagnostics to the `diagnosticHandler` callback',
|
||||
() => {
|
||||
const analyzer = setUpAnalyzer(
|
||||
[
|
||||
{
|
||||
name: _('/node_modules/test-package/index.js'),
|
||||
contents: `
|
||||
it('should report analyze diagnostics to the `diagnosticHandler` callback', () => {
|
||||
const analyzer = setUpAnalyzer(
|
||||
[
|
||||
{
|
||||
name: _('/node_modules/test-package/index.js'),
|
||||
contents: `
|
||||
import {Component, Directive, Injectable} from '@angular/core';
|
||||
export class MyComponent {}
|
||||
MyComponent.decorators = [{type: Component}];
|
||||
`,
|
||||
},
|
||||
],
|
||||
{analyzeError: true, resolveError: true});
|
||||
analyzer.analyzeProgram();
|
||||
expect(diagnosticLogs.length).toEqual(2);
|
||||
expect(diagnosticLogs[0]).toEqual(jasmine.objectContaining({code: -999999}));
|
||||
expect(diagnosticLogs[1]).toEqual(jasmine.objectContaining({code: -999998}));
|
||||
});
|
||||
},
|
||||
],
|
||||
{analyzeError: true, resolveError: true});
|
||||
analyzer.analyzeProgram();
|
||||
expect(diagnosticLogs.length).toEqual(1);
|
||||
expect(diagnosticLogs[0]).toEqual(jasmine.objectContaining({code: -999999}));
|
||||
expect(testHandler.analyze).toHaveBeenCalled();
|
||||
expect(testHandler.register).not.toHaveBeenCalled();
|
||||
expect(testHandler.resolve).not.toHaveBeenCalled();
|
||||
expect(testHandler.compile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should report resolve diagnostics to the `diagnosticHandler` callback', () => {
|
||||
const analyzer = setUpAnalyzer(
|
||||
[
|
||||
{
|
||||
name: _('/node_modules/test-package/index.js'),
|
||||
contents: `
|
||||
import {Component, Directive, Injectable} from '@angular/core';
|
||||
export class MyComponent {}
|
||||
MyComponent.decorators = [{type: Component}];
|
||||
`,
|
||||
},
|
||||
],
|
||||
{analyzeError: false, resolveError: true});
|
||||
analyzer.analyzeProgram();
|
||||
expect(diagnosticLogs.length).toEqual(1);
|
||||
expect(diagnosticLogs[0]).toEqual(jasmine.objectContaining({code: -999998}));
|
||||
expect(testHandler.analyze).toHaveBeenCalled();
|
||||
expect(testHandler.register).toHaveBeenCalled();
|
||||
expect(testHandler.resolve).toHaveBeenCalled();
|
||||
expect(testHandler.compile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('declaration files', () => {
|
||||
@ -410,7 +438,9 @@ runInEachFileSystem(() => {
|
||||
name: _('/node_modules/test-package/index.d.ts'),
|
||||
contents: 'export declare class SomeDirective {}',
|
||||
}]);
|
||||
analyzer.handlers = [new FakeDecoratorHandler()];
|
||||
|
||||
// Replace the default handlers with the test handler in the original array of handlers
|
||||
analyzer.handlers.splice(0, analyzer.handlers.length, new FakeDecoratorHandler());
|
||||
result = analyzer.analyzeProgram();
|
||||
expect(result.size).toBe(0);
|
||||
});
|
||||
|
@ -5,200 +5,86 @@
|
||||
* 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 {ErrorCode, makeDiagnostic} from '../../../src/ngtsc/diagnostics';
|
||||
import {AbsoluteFsPath, absoluteFrom} from '../../../src/ngtsc/file_system';
|
||||
import {makeDiagnostic} from '../../../src/ngtsc/diagnostics';
|
||||
import {absoluteFrom} from '../../../src/ngtsc/file_system';
|
||||
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
||||
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';
|
||||
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../src/ngtsc/transform';
|
||||
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 {AnalyzedClass, AnalyzedFile} from '../../src/analysis/types';
|
||||
import {NgccClassSymbol} from '../../src/host/ngcc_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 entryPointPath: AbsoluteFsPath;
|
||||
let mockHost: any;
|
||||
let mockMetadata: any = {};
|
||||
let mockEvaluator: any = {};
|
||||
let mockClazz: any;
|
||||
let mockDecorator: any = {name: 'MockDecorator'};
|
||||
let diagnosticHandler = () => {};
|
||||
let injectedDecorator: any = {name: 'InjectedDecorator'};
|
||||
beforeEach(() => {
|
||||
_ = absoluteFrom;
|
||||
entryPointPath = _('/node_modules/some-package/entry-point');
|
||||
mockHost = {
|
||||
getClassSymbol: (node: any): NgccClassSymbol | undefined => {
|
||||
const symbol = { valueDeclaration: node, name: node.name.text } as any;
|
||||
return {
|
||||
name: node.name.text,
|
||||
declaration: symbol,
|
||||
implementation: symbol,
|
||||
};
|
||||
},
|
||||
};
|
||||
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 call `detect()` on each of the provided handlers', () => {
|
||||
const log: string[] = [];
|
||||
const handler1 = new TestHandler('handler1', log);
|
||||
const handler2 = new TestHandler('handler2', log);
|
||||
const host = new DefaultMigrationHost(
|
||||
mockHost, mockMetadata, mockEvaluator, [handler1, handler2], entryPointPath, [],
|
||||
diagnosticHandler);
|
||||
host.injectSyntheticDecorator(mockClazz, mockDecorator);
|
||||
expect(log).toEqual([
|
||||
`handler1:detect:MockClazz:MockDecorator`,
|
||||
`handler2:detect:MockClazz:MockDecorator`,
|
||||
]);
|
||||
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 call `analyze()` on each of the provided handlers whose `detect()` call returns a result',
|
||||
() => {
|
||||
const log: string[] = [];
|
||||
const handler1 = new TestHandler('handler1', log);
|
||||
const handler2 = new AlwaysDetectHandler('handler2', log);
|
||||
const handler3 = new TestHandler('handler3', log);
|
||||
const host = new DefaultMigrationHost(
|
||||
mockHost, mockMetadata, mockEvaluator, [handler1, handler2, handler3],
|
||||
entryPointPath, [], diagnosticHandler);
|
||||
host.injectSyntheticDecorator(mockClazz, mockDecorator);
|
||||
expect(log).toEqual([
|
||||
`handler1:detect:MockClazz:MockDecorator`,
|
||||
`handler2:detect:MockClazz:MockDecorator`,
|
||||
`handler3:detect:MockClazz:MockDecorator`,
|
||||
'handler2:analyze:MockClazz',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should add a newly `AnalyzedFile` to the `analyzedFiles` object', () => {
|
||||
const log: string[] = [];
|
||||
const handler = new AlwaysDetectHandler('handler', log);
|
||||
const analyzedFiles: AnalyzedFile[] = [];
|
||||
const host = new DefaultMigrationHost(
|
||||
mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles,
|
||||
diagnosticHandler);
|
||||
host.injectSyntheticDecorator(mockClazz, mockDecorator);
|
||||
expect(analyzedFiles.length).toEqual(1);
|
||||
expect(analyzedFiles[0].analyzedClasses.length).toEqual(1);
|
||||
expect(analyzedFiles[0].analyzedClasses[0].name).toEqual('MockClazz');
|
||||
});
|
||||
|
||||
it('should add a newly `AnalyzedClass` to an existing `AnalyzedFile` object', () => {
|
||||
const DUMMY_CLASS_1: any = {};
|
||||
const DUMMY_CLASS_2: any = {};
|
||||
const log: string[] = [];
|
||||
const handler = new AlwaysDetectHandler('handler', log);
|
||||
const analyzedFiles: AnalyzedFile[] = [{
|
||||
sourceFile: mockClazz.getSourceFile(),
|
||||
analyzedClasses: [DUMMY_CLASS_1, DUMMY_CLASS_2],
|
||||
}];
|
||||
const host = new DefaultMigrationHost(
|
||||
mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles,
|
||||
diagnosticHandler);
|
||||
host.injectSyntheticDecorator(mockClazz, mockDecorator);
|
||||
expect(analyzedFiles.length).toEqual(1);
|
||||
expect(analyzedFiles[0].analyzedClasses.length).toEqual(3);
|
||||
expect(analyzedFiles[0].analyzedClasses[2].name).toEqual('MockClazz');
|
||||
});
|
||||
|
||||
it('should add a new decorator into an already existing `AnalyzedClass`', () => {
|
||||
const analyzedClass: AnalyzedClass = {
|
||||
name: 'MockClazz',
|
||||
declaration: mockClazz,
|
||||
matches: [],
|
||||
decorators: null,
|
||||
};
|
||||
const log: string[] = [];
|
||||
const handler = new AlwaysDetectHandler('handler', log);
|
||||
const analyzedFiles: AnalyzedFile[] = [{
|
||||
sourceFile: mockClazz.getSourceFile(),
|
||||
analyzedClasses: [analyzedClass],
|
||||
}];
|
||||
const host = new DefaultMigrationHost(
|
||||
mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles,
|
||||
diagnosticHandler);
|
||||
host.injectSyntheticDecorator(mockClazz, mockDecorator);
|
||||
expect(analyzedFiles.length).toEqual(1);
|
||||
expect(analyzedFiles[0].analyzedClasses.length).toEqual(1);
|
||||
expect(analyzedFiles[0].analyzedClasses[0]).toBe(analyzedClass);
|
||||
expect(analyzedClass.decorators !.length).toEqual(1);
|
||||
expect(analyzedClass.decorators ![0].name).toEqual('MockDecorator');
|
||||
});
|
||||
|
||||
it('should merge a new decorator into pre-existing decorators an already existing `AnalyzedClass`',
|
||||
() => {
|
||||
const analyzedClass: AnalyzedClass = {
|
||||
name: 'MockClazz',
|
||||
declaration: mockClazz,
|
||||
matches: [],
|
||||
decorators: [{name: 'OtherDecorator'} as Decorator],
|
||||
};
|
||||
const log: string[] = [];
|
||||
const handler = new AlwaysDetectHandler('handler', log);
|
||||
const analyzedFiles: AnalyzedFile[] = [{
|
||||
sourceFile: mockClazz.getSourceFile(),
|
||||
analyzedClasses: [analyzedClass],
|
||||
}];
|
||||
const host = new DefaultMigrationHost(
|
||||
mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles,
|
||||
diagnosticHandler);
|
||||
host.injectSyntheticDecorator(mockClazz, mockDecorator);
|
||||
expect(analyzedFiles.length).toEqual(1);
|
||||
expect(analyzedFiles[0].analyzedClasses.length).toEqual(1);
|
||||
expect(analyzedFiles[0].analyzedClasses[0]).toBe(analyzedClass);
|
||||
expect(analyzedClass.decorators !.length).toEqual(2);
|
||||
expect(analyzedClass.decorators ![1].name).toEqual('MockDecorator');
|
||||
});
|
||||
|
||||
it('should throw an error if the injected decorator already exists', () => {
|
||||
const analyzedClass: AnalyzedClass = {
|
||||
name: 'MockClazz',
|
||||
declaration: mockClazz,
|
||||
matches: [],
|
||||
decorators: [{name: 'MockDecorator'} as Decorator],
|
||||
};
|
||||
const log: string[] = [];
|
||||
const handler = new AlwaysDetectHandler('handler', log);
|
||||
const analyzedFiles: AnalyzedFile[] = [{
|
||||
sourceFile: mockClazz.getSourceFile(),
|
||||
analyzedClasses: [analyzedClass],
|
||||
}];
|
||||
const host = new DefaultMigrationHost(
|
||||
mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles,
|
||||
diagnosticHandler);
|
||||
expect(() => host.injectSyntheticDecorator(mockClazz, mockDecorator))
|
||||
.toThrow(jasmine.objectContaining(
|
||||
{code: ErrorCode.NGCC_MIGRATION_DECORATOR_INJECTION_ERROR}));
|
||||
});
|
||||
|
||||
it('should report diagnostics from handlers', () => {
|
||||
const log: string[] = [];
|
||||
const handler = new DiagnosticProducingHandler('handler', log);
|
||||
const analyzedFiles: AnalyzedFile[] = [];
|
||||
const diagnostics: ts.Diagnostic[] = [];
|
||||
const host = new DefaultMigrationHost(
|
||||
mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles,
|
||||
diagnostic => diagnostics.push(diagnostic));
|
||||
mockClazz.getStart = () => 0;
|
||||
mockClazz.getWidth = () => 0;
|
||||
|
||||
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);
|
||||
|
||||
expect(diagnostics.length).toBe(1);
|
||||
expect(ts.flattenDiagnosticMessageText(diagnostics[0].messageText, '\n'))
|
||||
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` +
|
||||
@ -206,125 +92,114 @@ runInEachFileSystem(() => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
describe('getAllDecorators', () => {
|
||||
it('should be null for unknown source files', () => {
|
||||
const log: string[] = [];
|
||||
const handler = new AlwaysDetectHandler('handler', log);
|
||||
const analyzedFiles: AnalyzedFile[] = [];
|
||||
const host = new DefaultMigrationHost(
|
||||
mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles,
|
||||
diagnosticHandler);
|
||||
|
||||
const decorators = host.getAllDecorators(mockClazz);
|
||||
expect(decorators).toBeNull();
|
||||
});
|
||||
|
||||
it('should be null for unknown classes', () => {
|
||||
const log: string[] = [];
|
||||
const handler = new AlwaysDetectHandler('handler', log);
|
||||
const analyzedFiles: AnalyzedFile[] = [];
|
||||
const host = new DefaultMigrationHost(
|
||||
mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles,
|
||||
diagnosticHandler);
|
||||
|
||||
const sourceFile: any = {};
|
||||
const unrelatedClass: any = {
|
||||
getSourceFile: () => sourceFile,
|
||||
};
|
||||
analyzedFiles.push({sourceFile, analyzedClasses: [unrelatedClass]});
|
||||
|
||||
const decorators = host.getAllDecorators(mockClazz);
|
||||
expect(decorators).toBeNull();
|
||||
});
|
||||
|
||||
it('should include injected decorators', () => {
|
||||
const log: string[] = [];
|
||||
const handler = new AlwaysDetectHandler('handler', log);
|
||||
const existingDecorator = { name: 'ExistingDecorator' } as Decorator;
|
||||
const analyzedClass: AnalyzedClass = {
|
||||
name: 'MockClazz',
|
||||
declaration: mockClazz,
|
||||
matches: [],
|
||||
decorators: [existingDecorator],
|
||||
};
|
||||
const analyzedFiles: AnalyzedFile[] = [{
|
||||
sourceFile: mockClazz.getSourceFile(),
|
||||
analyzedClasses: [analyzedClass],
|
||||
}];
|
||||
const host = new DefaultMigrationHost(
|
||||
mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles,
|
||||
diagnosticHandler);
|
||||
host.injectSyntheticDecorator(mockClazz, mockDecorator);
|
||||
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';
|
||||
|
||||
const decorators = host.getAllDecorators(mockClazz) !;
|
||||
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]).toBe(existingDecorator);
|
||||
expect(decorators[1]).toBe(mockDecorator);
|
||||
expect(decorators[0].name).toBe('Directive');
|
||||
expect(decorators[1].name).toBe('InjectedDecorator');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isInScope', () => {
|
||||
it('should be true for nodes within the entry-point', () => {
|
||||
const analyzedFiles: AnalyzedFile[] = [];
|
||||
const host = new DefaultMigrationHost(
|
||||
mockHost, mockMetadata, mockEvaluator, [], entryPointPath, analyzedFiles,
|
||||
diagnosticHandler);
|
||||
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);
|
||||
|
||||
const sourceFile: any = {
|
||||
fileName: _('/node_modules/some-package/entry-point/relative.js'),
|
||||
};
|
||||
const clazz: any = {
|
||||
getSourceFile: () => sourceFile,
|
||||
};
|
||||
expect(host.isInScope(clazz)).toBe(true);
|
||||
expect(host.isInScope(internalClass)).toBe(true);
|
||||
});
|
||||
|
||||
it('should be false for nodes outside the entry-point', () => {
|
||||
const analyzedFiles: AnalyzedFile[] = [];
|
||||
const host = new DefaultMigrationHost(
|
||||
mockHost, mockMetadata, mockEvaluator, [], entryPointPath, analyzedFiles,
|
||||
diagnosticHandler);
|
||||
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);
|
||||
|
||||
const sourceFile: any = {
|
||||
fileName: _('/node_modules/some-package/other-entry/index.js'),
|
||||
};
|
||||
const clazz: any = {
|
||||
getSourceFile: () => sourceFile,
|
||||
};
|
||||
expect(host.isInScope(clazz)).toBe(false);
|
||||
expect(host.isInScope(externalClass)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class TestHandler implements DecoratorHandler<unknown, unknown, unknown> {
|
||||
constructor(readonly name: string, protected log: string[]) {}
|
||||
class DetectDecoratorHandler implements DecoratorHandler<unknown, unknown, unknown> {
|
||||
readonly name = DetectDecoratorHandler.name;
|
||||
|
||||
constructor(private decorator: string, readonly precedence: HandlerPrecedence) {}
|
||||
|
||||
precedence = HandlerPrecedence.PRIMARY;
|
||||
detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<unknown>|undefined {
|
||||
this.log.push(`${this.name}:detect:${node.name.text}:${decorators !.map(d => d.name)}`);
|
||||
return undefined;
|
||||
}
|
||||
analyze(node: ClassDeclaration): AnalysisOutput<unknown> {
|
||||
this.log.push(this.name + ':analyze:' + node.name.text);
|
||||
return {};
|
||||
}
|
||||
compile(node: ClassDeclaration): CompileResult|CompileResult[] {
|
||||
this.log.push(this.name + ':compile:' + node.name.text);
|
||||
return [];
|
||||
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 AlwaysDetectHandler extends TestHandler {
|
||||
detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<unknown>|undefined {
|
||||
super.detect(node, decorators);
|
||||
return {trigger: node, metadata: {}};
|
||||
}
|
||||
}
|
||||
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: {}};
|
||||
}
|
||||
|
||||
class DiagnosticProducingHandler extends AlwaysDetectHandler {
|
||||
analyze(node: ClassDeclaration): AnalysisOutput<any> {
|
||||
super.analyze(node);
|
||||
return {diagnostics: [makeDiagnostic(9999, node, 'test diagnostic')]};
|
||||
}
|
||||
|
||||
compile(node: ClassDeclaration): CompileResult|CompileResult[] { return []; }
|
||||
}
|
||||
|
@ -0,0 +1,351 @@
|
||||
/**
|
||||
* @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 {ErrorCode, makeDiagnostic, ngErrorCode} 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 {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('NgccTraitCompiler', () => {
|
||||
let _: typeof absoluteFrom;
|
||||
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 createCompiler({entryPoint, handlers}: {
|
||||
entryPoint: EntryPointBundle; handlers: DecoratorHandler<unknown, unknown, unknown>[]
|
||||
}) {
|
||||
const reflectionHost = new Esm2015ReflectionHost(new MockLogger(), false, entryPoint.src);
|
||||
return new NgccTraitCompiler(handlers, reflectionHost);
|
||||
}
|
||||
|
||||
describe('injectSyntheticDecorator()', () => {
|
||||
it('should call `detect()` on each of the provided handlers', () => {
|
||||
const log: string[] = [];
|
||||
const handler1 = new TestHandler('handler1', log);
|
||||
const handler2 = new TestHandler('handler2', log);
|
||||
loadTestFiles([{name: _('/node_modules/test/index.js'), contents: ``}]);
|
||||
const entryPoint =
|
||||
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
|
||||
const compiler = createCompiler({entryPoint, handlers: [handler1, handler2]});
|
||||
compiler.injectSyntheticDecorator(mockClazz, injectedDecorator);
|
||||
expect(log).toEqual([
|
||||
`handler1:detect:MockClazz:InjectedDecorator`,
|
||||
`handler2:detect:MockClazz:InjectedDecorator`,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should call `analyze()` on each of the provided handlers whose `detect()` call returns a result',
|
||||
() => {
|
||||
const log: string[] = [];
|
||||
const handler1 = new TestHandler('handler1', log);
|
||||
const handler2 = new AlwaysDetectHandler('handler2', log);
|
||||
const handler3 = new TestHandler('handler3', log);
|
||||
loadTestFiles([{name: _('/node_modules/test/index.js'), contents: ``}]);
|
||||
const entryPoint = makeTestEntryPointBundle(
|
||||
'test', 'esm2015', false, [_('/node_modules/test/index.js')]);
|
||||
const compiler = createCompiler({entryPoint, handlers: [handler1, handler2, handler3]});
|
||||
compiler.injectSyntheticDecorator(mockClazz, injectedDecorator);
|
||||
expect(log).toEqual([
|
||||
`handler1:detect:MockClazz:InjectedDecorator`,
|
||||
`handler2:detect:MockClazz:InjectedDecorator`,
|
||||
`handler3:detect:MockClazz:InjectedDecorator`,
|
||||
'handler2:analyze:MockClazz',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should inject a new class record into the compilation', () => {
|
||||
const injectedHandler =
|
||||
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 compiler = createCompiler({entryPoint, handlers: [injectedHandler]});
|
||||
compiler.injectSyntheticDecorator(mockClazz, injectedDecorator);
|
||||
|
||||
const record = compiler.recordFor(mockClazz);
|
||||
expect(record).toBeDefined();
|
||||
expect(record !.traits.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should add a new trait to an existing class record', () => {
|
||||
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 compiler =
|
||||
createCompiler({entryPoint, handlers: [directiveHandler, injectedHandler]});
|
||||
const myClass = getDeclaration(
|
||||
entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass',
|
||||
isNamedClassDeclaration);
|
||||
|
||||
compiler.analyzeFile(entryPoint.src.file);
|
||||
compiler.injectSyntheticDecorator(myClass, injectedDecorator);
|
||||
|
||||
const record = compiler.recordFor(myClass) !;
|
||||
expect(record).toBeDefined();
|
||||
expect(record.traits.length).toBe(2);
|
||||
expect(record.traits[0].detected.decorator !.name).toBe('Directive');
|
||||
expect(record.traits[1].detected.decorator !.name).toBe('InjectedDecorator');
|
||||
});
|
||||
|
||||
it('should not add a weak handler when a primary handler already exists', () => {
|
||||
const directiveHandler = new DetectDecoratorHandler('Directive', HandlerPrecedence.PRIMARY);
|
||||
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 compiler =
|
||||
createCompiler({entryPoint, handlers: [directiveHandler, injectedHandler]});
|
||||
const myClass = getDeclaration(
|
||||
entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass',
|
||||
isNamedClassDeclaration);
|
||||
|
||||
compiler.analyzeFile(entryPoint.src.file);
|
||||
|
||||
compiler.injectSyntheticDecorator(myClass, injectedDecorator);
|
||||
|
||||
const record = compiler.recordFor(myClass) !;
|
||||
expect(record).toBeDefined();
|
||||
expect(record.traits.length).toBe(1);
|
||||
expect(record.traits[0].detected.decorator !.name).toBe('Directive');
|
||||
});
|
||||
|
||||
it('should replace an existing weak handler when injecting a primary handler', () => {
|
||||
const directiveHandler = new DetectDecoratorHandler('Directive', HandlerPrecedence.WEAK);
|
||||
const injectedHandler =
|
||||
new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.PRIMARY);
|
||||
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 compiler =
|
||||
createCompiler({entryPoint, handlers: [directiveHandler, injectedHandler]});
|
||||
const myClass = getDeclaration(
|
||||
entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass',
|
||||
isNamedClassDeclaration);
|
||||
|
||||
compiler.analyzeFile(entryPoint.src.file);
|
||||
|
||||
compiler.injectSyntheticDecorator(myClass, injectedDecorator);
|
||||
|
||||
const record = compiler.recordFor(myClass) !;
|
||||
expect(record).toBeDefined();
|
||||
expect(record.traits.length).toBe(1);
|
||||
expect(record.traits[0].detected.decorator !.name).toBe('InjectedDecorator');
|
||||
});
|
||||
|
||||
it('should produce an error when a primary handler is added when a primary handler is already present',
|
||||
() => {
|
||||
const directiveHandler =
|
||||
new DetectDecoratorHandler('Directive', HandlerPrecedence.PRIMARY);
|
||||
const injectedHandler =
|
||||
new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.PRIMARY);
|
||||
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 compiler =
|
||||
createCompiler({entryPoint, handlers: [directiveHandler, injectedHandler]});
|
||||
const myClass = getDeclaration(
|
||||
entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass',
|
||||
isNamedClassDeclaration);
|
||||
|
||||
compiler.analyzeFile(entryPoint.src.file);
|
||||
|
||||
compiler.injectSyntheticDecorator(myClass, injectedDecorator);
|
||||
|
||||
const record = compiler.recordFor(myClass) !;
|
||||
expect(record).toBeDefined();
|
||||
expect(record.metaDiagnostics).toBeDefined();
|
||||
expect(record.metaDiagnostics !.length).toBe(1);
|
||||
expect(record.metaDiagnostics ![0].code)
|
||||
.toBe(ngErrorCode(ErrorCode.DECORATOR_COLLISION));
|
||||
expect(record.traits.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should report diagnostics from handlers', () => {
|
||||
const log: string[] = [];
|
||||
const handler = new DiagnosticProducingHandler('handler', log);
|
||||
loadTestFiles([{name: _('/node_modules/test/index.js'), contents: ``}]);
|
||||
const entryPoint =
|
||||
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
|
||||
const compiler = createCompiler({entryPoint, handlers: [handler]});
|
||||
const decorator = createComponentDecorator(mockClazz, {selector: 'comp', exportAs: null});
|
||||
compiler.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(migratedTrait.diagnostics[0].messageText).toEqual(`test diagnostic`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
describe('getAllDecorators', () => {
|
||||
it('should be null for classes without decorators', () => {
|
||||
loadTestFiles(
|
||||
[{name: _('/node_modules/test/index.js'), contents: `export class MyClass {};`}]);
|
||||
const entryPoint =
|
||||
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
|
||||
const compiler = createCompiler({entryPoint, handlers: []});
|
||||
const myClass = getDeclaration(
|
||||
entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass',
|
||||
isNamedClassDeclaration);
|
||||
|
||||
const decorators = compiler.getAllDecorators(myClass);
|
||||
expect(decorators).toBeNull();
|
||||
});
|
||||
|
||||
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 compiler =
|
||||
createCompiler({entryPoint, handlers: [directiveHandler, injectedHandler]});
|
||||
const myClass = getDeclaration(
|
||||
entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass',
|
||||
isNamedClassDeclaration);
|
||||
|
||||
compiler.analyzeFile(entryPoint.src.file);
|
||||
|
||||
compiler.injectSyntheticDecorator(myClass, injectedDecorator);
|
||||
|
||||
const decorators = compiler.getAllDecorators(myClass) !;
|
||||
expect(decorators.length).toBe(2);
|
||||
expect(decorators[0].name).toBe('Directive');
|
||||
expect(decorators[1].name).toBe('InjectedDecorator');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
class TestHandler implements DecoratorHandler<unknown, unknown, unknown> {
|
||||
constructor(readonly name: string, protected log: string[]) {}
|
||||
|
||||
precedence = HandlerPrecedence.PRIMARY;
|
||||
|
||||
detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<unknown>|undefined {
|
||||
this.log.push(`${this.name}:detect:${node.name.text}:${decorators !.map(d => d.name)}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
analyze(node: ClassDeclaration): AnalysisOutput<unknown> {
|
||||
this.log.push(this.name + ':analyze:' + node.name.text);
|
||||
return {};
|
||||
}
|
||||
|
||||
compile(node: ClassDeclaration): CompileResult|CompileResult[] {
|
||||
this.log.push(this.name + ':compile:' + node.name.text);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
class AlwaysDetectHandler extends TestHandler {
|
||||
detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<unknown>|undefined {
|
||||
super.detect(node, decorators);
|
||||
const decorator = decorators !== null ? decorators[0] : null;
|
||||
return {trigger: node, decorator, metadata: {}};
|
||||
}
|
||||
}
|
||||
|
||||
class DetectDecoratorHandler extends TestHandler {
|
||||
constructor(private decorator: string, readonly precedence: HandlerPrecedence) {
|
||||
super(decorator, []);
|
||||
}
|
||||
|
||||
detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<unknown>|undefined {
|
||||
super.detect(node, decorators);
|
||||
if (decorators === null) {
|
||||
return undefined;
|
||||
}
|
||||
const decorator = decorators.find(decorator => decorator.name === this.decorator);
|
||||
if (decorator === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return {trigger: node, decorator, metadata: {}};
|
||||
}
|
||||
}
|
||||
|
||||
class DiagnosticProducingHandler extends AlwaysDetectHandler {
|
||||
analyze(node: ClassDeclaration): AnalysisOutput<any> {
|
||||
super.analyze(node);
|
||||
return {diagnostics: [makeDiagnostic(9999, node, 'test diagnostic')]};
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user