Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export class DecorationAnalyzer {
const analysedFiles = this.program.getSourceFiles()
.map(sourceFile => this.analyzeFile(sourceFile))
.filter(isDefined);
analysedFiles.forEach(analysedFile => this.resolveFile(analysedFile));
const compiledFiles = analysedFiles.map(analysedFile => this.compileFile(analysedFile));
compiledFiles.forEach(
compiledFile => decorationAnalyses.set(compiledFile.sourceFile, compiledFile));
Expand Down Expand Up @@ -202,6 +203,16 @@ export class DecorationAnalyzer {
}
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>>):
Expand Down
222 changes: 138 additions & 84 deletions packages/compiler-cli/src/ngcc/test/analysis/decoration_analyzer_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,37 @@ import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../ngtsc/path';
import {Decorator} from '../../../ngtsc/reflection';
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 {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {makeTestBundleProgram} from '../helpers/utils';

const TEST_PROGRAM = {
name: 'test.js',
contents: `
import {Component, Injectable} from '@angular/core';
const TEST_PROGRAM = [
{
name: 'test.js',
contents: `
import {Component, Directive, Injectable} from '@angular/core';

export class MyComponent {}
MyComponent.decorators = [{type: Component}];
export class MyComponent {}
MyComponent.decorators = [{type: Component}];

export class MyService {}
MyService.decorators = [{type: Injectable}];
`
};
export class MyDirective {}
MyDirective.decorators = [{type: Directive}];

export class MyService {}
MyService.decorators = [{type: Injectable}];
`,
},
{
name: 'other.js',
contents: `
import {Component} from '@angular/core';

export class MyOtherComponent {}
MyOtherComponent.decorators = [{type: Component}];
`,
},
];

const INTERNAL_COMPONENT_PROGRAM = [
{
Expand Down Expand Up @@ -56,44 +70,66 @@ const INTERNAL_COMPONENT_PROGRAM = [
}
];

function createTestHandler() {
const handler = jasmine.createSpyObj<DecoratorHandler<any, any>>('TestDecoratorHandler', [
'detect',
'analyze',
'compile',
]);
// Only detect the Component decorator
handler.detect.and.callFake(
(node: ts.Declaration, decorators: Decorator[]): DetectResult<any>| undefined => {
if (!decorators) {
return undefined;
}
const metadata = decorators.find(d => d.name === 'Component');
if (metadata === undefined) {
return undefined;
} else {
return {
metadata,
trigger: metadata.node,
};
}
});
// The "test" analysis is just the name of the decorator being analyzed
handler.analyze.and.callFake(
((decl: ts.Declaration, dec: Decorator) => ({analysis: 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;
}
type DecoratorHandlerWithResolve = DecoratorHandler<any, any>& {
resolve: NonNullable<DecoratorHandler<any, any>['resolve']>;
Comment thread
gkalpak marked this conversation as resolved.
Resolved
};

describe('DecorationAnalyzer', () => {
describe('analyzeProgram()', () => {
let logs: string[];
let program: ts.Program;
let testHandler: jasmine.SpyObj<DecoratorHandler<any, any>>;
let testHandler: jasmine.SpyObj<DecoratorHandlerWithResolve>;
let result: DecorationAnalyses;

beforeEach(() => {
const {options, host, ...bundle} = makeTestBundleProgram([TEST_PROGRAM]);
// 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>) => {
Comment thread
gkalpak marked this conversation as resolved.
Resolved
logs = [];

const {options, host, ...bundle} = makeTestBundleProgram(...progArgs);
program = bundle.program;

const reflectionHost = new Esm2015ReflectionHost(false, program.getTypeChecker());
Expand All @@ -104,50 +140,77 @@ describe('DecorationAnalyzer', () => {
testHandler = createTestHandler();
analyzer.handlers = [testHandler];
result = analyzer.analyzeProgram();
});
};

it('should return an object containing a reference to the original source file', () => {
const file = program.getSourceFile(TEST_PROGRAM.name) !;
expect(result.get(file) !.sourceFile).toBe(file);
});
describe('basic usage', () => {
beforeEach(() => setUpAndAnalyzeProgram(TEST_PROGRAM));

it('should call detect on the decorator handlers with each class from the parsed file', () => {
expect(testHandler.detect).toHaveBeenCalledTimes(2);
expect(testHandler.detect.calls.allArgs()[0][1]).toEqual([jasmine.objectContaining(
{name: 'Component'})]);
expect(testHandler.detect.calls.allArgs()[1][1]).toEqual([jasmine.objectContaining(
{name: 'Injectable'})]);
});
it('should return an object containing a reference to the original source file', () => {
TEST_PROGRAM.forEach(({name}) => {
const file = program.getSourceFile(name) !;
expect(result.get(file) !.sourceFile).toBe(file);
});
});

it('should return an object containing the classes that were analyzed', () => {
const file = program.getSourceFile(TEST_PROGRAM.name) !;
const compiledFile = result.get(file) !;
expect(compiledFile.compiledClasses.length).toEqual(1);
expect(compiledFile.compiledClasses[0].name).toEqual('MyComponent');
});
it('should call detect on the decorator handlers with each class from the parsed file',
() => {
expect(testHandler.detect).toHaveBeenCalledTimes(4);
expect(testHandler.detect.calls.allArgs().map(args => args[1][0])).toEqual([
jasmine.objectContaining({name: 'Component'}),
jasmine.objectContaining({name: 'Directive'}),
jasmine.objectContaining({name: 'Injectable'}),
jasmine.objectContaining({name: 'Component'}),
]);
});

it('should analyze and compile the classes that are detected', () => {
expect(testHandler.analyze).toHaveBeenCalledTimes(1);
expect(testHandler.analyze.calls.allArgs()[0][1].name).toEqual('Component');
it('should return an object containing the classes that were analyzed', () => {
const file1 = program.getSourceFile(TEST_PROGRAM[0].name) !;
const compiledFile1 = result.get(file1) !;
expect(compiledFile1.compiledClasses.length).toEqual(2);
expect(compiledFile1.compiledClasses[0]).toEqual(jasmine.objectContaining({
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 compiledFile2 = result.get(file2) !;
expect(compiledFile2.compiledClasses.length).toEqual(1);
expect(compiledFile2.compiledClasses[0]).toEqual(jasmine.objectContaining({
name: 'MyOtherComponent', compilation: ['@Component (compiled)'],
} as unknown as CompiledClass));
});

expect(testHandler.compile).toHaveBeenCalledTimes(1);
expect(testHandler.compile.calls.allArgs()[0][1]).toEqual('Component');
it('should analyze, resolve and compile the classes that are detected', () => {
expect(logs).toEqual([
// First detect and (potentially) analyze.
'detect: MyComponent@Component',
'analyze: MyComponent@Component',
'detect: MyDirective@Directive',
'analyze: MyDirective@Directive',
'detect: MyService@Injectable',
'detect: MyOtherComponent@Component',
'analyze: MyOtherComponent@Component',
// The resolve.
'resolve: MyComponent@Component',
'resolve: MyDirective@Directive',
'resolve: MyOtherComponent@Component',
// Finally compile.
'compile: MyComponent@Component (resolved: true)',
'compile: MyDirective@Directive (resolved: true)',
'compile: MyOtherComponent@Component (resolved: true)',
]);
});
});

describe('internal components', () => {
beforeEach(() => setUpAndAnalyzeProgram(INTERNAL_COMPONENT_PROGRAM));

// The problem of exposing the type of these internal components in the .d.ts typing files
// is not yet solved.
it('should analyze an internally imported component, which is not publicly exported from the entry-point',
() => {
const {program, options, host} = makeTestBundleProgram(INTERNAL_COMPONENT_PROGRAM);
const reflectionHost = new Esm2015ReflectionHost(false, program.getTypeChecker());
const referencesRegistry = new NgccReferencesRegistry(reflectionHost);
const analyzer = new DecorationAnalyzer(
program, options, host, program.getTypeChecker(), reflectionHost, referencesRegistry,
[AbsoluteFsPath.fromUnchecked('/')], false);
const testHandler = createTestHandler();
analyzer.handlers = [testHandler];
const result = analyzer.analyzeProgram();
const file = program.getSourceFile('component.js') !;
const analysis = result.get(file) !;
expect(analysis).toBeDefined();
Expand All @@ -157,15 +220,6 @@ describe('DecorationAnalyzer', () => {
});

it('should analyze an internally defined component, which is not exported at all', () => {
const {program, options, host} = makeTestBundleProgram(INTERNAL_COMPONENT_PROGRAM);
const reflectionHost = new Esm2015ReflectionHost(false, program.getTypeChecker());
const referencesRegistry = new NgccReferencesRegistry(reflectionHost);
const analyzer = new DecorationAnalyzer(
program, options, host, program.getTypeChecker(), reflectionHost, referencesRegistry,
[AbsoluteFsPath.fromUnchecked('/')], false);
const testHandler = createTestHandler();
analyzer.handlers = [testHandler];
const result = analyzer.analyzeProgram();
const file = program.getSourceFile('entrypoint.js') !;
const analysis = result.get(file) !;
expect(analysis).toBeDefined();
Expand Down
4 changes: 2 additions & 2 deletions packages/compiler-cli/src/ngtsc/imports/src/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ export interface ReferenceEmitStrategy {
/**
* Generates `Expression`s which refer to `Reference`s in a given context.
*
* A `ReferenceEmitter` uses one or more `ReferenceEmitStrategy` implementations to produce a
* an `Expression` which refers to a `Reference` in the context of a particular file.
* A `ReferenceEmitter` uses one or more `ReferenceEmitStrategy` implementations to produce an
Comment thread
gkalpak marked this conversation as resolved.
Resolved
* `Expression` which refers to a `Reference` in the context of a particular file.
*/
export class ReferenceEmitter {
constructor(private strategies: ReferenceEmitStrategy[]) {}
Expand Down
5 changes: 3 additions & 2 deletions packages/compiler-cli/src/ngtsc/imports/src/find_export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ export function findExportedNameOfNode(
// Look for the export which declares the node.
const found = exports.find(sym => symbolDeclaresNode(sym, target, checker));
if (found === undefined) {
throw new Error(`failed to find target in ${file.fileName}`);
throw new Error(
`Failed to find exported name of node (${target.getText()}) in '${file.fileName}'.`);
}
return found !== undefined ? found.name : null;
return found.name;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-cli/src/ngtsc/path/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// TODO(alxhub): Unify this file with `util/src/path`.

const TS_DTS_JS_EXTENSION = /(?:\.d)?\.ts$|\.js$/;
const ABSOLUTE_PATH = /^([a-zA-Z]\:\/|\/)/;
const ABSOLUTE_PATH = /^([a-zA-Z]:\/|\/)/;

/**
* Convert Windows-style separators to POSIX separators.
Expand Down