Skip to content

Commit a0a29fd

Browse files
committed
feat(ivy): Add AOT handling for bare classes with Input and Output decorators (angular#25367)
PR Close angular#25367
1 parent 26066f2 commit a0a29fd

22 files changed

Lines changed: 483 additions & 60 deletions

File tree

packages/compiler-cli/src/ngcc/src/analyzer.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {ConstantPool} from '@angular/compiler';
99
import * as fs from 'fs';
1010
import * as ts from 'typescript';
1111

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

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

21-
export interface AnalyzedClass<T = any> extends ParsedClass {
22-
handler: DecoratorHandler<T>;
21+
export interface AnalyzedClass<A = any, M = any> extends ParsedClass {
22+
handler: DecoratorHandler<A, M>;
2323
analysis: any;
2424
diagnostics?: ts.Diagnostic[];
2525
compilation: CompileResult[];
@@ -31,9 +31,9 @@ export interface AnalyzedFile {
3131
constantPool: ConstantPool;
3232
}
3333

34-
export interface MatchingHandler<T> {
35-
handler: DecoratorHandler<T>;
36-
decorator: Decorator;
34+
export interface MatchingHandler<A, M> {
35+
handler: DecoratorHandler<A, M>;
36+
match: M;
3737
}
3838

3939
/**
@@ -46,7 +46,8 @@ export class FileResourceLoader implements ResourceLoader {
4646
export class Analyzer {
4747
resourceLoader = new FileResourceLoader();
4848
scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.host);
49-
handlers: DecoratorHandler<any>[] = [
49+
handlers: DecoratorHandler<any, any>[] = [
50+
new BaseDefDecoratorHandler(this.typeChecker, this.host),
5051
new ComponentDecoratorHandler(
5152
this.typeChecker, this.host, this.scopeRegistry, false, this.resourceLoader),
5253
new DirectiveDecoratorHandler(this.typeChecker, this.host, this.scopeRegistry, false),
@@ -76,20 +77,23 @@ export class Analyzer {
7677

7778
protected analyzeClass(file: ts.SourceFile, pool: ConstantPool, clazz: ParsedClass): AnalyzedClass
7879
|undefined {
79-
const matchingHandlers =
80-
this.handlers.map(handler => ({handler, decorator: handler.detect(clazz.decorators)}))
81-
.filter(isMatchingHandler);
80+
const matchingHandlers = this.handlers
81+
.map(handler => ({
82+
handler,
83+
match: handler.detect(clazz.declaration, clazz.decorators),
84+
}))
85+
.filter(isMatchingHandler);
8286

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

87-
if (matchingHandlers.length == 0) {
91+
if (matchingHandlers.length === 0) {
8892
return undefined;
8993
}
9094

91-
const {handler, decorator} = matchingHandlers[0];
92-
const {analysis, diagnostics} = handler.analyze(clazz.declaration, decorator);
95+
const {handler, match} = matchingHandlers[0];
96+
const {analysis, diagnostics} = handler.analyze(clazz.declaration, match);
9397
let compilation = handler.compile(clazz.declaration, analysis, pool);
9498
if (!Array.isArray(compilation)) {
9599
compilation = [compilation];
@@ -98,6 +102,7 @@ export class Analyzer {
98102
}
99103
}
100104

101-
function isMatchingHandler<T>(handler: Partial<MatchingHandler<T>>): handler is MatchingHandler<T> {
102-
return !!handler.decorator;
103-
}
105+
function isMatchingHandler<A, M>(handler: Partial<MatchingHandler<A, M>>):
106+
handler is MatchingHandler<A, M> {
107+
return !!handler.match;
108+
}

packages/compiler-cli/src/ngcc/test/analyzer_spec.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,18 @@ const TEST_PROGRAM = {
2828
};
2929

3030
function createTestHandler() {
31-
const handler = jasmine.createSpyObj<DecoratorHandler<any>>('TestDecoratorHandler', [
31+
const handler = jasmine.createSpyObj<DecoratorHandler<any, any>>('TestDecoratorHandler', [
3232
'detect',
3333
'analyze',
3434
'compile',
3535
]);
3636
// Only detect the Component decorator
37-
handler.detect.and.callFake(
38-
(decorators: Decorator[]) => decorators.find(d => d.name === 'Component'));
37+
handler.detect.and.callFake((node: ts.Declaration, decorators: Decorator[]) => {
38+
if (!decorators) {
39+
return undefined;
40+
}
41+
return decorators.find(d => d.name === 'Component');
42+
});
3943
// The "test" analysis is just the name of the decorator being analyzed
4044
handler.analyze.and.callFake(
4145
((decl: ts.Declaration, dec: Decorator) => ({analysis: dec.name, diagnostics: null})));
@@ -69,7 +73,7 @@ function createParsedFile(program: ts.Program) {
6973
describe('Analyzer', () => {
7074
describe('analyzeFile()', () => {
7175
let program: ts.Program;
72-
let testHandler: jasmine.SpyObj<DecoratorHandler<any>>;
76+
let testHandler: jasmine.SpyObj<DecoratorHandler<any, any>>;
7377
let result: AnalyzedFile;
7478

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

8892
it('should call detect on the decorator handlers with each class from the parsed file', () => {
8993
expect(testHandler.detect).toHaveBeenCalledTimes(2);
90-
expect(testHandler.detect.calls.allArgs()[0][0]).toEqual([jasmine.objectContaining(
94+
expect(testHandler.detect.calls.allArgs()[0][1]).toEqual([jasmine.objectContaining(
9195
{name: 'Component'})]);
92-
expect(testHandler.detect.calls.allArgs()[1][0]).toEqual([jasmine.objectContaining(
96+
expect(testHandler.detect.calls.allArgs()[1][1]).toEqual([jasmine.objectContaining(
9397
{name: 'Injectable'})]);
9498
});
9599

packages/compiler-cli/src/ngtsc/annotations/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
export {ResourceLoader} from './src/api';
10+
export {BaseDefDecoratorHandler} from './src/base_def';
1011
export {ComponentDecoratorHandler} from './src/component';
1112
export {DirectiveDecoratorHandler} from './src/directive';
1213
export {InjectableDecoratorHandler} from './src/injectable';
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {R3BaseRefMetaData, compileBaseDefFromMetadata} from '@angular/compiler';
10+
import * as ts from 'typescript';
11+
12+
import {ClassMember, Decorator, ReflectionHost} from '../../host';
13+
import {staticallyResolve} from '../../metadata';
14+
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
15+
import {isAngularCore} from './util';
16+
17+
function containsNgTopLevelDecorator(decorators: Decorator[] | null): boolean {
18+
if (!decorators) {
19+
return false;
20+
}
21+
return decorators.find(
22+
decorator => (decorator.name === 'Component' || decorator.name === 'Directive' ||
23+
decorator.name === 'NgModule') &&
24+
isAngularCore(decorator)) !== undefined;
25+
}
26+
27+
export class BaseDefDecoratorHandler implements
28+
DecoratorHandler<R3BaseRefMetaData, R3BaseRefDecoratorDetection> {
29+
constructor(private checker: ts.TypeChecker, private reflector: ReflectionHost, ) {}
30+
31+
detect(node: ts.ClassDeclaration, decorators: Decorator[]|null): R3BaseRefDecoratorDetection
32+
|undefined {
33+
if (containsNgTopLevelDecorator(decorators)) {
34+
// If the class is already decorated by @Component or @Directive let that
35+
// DecoratorHandler handle this. BaseDef is unnecessary.
36+
return undefined;
37+
}
38+
39+
let result: R3BaseRefDecoratorDetection|undefined = undefined;
40+
41+
this.reflector.getMembersOfClass(node).forEach(property => {
42+
const {decorators} = property;
43+
if (decorators) {
44+
for (const decorator of decorators) {
45+
const decoratorName = decorator.name;
46+
if (decoratorName === 'Input' && isAngularCore(decorator)) {
47+
result = result || {};
48+
const inputs = result.inputs = result.inputs || [];
49+
inputs.push({decorator, property});
50+
} else if (decoratorName === 'Output' && isAngularCore(decorator)) {
51+
result = result || {};
52+
const outputs = result.outputs = result.outputs || [];
53+
outputs.push({decorator, property});
54+
}
55+
}
56+
}
57+
});
58+
59+
return result;
60+
}
61+
62+
analyze(node: ts.ClassDeclaration, metadata: R3BaseRefDecoratorDetection):
63+
AnalysisOutput<R3BaseRefMetaData> {
64+
const analysis: R3BaseRefMetaData = {};
65+
if (metadata.inputs) {
66+
const inputs = analysis.inputs = {} as{[key: string]: string | [string, string]};
67+
metadata.inputs.forEach(({decorator, property}) => {
68+
const propName = property.name;
69+
const args = decorator.args;
70+
let value: string|[string, string];
71+
if (args && args.length > 0) {
72+
const resolvedValue = staticallyResolve(args[0], this.reflector, this.checker);
73+
if (typeof resolvedValue !== 'string') {
74+
throw new TypeError('Input alias does not resolve to a string value');
75+
}
76+
value = [resolvedValue, propName];
77+
} else {
78+
value = propName;
79+
}
80+
inputs[propName] = value;
81+
});
82+
}
83+
84+
if (metadata.outputs) {
85+
const outputs = analysis.outputs = {} as{[key: string]: string};
86+
metadata.outputs.forEach(({decorator, property}) => {
87+
const propName = property.name;
88+
const args = decorator.args;
89+
let value: string;
90+
if (args && args.length > 0) {
91+
const resolvedValue = staticallyResolve(args[0], this.reflector, this.checker);
92+
if (typeof resolvedValue !== 'string') {
93+
throw new TypeError('Output alias does not resolve to a string value');
94+
}
95+
value = resolvedValue;
96+
} else {
97+
value = propName;
98+
}
99+
outputs[propName] = value;
100+
});
101+
}
102+
103+
return {analysis};
104+
}
105+
106+
compile(node: ts.Declaration, analysis: R3BaseRefMetaData): CompileResult[]|CompileResult {
107+
const {expression, type} = compileBaseDefFromMetadata(analysis);
108+
109+
return {
110+
name: 'ngBaseDef',
111+
initializer: expression, type,
112+
statements: [],
113+
};
114+
}
115+
}
116+
117+
export interface R3BaseRefDecoratorDetection {
118+
inputs?: Array<{property: ClassMember, decorator: Decorator}>;
119+
outputs?: Array<{property: ClassMember, decorator: Decorator}>;
120+
}

packages/compiler-cli/src/ngtsc/annotations/src/component.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const EMPTY_MAP = new Map<string, Expression>();
2424
/**
2525
* `DecoratorHandler` which handles the `@Component` annotation.
2626
*/
27-
export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMetadata> {
27+
export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMetadata, Decorator> {
2828
constructor(
2929
private checker: ts.TypeChecker, private reflector: ReflectionHost,
3030
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean,
@@ -33,7 +33,10 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
3333
private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
3434

3535

36-
detect(decorators: Decorator[]): Decorator|undefined {
36+
detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined {
37+
if (!decorators) {
38+
return undefined;
39+
}
3740
return decorators.find(
3841
decorator => decorator.name === 'Component' && (this.isCore || isAngularCore(decorator)));
3942
}

packages/compiler-cli/src/ngtsc/annotations/src/directive.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@ import {getConstructorDependencies, isAngularCore, unwrapExpression, unwrapForwa
1818

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

21-
export class DirectiveDecoratorHandler implements DecoratorHandler<R3DirectiveMetadata> {
21+
export class DirectiveDecoratorHandler implements DecoratorHandler<R3DirectiveMetadata, Decorator> {
2222
constructor(
2323
private checker: ts.TypeChecker, private reflector: ReflectionHost,
2424
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}
2525

26-
detect(decorators: Decorator[]): Decorator|undefined {
26+
detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined {
27+
if (!decorators) {
28+
return undefined;
29+
}
2730
return decorators.find(
2831
decorator => decorator.name === 'Directive' && (this.isCore || isAngularCore(decorator)));
2932
}

packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@ import {getConstructorDependencies, isAngularCore} from './util';
1919
/**
2020
* Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler.
2121
*/
22-
export class InjectableDecoratorHandler implements DecoratorHandler<R3InjectableMetadata> {
22+
export class InjectableDecoratorHandler implements
23+
DecoratorHandler<R3InjectableMetadata, Decorator> {
2324
constructor(private reflector: ReflectionHost, private isCore: boolean) {}
2425

25-
detect(decorator: Decorator[]): Decorator|undefined {
26-
return decorator.find(
26+
detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined {
27+
if (!decorators) {
28+
return undefined;
29+
}
30+
return decorators.find(
2731
decorator => decorator.name === 'Injectable' && (this.isCore || isAngularCore(decorator)));
2832
}
2933

packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,15 @@ export interface NgModuleAnalysis {
2626
*
2727
* TODO(alxhub): handle injector side of things as well.
2828
*/
29-
export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalysis> {
29+
export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalysis, Decorator> {
3030
constructor(
3131
private checker: ts.TypeChecker, private reflector: ReflectionHost,
3232
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}
3333

34-
detect(decorators: Decorator[]): Decorator|undefined {
34+
detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined {
35+
if (!decorators) {
36+
return undefined;
37+
}
3538
return decorators.find(
3639
decorator => decorator.name === 'NgModule' && (this.isCore || isAngularCore(decorator)));
3740
}

packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
1616
import {SelectorScopeRegistry} from './selector_scope';
1717
import {getConstructorDependencies, isAngularCore, unwrapExpression} from './util';
1818

19-
export class PipeDecoratorHandler implements DecoratorHandler<R3PipeMetadata> {
19+
export class PipeDecoratorHandler implements DecoratorHandler<R3PipeMetadata, Decorator> {
2020
constructor(
2121
private checker: ts.TypeChecker, private reflector: ReflectionHost,
2222
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}
2323

24-
detect(decorator: Decorator[]): Decorator|undefined {
25-
return decorator.find(
24+
detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined {
25+
if (!decorators) {
26+
return undefined;
27+
}
28+
return decorators.find(
2629
decorator => decorator.name === 'Pipe' && (this.isCore || isAngularCore(decorator)));
2730
}
2831

packages/compiler-cli/src/ngtsc/program.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import * as ts from 'typescript';
1313
import * as api from '../transformers/api';
1414

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

170171
// Set up the IvyCompilation, which manages state for the Ivy transformer.
171172
const handlers = [
173+
new BaseDefDecoratorHandler(checker, this.reflector),
172174
new ComponentDecoratorHandler(
173175
checker, this.reflector, scopeRegistry, this.isCore, this.resourceLoader),
174176
new DirectiveDecoratorHandler(checker, this.reflector, scopeRegistry, this.isCore),

0 commit comments

Comments
 (0)