feat(ivy): support @Injectable on already decorated classes (#28523)

Previously, ngtsc would throw an error if two decorators were matched on
the same class simultaneously. However, @Injectable is a special case, and
it appears frequently on component, directive, and pipe classes. For pipes
in particular, it's a common pattern to treat the pipe class also as an
injectable service.

ngtsc actually lacked the capability to compile multiple matching
decorators on a class, so this commit adds support for that. Decorator
handlers (and thus the decorators they match) are classified into three
categories: PRIMARY, SHARED, and WEAK.

PRIMARY handlers compile decorators that cannot coexist with other primary
decorators. The handlers for Component, Directive, Pipe, and NgModule are
marked as PRIMARY. A class may only have one decorator from this group.

SHARED handlers compile decorators that can coexist with others. Injectable
is the only decorator in this category, meaning it's valid to put an
@Injectable decorator on a previously decorated class.

WEAK handlers behave like SHARED, but are dropped if any non-WEAK handler
matches a class. The handler which compiles ngBaseDef is WEAK, since
ngBaseDef is only needed if a class doesn't otherwise have a decorator.

Tests are added to validate that @Injectable can coexist with the other
decorators and that an error is generated when mixing the primaries.

PR Close #28523
This commit is contained in:
Alex Rickabaugh
2019-02-01 12:23:21 -08:00
committed by Misko Hevery
parent d2742cf473
commit 99d8582882
15 changed files with 466 additions and 123 deletions

View File

@ -14,7 +14,7 @@ import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHa
import {CycleAnalyzer, ImportGraph} from '../../../ngtsc/cycles';
import {ModuleResolver, TsReferenceResolver} from '../../../ngtsc/imports';
import {PartialEvaluator} from '../../../ngtsc/partial_evaluator';
import {CompileResult, DecoratorHandler} from '../../../ngtsc/transform';
import {CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../ngtsc/transform';
import {DecoratedClass} from '../host/decorated_class';
import {NgccReflectionHost} from '../host/ngcc_host';
import {isDefined} from '../utils';
@ -26,8 +26,7 @@ export interface AnalyzedFile {
export interface AnalyzedClass extends DecoratedClass {
diagnostics?: ts.Diagnostic[];
handler: DecoratorHandler<any, any>;
analysis: any;
matches: {handler: DecoratorHandler<any, any>; analysis: any;}[];
}
export interface CompiledClass extends AnalyzedClass { compilation: CompileResult[]; }
@ -43,7 +42,7 @@ export const DecorationAnalyses = Map;
export interface MatchingHandler<A, M> {
handler: DecoratorHandler<A, M>;
match: M;
detected: M;
}
/**
@ -118,21 +117,52 @@ export class DecorationAnalyzer {
protected analyzeClass(clazz: DecoratedClass): AnalyzedClass|null {
const matchingHandlers = this.handlers
.map(handler => {
const match =
const detected =
handler.detect(clazz.declaration, clazz.decorators);
return {handler, match};
return {handler, detected};
})
.filter(isMatchingHandler);
if (matchingHandlers.length > 1) {
throw new Error('TODO.Diagnostic: Class has multiple Angular decorators.');
}
if (matchingHandlers.length === 0) {
return null;
}
const {handler, match} = matchingHandlers[0];
const {analysis, diagnostics} = handler.analyze(clazz.declaration, match);
return {...clazz, handler, analysis, diagnostics};
const detections: {handler: DecoratorHandler<any, any>, detected: DetectResult<any>}[] = [];
let hasWeakHandler: boolean = false;
let hasNonWeakHandler: boolean = false;
let hasPrimaryHandler: boolean = false;
for (const {handler, detected} of matchingHandlers) {
if (hasNonWeakHandler && handler.precedence === HandlerPrecedence.WEAK) {
continue;
} else if (hasWeakHandler && handler.precedence !== HandlerPrecedence.WEAK) {
// Clear all the WEAK handlers from the list of matches.
detections.length = 0;
}
if (hasPrimaryHandler && handler.precedence === HandlerPrecedence.PRIMARY) {
throw new Error(`TODO.Diagnostic: Class has multiple incompatible Angular decorators.`);
}
detections.push({handler, detected});
if (handler.precedence === HandlerPrecedence.WEAK) {
hasWeakHandler = true;
} else if (handler.precedence === HandlerPrecedence.SHARED) {
hasNonWeakHandler = true;
} else if (handler.precedence === HandlerPrecedence.PRIMARY) {
hasNonWeakHandler = true;
hasPrimaryHandler = true;
}
}
const matches: {handler: DecoratorHandler<any, any>, analysis: any}[] = [];
const allDiagnostics: ts.Diagnostic[] = [];
for (const {handler, detected} of detections) {
const {analysis, diagnostics} = handler.analyze(clazz.declaration, detected.metadata);
if (diagnostics !== undefined) {
allDiagnostics.push(...diagnostics);
}
matches.push({handler, analysis});
}
return {...clazz, matches, diagnostics: allDiagnostics.length > 0 ? allDiagnostics : undefined};
}
protected compileFile(analyzedFile: AnalyzedFile): CompiledFile {
@ -145,15 +175,20 @@ export class DecorationAnalyzer {
}
protected compileClass(clazz: AnalyzedClass, constantPool: ConstantPool): CompileResult[] {
let compilation = clazz.handler.compile(clazz.declaration, clazz.analysis, constantPool);
if (!Array.isArray(compilation)) {
compilation = [compilation];
const compilations: CompileResult[] = [];
for (const {handler, analysis} of clazz.matches) {
const result = handler.compile(clazz.declaration, analysis, constantPool);
if (Array.isArray(result)) {
compilations.push(...result);
} else {
compilations.push(result);
}
}
return compilation;
return compilations;
}
}
function isMatchingHandler<A, M>(handler: Partial<MatchingHandler<A, M>>):
handler is MatchingHandler<A, M> {
return !!handler.match;
return !!handler.detected;
}

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript';
import {Decorator} from '../../../ngtsc/reflection';
import {DecoratorHandler} from '../../../ngtsc/transform';
import {DecoratorHandler, DetectResult} from '../../../ngtsc/transform';
import {DecorationAnalyses, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
@ -62,15 +62,24 @@ function createTestHandler() {
'compile',
]);
// Only detect the Component decorator
handler.detect.and.callFake((node: ts.Declaration, decorators: Decorator[]) => {
if (!decorators) {
return undefined;
}
return decorators.find(d => d.name === 'Component');
});
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: null})));
((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;