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:

committed by
Misko Hevery

parent
d2742cf473
commit
99d8582882
@ -11,7 +11,7 @@ import * as ts from 'typescript';
|
||||
|
||||
import {PartialEvaluator} from '../../partial_evaluator';
|
||||
import {ClassMember, Decorator, ReflectionHost} from '../../reflection';
|
||||
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
|
||||
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
|
||||
import {isAngularCore} from './util';
|
||||
|
||||
function containsNgTopLevelDecorator(decorators: Decorator[] | null): boolean {
|
||||
@ -28,8 +28,10 @@ export class BaseDefDecoratorHandler implements
|
||||
DecoratorHandler<R3BaseRefMetaData, R3BaseRefDecoratorDetection> {
|
||||
constructor(private reflector: ReflectionHost, private evaluator: PartialEvaluator) {}
|
||||
|
||||
detect(node: ts.ClassDeclaration, decorators: Decorator[]|null): R3BaseRefDecoratorDetection
|
||||
|undefined {
|
||||
readonly precedence = HandlerPrecedence.WEAK;
|
||||
|
||||
detect(node: ts.ClassDeclaration, decorators: Decorator[]|null):
|
||||
DetectResult<R3BaseRefDecoratorDetection>|undefined {
|
||||
if (containsNgTopLevelDecorator(decorators)) {
|
||||
// If the class is already decorated by @Component or @Directive let that
|
||||
// DecoratorHandler handle this. BaseDef is unnecessary.
|
||||
@ -56,7 +58,14 @@ export class BaseDefDecoratorHandler implements
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
if (result !== undefined) {
|
||||
return {
|
||||
metadata: result,
|
||||
trigger: null,
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
analyze(node: ts.ClassDeclaration, metadata: R3BaseRefDecoratorDetection):
|
||||
|
@ -12,10 +12,10 @@ import * as ts from 'typescript';
|
||||
|
||||
import {CycleAnalyzer} from '../../cycles';
|
||||
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
||||
import {ModuleResolver, Reference, ResolvedReference} from '../../imports';
|
||||
import {ModuleResolver, ResolvedReference} from '../../imports';
|
||||
import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
|
||||
import {Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection';
|
||||
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
|
||||
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
|
||||
import {TypeCheckContext} from '../../typecheck';
|
||||
import {tsSourceMapBug29300Fixed} from '../../util/src/ts_source_map_bug_29300';
|
||||
|
||||
@ -49,13 +49,22 @@ export class ComponentDecoratorHandler implements
|
||||
private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
|
||||
private elementSchemaRegistry = new DomElementSchemaRegistry();
|
||||
|
||||
readonly precedence = HandlerPrecedence.PRIMARY;
|
||||
|
||||
detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined {
|
||||
detect(node: ts.Declaration, decorators: Decorator[]|null): DetectResult<Decorator>|undefined {
|
||||
if (!decorators) {
|
||||
return undefined;
|
||||
}
|
||||
return decorators.find(
|
||||
const decorator = decorators.find(
|
||||
decorator => decorator.name === 'Component' && (this.isCore || isAngularCore(decorator)));
|
||||
if (decorator !== undefined) {
|
||||
return {
|
||||
trigger: decorator.node,
|
||||
metadata: decorator,
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
preanalyze(node: ts.ClassDeclaration, decorator: Decorator): Promise<void>|undefined {
|
||||
|
@ -13,7 +13,7 @@ import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
||||
import {Reference, ResolvedReference} from '../../imports';
|
||||
import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
|
||||
import {ClassMember, ClassMemberKind, Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection';
|
||||
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
|
||||
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
|
||||
|
||||
import {generateSetClassMetadataCall} from './metadata';
|
||||
import {SelectorScopeRegistry} from './selector_scope';
|
||||
@ -31,12 +31,22 @@ export class DirectiveDecoratorHandler implements
|
||||
private reflector: ReflectionHost, private evaluator: PartialEvaluator,
|
||||
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}
|
||||
|
||||
detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined {
|
||||
readonly precedence = HandlerPrecedence.PRIMARY;
|
||||
|
||||
detect(node: ts.Declaration, decorators: Decorator[]|null): DetectResult<Decorator>|undefined {
|
||||
if (!decorators) {
|
||||
return undefined;
|
||||
}
|
||||
return decorators.find(
|
||||
const decorator = decorators.find(
|
||||
decorator => decorator.name === 'Directive' && (this.isCore || isAngularCore(decorator)));
|
||||
if (decorator !== undefined) {
|
||||
return {
|
||||
trigger: decorator.node,
|
||||
metadata: decorator,
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<DirectiveHandlerData> {
|
||||
|
@ -11,7 +11,7 @@ import * as ts from 'typescript';
|
||||
|
||||
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
||||
import {Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
|
||||
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
|
||||
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
|
||||
|
||||
import {generateSetClassMetadataCall} from './metadata';
|
||||
import {getConstructorDependencies, getValidConstructorDependencies, isAngularCore, validateConstructorDependencies} from './util';
|
||||
@ -30,12 +30,22 @@ export class InjectableDecoratorHandler implements
|
||||
private reflector: ReflectionHost, private isCore: boolean, private strictCtorDeps: boolean) {
|
||||
}
|
||||
|
||||
detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined {
|
||||
readonly precedence = HandlerPrecedence.SHARED;
|
||||
|
||||
detect(node: ts.Declaration, decorators: Decorator[]|null): DetectResult<Decorator>|undefined {
|
||||
if (!decorators) {
|
||||
return undefined;
|
||||
}
|
||||
return decorators.find(
|
||||
const decorator = decorators.find(
|
||||
decorator => decorator.name === 'Injectable' && (this.isCore || isAngularCore(decorator)));
|
||||
if (decorator !== undefined) {
|
||||
return {
|
||||
trigger: decorator.node,
|
||||
metadata: decorator,
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<InjectableHandlerData> {
|
||||
|
@ -14,7 +14,7 @@ import {Reference, ResolvedReference} from '../../imports';
|
||||
import {PartialEvaluator, ResolvedValue} from '../../partial_evaluator';
|
||||
import {Decorator, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../reflection';
|
||||
import {NgModuleRouteAnalyzer} from '../../routing';
|
||||
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
|
||||
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
|
||||
|
||||
import {generateSetClassMetadataCall} from './metadata';
|
||||
import {ReferencesRegistry} from './references_registry';
|
||||
@ -39,12 +39,22 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
|
||||
private scopeRegistry: SelectorScopeRegistry, private referencesRegistry: ReferencesRegistry,
|
||||
private isCore: boolean, private routeAnalyzer: NgModuleRouteAnalyzer|null) {}
|
||||
|
||||
detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined {
|
||||
readonly precedence = HandlerPrecedence.PRIMARY;
|
||||
|
||||
detect(node: ts.Declaration, decorators: Decorator[]|null): DetectResult<Decorator>|undefined {
|
||||
if (!decorators) {
|
||||
return undefined;
|
||||
}
|
||||
return decorators.find(
|
||||
const decorator = decorators.find(
|
||||
decorator => decorator.name === 'NgModule' && (this.isCore || isAngularCore(decorator)));
|
||||
if (decorator !== undefined) {
|
||||
return {
|
||||
trigger: decorator.node,
|
||||
metadata: decorator,
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<NgModuleAnalysis> {
|
||||
|
@ -12,7 +12,7 @@ import * as ts from 'typescript';
|
||||
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
||||
import {PartialEvaluator} from '../../partial_evaluator';
|
||||
import {Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
|
||||
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
|
||||
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
|
||||
|
||||
import {generateSetClassMetadataCall} from './metadata';
|
||||
import {SelectorScopeRegistry} from './selector_scope';
|
||||
@ -28,12 +28,22 @@ export class PipeDecoratorHandler implements DecoratorHandler<PipeHandlerData, D
|
||||
private reflector: ReflectionHost, private evaluator: PartialEvaluator,
|
||||
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}
|
||||
|
||||
detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined {
|
||||
readonly precedence = HandlerPrecedence.PRIMARY;
|
||||
|
||||
detect(node: ts.Declaration, decorators: Decorator[]|null): DetectResult<Decorator>|undefined {
|
||||
if (!decorators) {
|
||||
return undefined;
|
||||
}
|
||||
return decorators.find(
|
||||
const decorator = decorators.find(
|
||||
decorator => decorator.name === 'Pipe' && (this.isCore || isAngularCore(decorator)));
|
||||
if (decorator !== undefined) {
|
||||
return {
|
||||
trigger: decorator.node,
|
||||
metadata: decorator,
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
analyze(clazz: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<PipeHandlerData> {
|
||||
|
@ -59,7 +59,7 @@ describe('ComponentDecoratorHandler', () => {
|
||||
return fail('Failed to recognize @Component');
|
||||
}
|
||||
try {
|
||||
handler.analyze(TestCmp, detected);
|
||||
handler.analyze(TestCmp, detected.metadata);
|
||||
return fail('Analysis should have failed');
|
||||
} catch (err) {
|
||||
if (!(err instanceof FatalDiagnosticError)) {
|
||||
@ -68,7 +68,7 @@ describe('ComponentDecoratorHandler', () => {
|
||||
const diag = err.toDiagnostic();
|
||||
expect(diag.code).toEqual(ivyCode(ErrorCode.DECORATOR_ARG_NOT_LITERAL));
|
||||
expect(diag.file.fileName.endsWith('entry.ts')).toBe(true);
|
||||
expect(diag.start).toBe(detected.args ![0].getStart());
|
||||
expect(diag.start).toBe(detected.metadata.args ![0].getStart());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user