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
37 changes: 21 additions & 16 deletions packages/compiler-cli/src/ngcc/src/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {ConstantPool} from '@angular/compiler';
import * as fs from 'fs';
import * as ts from 'typescript';

import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from '../../ngtsc/annotations';
import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from '../../ngtsc/annotations';
import {Decorator} from '../../ngtsc/host';
import {CompileResult, DecoratorHandler} from '../../ngtsc/transform';

Expand All @@ -18,8 +18,8 @@ import {ParsedClass} from './parsing/parsed_class';
import {ParsedFile} from './parsing/parsed_file';
import {isDefined} from './utils';

export interface AnalyzedClass<T = any> extends ParsedClass {
handler: DecoratorHandler<T>;
export interface AnalyzedClass<A = any, M = any> extends ParsedClass {
handler: DecoratorHandler<A, M>;
analysis: any;
diagnostics?: ts.Diagnostic[];
compilation: CompileResult[];
Expand All @@ -31,9 +31,9 @@ export interface AnalyzedFile {
constantPool: ConstantPool;
}

export interface MatchingHandler<T> {
handler: DecoratorHandler<T>;
decorator: Decorator;
export interface MatchingHandler<A, M> {
handler: DecoratorHandler<A, M>;
match: M;
}

/**
Expand All @@ -46,7 +46,8 @@ export class FileResourceLoader implements ResourceLoader {
export class Analyzer {
resourceLoader = new FileResourceLoader();
scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.host);
handlers: DecoratorHandler<any>[] = [
handlers: DecoratorHandler<any, any>[] = [
new BaseDefDecoratorHandler(this.typeChecker, this.host),
new ComponentDecoratorHandler(
this.typeChecker, this.host, this.scopeRegistry, false, this.resourceLoader),
new DirectiveDecoratorHandler(this.typeChecker, this.host, this.scopeRegistry, false),
Expand Down Expand Up @@ -76,20 +77,23 @@ export class Analyzer {

protected analyzeClass(file: ts.SourceFile, pool: ConstantPool, clazz: ParsedClass): AnalyzedClass
|undefined {
const matchingHandlers =
this.handlers.map(handler => ({handler, decorator: handler.detect(clazz.decorators)}))
.filter(isMatchingHandler);
const matchingHandlers = this.handlers
.map(handler => ({
handler,
match: handler.detect(clazz.declaration, clazz.decorators),
}))
.filter(isMatchingHandler);

if (matchingHandlers.length > 1) {
throw new Error('TODO.Diagnostic: Class has multiple Angular decorators.');
}

if (matchingHandlers.length == 0) {
if (matchingHandlers.length === 0) {
return undefined;
}

const {handler, decorator} = matchingHandlers[0];
const {analysis, diagnostics} = handler.analyze(clazz.declaration, decorator);
const {handler, match} = matchingHandlers[0];
const {analysis, diagnostics} = handler.analyze(clazz.declaration, match);
let compilation = handler.compile(clazz.declaration, analysis, pool);
if (!Array.isArray(compilation)) {
compilation = [compilation];
Expand All @@ -98,6 +102,7 @@ export class Analyzer {
}
}

function isMatchingHandler<T>(handler: Partial<MatchingHandler<T>>): handler is MatchingHandler<T> {
return !!handler.decorator;
}
function isMatchingHandler<A, M>(handler: Partial<MatchingHandler<A, M>>):
handler is MatchingHandler<A, M> {
return !!handler.match;
}
16 changes: 10 additions & 6 deletions packages/compiler-cli/src/ngcc/test/analyzer_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,18 @@ const TEST_PROGRAM = {
};

function createTestHandler() {
const handler = jasmine.createSpyObj<DecoratorHandler<any>>('TestDecoratorHandler', [
const handler = jasmine.createSpyObj<DecoratorHandler<any, any>>('TestDecoratorHandler', [
'detect',
'analyze',
'compile',
]);
// Only detect the Component decorator
handler.detect.and.callFake(
(decorators: Decorator[]) => decorators.find(d => d.name === 'Component'));
handler.detect.and.callFake((node: ts.Declaration, decorators: Decorator[]) => {
if (!decorators) {
return undefined;
}
return decorators.find(d => d.name === 'Component');
});
// 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: null})));
Expand Down Expand Up @@ -69,7 +73,7 @@ function createParsedFile(program: ts.Program) {
describe('Analyzer', () => {
describe('analyzeFile()', () => {
let program: ts.Program;
let testHandler: jasmine.SpyObj<DecoratorHandler<any>>;
let testHandler: jasmine.SpyObj<DecoratorHandler<any, any>>;
let result: AnalyzedFile;

beforeEach(() => {
Expand All @@ -87,9 +91,9 @@ describe('Analyzer', () => {

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][0]).toEqual([jasmine.objectContaining(
expect(testHandler.detect.calls.allArgs()[0][1]).toEqual([jasmine.objectContaining(
{name: 'Component'})]);
expect(testHandler.detect.calls.allArgs()[1][0]).toEqual([jasmine.objectContaining(
expect(testHandler.detect.calls.allArgs()[1][1]).toEqual([jasmine.objectContaining(
{name: 'Injectable'})]);
});

Expand Down
1 change: 1 addition & 0 deletions packages/compiler-cli/src/ngtsc/annotations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

export {ResourceLoader} from './src/api';
export {BaseDefDecoratorHandler} from './src/base_def';
export {ComponentDecoratorHandler} from './src/component';
export {DirectiveDecoratorHandler} from './src/directive';
export {InjectableDecoratorHandler} from './src/injectable';
Expand Down
120 changes: 120 additions & 0 deletions packages/compiler-cli/src/ngtsc/annotations/src/base_def.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* @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 {R3BaseRefMetaData, compileBaseDefFromMetadata} from '@angular/compiler';
import * as ts from 'typescript';

import {ClassMember, Decorator, ReflectionHost} from '../../host';
import {staticallyResolve} from '../../metadata';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {isAngularCore} from './util';

function containsNgTopLevelDecorator(decorators: Decorator[] | null): boolean {
if (!decorators) {
return false;
}
return decorators.find(
decorator => (decorator.name === 'Component' || decorator.name === 'Directive' ||
decorator.name === 'NgModule') &&
isAngularCore(decorator)) !== undefined;
}

export class BaseDefDecoratorHandler implements
DecoratorHandler<R3BaseRefMetaData, R3BaseRefDecoratorDetection> {
constructor(private checker: ts.TypeChecker, private reflector: ReflectionHost, ) {}

detect(node: ts.ClassDeclaration, decorators: Decorator[]|null): R3BaseRefDecoratorDetection
|undefined {
if (containsNgTopLevelDecorator(decorators)) {
// If the class is already decorated by @Component or @Directive let that
// DecoratorHandler handle this. BaseDef is unnecessary.
return undefined;
}

let result: R3BaseRefDecoratorDetection|undefined = undefined;

this.reflector.getMembersOfClass(node).forEach(property => {
const {decorators} = property;
if (decorators) {
for (const decorator of decorators) {
const decoratorName = decorator.name;
if (decoratorName === 'Input' && isAngularCore(decorator)) {
result = result || {};
const inputs = result.inputs = result.inputs || [];
inputs.push({decorator, property});
} else if (decoratorName === 'Output' && isAngularCore(decorator)) {
result = result || {};
const outputs = result.outputs = result.outputs || [];
outputs.push({decorator, property});
}
}
}
});

return result;
}

analyze(node: ts.ClassDeclaration, metadata: R3BaseRefDecoratorDetection):
AnalysisOutput<R3BaseRefMetaData> {
const analysis: R3BaseRefMetaData = {};
if (metadata.inputs) {
const inputs = analysis.inputs = {} as{[key: string]: string | [string, string]};
metadata.inputs.forEach(({decorator, property}) => {
const propName = property.name;
const args = decorator.args;
let value: string|[string, string];
if (args && args.length > 0) {
const resolvedValue = staticallyResolve(args[0], this.reflector, this.checker);
if (typeof resolvedValue !== 'string') {
throw new TypeError('Input alias does not resolve to a string value');
}
value = [resolvedValue, propName];
} else {
value = propName;
}
inputs[propName] = value;
});
}

if (metadata.outputs) {
const outputs = analysis.outputs = {} as{[key: string]: string};
metadata.outputs.forEach(({decorator, property}) => {
const propName = property.name;
const args = decorator.args;
let value: string;
if (args && args.length > 0) {
const resolvedValue = staticallyResolve(args[0], this.reflector, this.checker);
if (typeof resolvedValue !== 'string') {
throw new TypeError('Output alias does not resolve to a string value');
}
value = resolvedValue;
} else {
value = propName;
}
outputs[propName] = value;
});
}

return {analysis};
}

compile(node: ts.Declaration, analysis: R3BaseRefMetaData): CompileResult[]|CompileResult {
const {expression, type} = compileBaseDefFromMetadata(analysis);

return {
name: 'ngBaseDef',
initializer: expression, type,
statements: [],
};
}
}

export interface R3BaseRefDecoratorDetection {
inputs?: Array<{property: ClassMember, decorator: Decorator}>;
outputs?: Array<{property: ClassMember, decorator: Decorator}>;
}
7 changes: 5 additions & 2 deletions packages/compiler-cli/src/ngtsc/annotations/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const EMPTY_MAP = new Map<string, Expression>();
/**
* `DecoratorHandler` which handles the `@Component` annotation.
*/
export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMetadata> {
export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMetadata, Decorator> {
constructor(
private checker: ts.TypeChecker, private reflector: ReflectionHost,
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean,
Expand All @@ -33,7 +33,10 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();


detect(decorators: Decorator[]): Decorator|undefined {
detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined {
if (!decorators) {
return undefined;
}
return decorators.find(
decorator => decorator.name === 'Component' && (this.isCore || isAngularCore(decorator)));
}
Expand Down
7 changes: 5 additions & 2 deletions packages/compiler-cli/src/ngtsc/annotations/src/directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ import {getConstructorDependencies, isAngularCore, unwrapExpression, unwrapForwa

const EMPTY_OBJECT: {[key: string]: string} = {};

export class DirectiveDecoratorHandler implements DecoratorHandler<R3DirectiveMetadata> {
export class DirectiveDecoratorHandler implements DecoratorHandler<R3DirectiveMetadata, Decorator> {
constructor(
private checker: ts.TypeChecker, private reflector: ReflectionHost,
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}

detect(decorators: Decorator[]): Decorator|undefined {
detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined {
if (!decorators) {
return undefined;
}
return decorators.find(
decorator => decorator.name === 'Directive' && (this.isCore || isAngularCore(decorator)));
}
Expand Down
10 changes: 7 additions & 3 deletions packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ import {getConstructorDependencies, isAngularCore} from './util';
/**
* Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler.
*/
export class InjectableDecoratorHandler implements DecoratorHandler<R3InjectableMetadata> {
export class InjectableDecoratorHandler implements
DecoratorHandler<R3InjectableMetadata, Decorator> {
constructor(private reflector: ReflectionHost, private isCore: boolean) {}

detect(decorator: Decorator[]): Decorator|undefined {
return decorator.find(
detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined {
if (!decorators) {
return undefined;
}
return decorators.find(
decorator => decorator.name === 'Injectable' && (this.isCore || isAngularCore(decorator)));
}

Expand Down
7 changes: 5 additions & 2 deletions packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ export interface NgModuleAnalysis {
*
* TODO(alxhub): handle injector side of things as well.
*/
export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalysis> {
export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalysis, Decorator> {
constructor(
private checker: ts.TypeChecker, private reflector: ReflectionHost,
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}

detect(decorators: Decorator[]): Decorator|undefined {
detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined {
if (!decorators) {
return undefined;
}
return decorators.find(
decorator => decorator.name === 'NgModule' && (this.isCore || isAngularCore(decorator)));
}
Expand Down
9 changes: 6 additions & 3 deletions packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {SelectorScopeRegistry} from './selector_scope';
import {getConstructorDependencies, isAngularCore, unwrapExpression} from './util';

export class PipeDecoratorHandler implements DecoratorHandler<R3PipeMetadata> {
export class PipeDecoratorHandler implements DecoratorHandler<R3PipeMetadata, Decorator> {
constructor(
private checker: ts.TypeChecker, private reflector: ReflectionHost,
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}

detect(decorator: Decorator[]): Decorator|undefined {
return decorator.find(
detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined {
if (!decorators) {
return undefined;
}
return decorators.find(
decorator => decorator.name === 'Pipe' && (this.isCore || isAngularCore(decorator)));
}

Expand Down
2 changes: 2 additions & 0 deletions packages/compiler-cli/src/ngtsc/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as ts from 'typescript';
import * as api from '../transformers/api';

import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from './annotations';
import {BaseDefDecoratorHandler} from './annotations/src/base_def';
import {FactoryGenerator, FactoryInfo, GeneratedFactoryHostWrapper, generatedFactoryTransform} from './factories';
import {TypeScriptReflectionHost} from './metadata';
import {FileResourceLoader, HostResourceLoader} from './resource_loader';
Expand Down Expand Up @@ -169,6 +170,7 @@ export class NgtscProgram implements api.Program {

// Set up the IvyCompilation, which manages state for the Ivy transformer.
const handlers = [
new BaseDefDecoratorHandler(checker, this.reflector),
new ComponentDecoratorHandler(
checker, this.reflector, scopeRegistry, this.isCore, this.resourceLoader),
new DirectiveDecoratorHandler(checker, this.reflector, scopeRegistry, this.isCore),
Expand Down
Loading