From cfed0c0cf1238e5ad931598ce04c32f8c9a75143 Mon Sep 17 00:00:00 2001 From: atscott Date: Mon, 12 Aug 2019 14:56:30 -0700 Subject: [PATCH] fix(ivy): Support selector-less directive as base classes (#32125) Following #31379, this adds support for directives without a selector to Ivy. PR Close #32125 --- .../ngcc/src/analysis/decoration_analyzer.ts | 6 +-- .../src/ngtsc/annotations/src/directive.ts | 36 +++++++++++----- .../src/ngtsc/annotations/src/ng_module.ts | 17 ++++++-- .../ngtsc/annotations/test/ng_module_spec.ts | 8 ++-- .../src/ngtsc/diagnostics/src/code.ts | 1 + .../src/ngtsc/incremental/src/state.ts | 14 +++++++ .../src/ngtsc/metadata/src/api.ts | 2 + .../src/ngtsc/metadata/src/dts.ts | 5 +++ .../src/ngtsc/metadata/src/registry.ts | 11 +++++ .../src/ngtsc/metadata/src/util.ts | 4 ++ packages/compiler-cli/src/ngtsc/program.ts | 6 +-- .../compiler-cli/src/ngtsc/scope/src/local.ts | 2 + .../compliance/r3_compiler_compliance_spec.ts | 38 ----------------- .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 42 +++++++++++++++---- .../compiler/src/render3/view/compiler.ts | 4 -- packages/core/src/render3/jit/directive.ts | 7 +++- packages/core/src/render3/jit/module.ts | 9 ++++ packages/core/test/linker/integration_spec.ts | 34 ++++++++++++++- .../core/testing/src/r3_test_bed_compiler.ts | 2 +- 19 files changed, 169 insertions(+), 79 deletions(-) diff --git a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts index 49ba5391e0..b8b50f21bf 100644 --- a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts @@ -86,9 +86,9 @@ export class DecorationAnalyzer { this.reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore, /* strictCtorDeps */ false), new NgModuleDecoratorHandler( - this.reflectionHost, this.evaluator, this.fullRegistry, this.scopeRegistry, - this.referencesRegistry, this.isCore, /* routeAnalyzer */ null, this.refEmitter, - NOOP_DEFAULT_IMPORT_RECORDER), + this.reflectionHost, this.evaluator, this.fullMetaReader, this.fullRegistry, + this.scopeRegistry, this.referencesRegistry, this.isCore, /* routeAnalyzer */ null, + this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER), new PipeDecoratorHandler( this.reflectionHost, this.evaluator, this.metaRegistry, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore), diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index b5e27c50a4..e3ccfc0fdd 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -72,6 +72,10 @@ export class DirectiveDecoratorHandler implements }); } + if (analysis && !analysis.selector) { + this.metaRegistry.registerAbstractDirective(node); + } + if (analysis === undefined) { return {}; } @@ -102,7 +106,10 @@ export class DirectiveDecoratorHandler implements } /** - * Helper function to extract metadata from a `Directive` or `Component`. + * Helper function to extract metadata from a `Directive` or `Component`. `Directive`s without a + * selector are allowed to be used for abstract base classes. These abstract directives should not + * appear in the declarations of an `NgModule` and additional verification is done when processing + * the module. */ export function extractDirectiveMetadata( clazz: ClassDeclaration, decorator: Decorator, reflector: ReflectionHost, @@ -112,17 +119,22 @@ export function extractDirectiveMetadata( metadata: R3DirectiveMetadata, decoratedElements: ClassMember[], }|undefined { - if (decorator.args === null || decorator.args.length !== 1) { + let directive: Map; + if (decorator.args === null || decorator.args.length === 0) { + directive = new Map(); + } else if (decorator.args.length !== 1) { throw new FatalDiagnosticError( ErrorCode.DECORATOR_ARITY_WRONG, decorator.node, `Incorrect number of arguments to @${decorator.name} decorator`); + } else { + const meta = unwrapExpression(decorator.args[0]); + if (!ts.isObjectLiteralExpression(meta)) { + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_ARG_NOT_LITERAL, meta, + `@${decorator.name} argument must be literal.`); + } + directive = reflectObjectLiteral(meta); } - const meta = unwrapExpression(decorator.args[0]); - if (!ts.isObjectLiteralExpression(meta)) { - throw new FatalDiagnosticError( - ErrorCode.DECORATOR_ARG_NOT_LITERAL, meta, `@${decorator.name} argument must be literal.`); - } - const directive = reflectObjectLiteral(meta); if (directive.has('jit')) { // The only allowed value is true, so there's no need to expand further. @@ -188,9 +200,11 @@ export function extractDirectiveMetadata( } // use default selector in case selector is an empty string selector = resolved === '' ? defaultSelector : resolved; - } - if (!selector) { - throw new Error(`Directive ${clazz.name.text} has no selector, please add it!`); + if (!selector) { + throw new FatalDiagnosticError( + ErrorCode.DIRECTIVE_MISSING_SELECTOR, expr, + `Directive ${clazz.name.text} has no selector, please add it!`); + } } const host = extractHostBindings(decoratedElements, evaluator, coreModule, directive); diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts index 18cceb30a6..84671854dd 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -11,7 +11,7 @@ import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {DefaultImportRecorder, Reference, ReferenceEmitter} from '../../imports'; -import {MetadataRegistry} from '../../metadata'; +import {MetadataReader, MetadataRegistry} from '../../metadata'; import {PartialEvaluator, ResolvedValue} from '../../partial_evaluator'; import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../reflection'; import {NgModuleRouteAnalyzer} from '../../routing'; @@ -40,7 +40,8 @@ export interface NgModuleAnalysis { export class NgModuleDecoratorHandler implements DecoratorHandler { constructor( private reflector: ReflectionHost, private evaluator: PartialEvaluator, - private metaRegistry: MetadataRegistry, private scopeRegistry: LocalModuleScopeRegistry, + private metaReader: MetadataReader, private metaRegistry: MetadataRegistry, + private scopeRegistry: LocalModuleScopeRegistry, private referencesRegistry: ReferencesRegistry, private isCore: boolean, private routeAnalyzer: NgModuleRouteAnalyzer|null, private refEmitter: ReferenceEmitter, private defaultImportRecorder: DefaultImportRecorder, private localeId?: string) {} @@ -210,15 +211,23 @@ export class NgModuleDecoratorHandler implements DecoratorHandler { const evaluator = new PartialEvaluator(reflectionHost, checker); const referencesRegistry = new NoopReferencesRegistry(); const metaRegistry = new LocalMetadataRegistry(); + const metaReader = new CompoundMetadataReader([metaRegistry]); const dtsReader = new DtsMetadataReader(checker, reflectionHost); const scopeRegistry = new LocalModuleScopeRegistry( metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null), @@ -66,8 +68,8 @@ runInEachFileSystem(() => { const refEmitter = new ReferenceEmitter([new LocalIdentifierStrategy()]); const handler = new NgModuleDecoratorHandler( - reflectionHost, evaluator, metaRegistry, scopeRegistry, referencesRegistry, false, null, - refEmitter, NOOP_DEFAULT_IMPORT_RECORDER); + reflectionHost, evaluator, metaReader, metaRegistry, scopeRegistry, referencesRegistry, + false, null, refEmitter, NOOP_DEFAULT_IMPORT_RECORDER); const TestModule = getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration); const detected = diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/src/code.ts b/packages/compiler-cli/src/ngtsc/diagnostics/src/code.ts index dad1acb5ad..313d0c9a98 100644 --- a/packages/compiler-cli/src/ngtsc/diagnostics/src/code.ts +++ b/packages/compiler-cli/src/ngtsc/diagnostics/src/code.ts @@ -24,6 +24,7 @@ export enum ErrorCode { COMPONENT_MISSING_TEMPLATE = 2001, PIPE_MISSING_NAME = 2002, PARAM_MISSING_TOKEN = 2003, + DIRECTIVE_MISSING_SELECTOR = 2004, SYMBOL_NOT_EXPORTED = 3001, SYMBOL_EXPORTED_UNDER_DIFFERENT_NAME = 3002, diff --git a/packages/compiler-cli/src/ngtsc/incremental/src/state.ts b/packages/compiler-cli/src/ngtsc/incremental/src/state.ts index b95983f642..ea82df1f20 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/src/state.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/src/state.ts @@ -93,6 +93,19 @@ export class IncrementalState implements DependencyTracker, MetadataReader, Meta metadata.ngModuleMeta.set(meta.ref.node, meta); } + isAbstractDirective(ref: Reference): boolean { + if (!this.metadata.has(ref.node.getSourceFile())) { + return false; + } + const metadata = this.metadata.get(ref.node.getSourceFile()) !; + return metadata.abstractDirectives.has(ref.node); + } + + registerAbstractDirective(clazz: ClassDeclaration): void { + const metadata = this.ensureMetadata(clazz.getSourceFile()); + metadata.abstractDirectives.add(clazz); + } + getDirectiveMetadata(ref: Reference): DirectiveMeta|null { if (!this.metadata.has(ref.node.getSourceFile())) { return null; @@ -187,6 +200,7 @@ class FileMetadata { /** A set of source files that this file depends upon. */ fileDependencies = new Set(); resourcePaths = new Set(); + abstractDirectives = new Set(); directiveMeta = new Map(); ngModuleMeta = new Map(); pipeMeta = new Map(); diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/api.ts b/packages/compiler-cli/src/ngtsc/metadata/src/api.ts index c8770b758d..422d0925dd 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/api.ts @@ -75,6 +75,7 @@ export interface PipeMeta { * or a registry. */ export interface MetadataReader { + isAbstractDirective(node: Reference): boolean; getDirectiveMetadata(node: Reference): DirectiveMeta|null; getNgModuleMetadata(node: Reference): NgModuleMeta|null; getPipeMetadata(node: Reference): PipeMeta|null; @@ -84,6 +85,7 @@ export interface MetadataReader { * Registers new metadata for directives, pipes, and modules. */ export interface MetadataRegistry { + registerAbstractDirective(clazz: ClassDeclaration): void; registerDirectiveMetadata(meta: DirectiveMeta): void; registerNgModuleMetadata(meta: NgModuleMeta): void; registerPipeMetadata(meta: PipeMeta): void; diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts b/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts index 28347d5460..76d4d83cac 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts @@ -21,6 +21,11 @@ import {extractDirectiveGuards, extractReferencesFromType, readStringArrayType, export class DtsMetadataReader implements MetadataReader { constructor(private checker: ts.TypeChecker, private reflector: ReflectionHost) {} + isAbstractDirective(ref: Reference): boolean { + const meta = this.getDirectiveMetadata(ref); + return meta !== null && meta.selector === null; + } + /** * Read the metadata from a class that has already been compiled somehow (either it's in a .d.ts * file, or in a .ts file with a handwritten definition). diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/registry.ts b/packages/compiler-cli/src/ngtsc/metadata/src/registry.ts index f834218efd..c4d7ec05e6 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/registry.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/registry.ts @@ -16,10 +16,14 @@ import {DirectiveMeta, MetadataReader, MetadataRegistry, NgModuleMeta, PipeMeta} * unit, which supports both reading and registering. */ export class LocalMetadataRegistry implements MetadataRegistry, MetadataReader { + private abstractDirectives = new Set(); private directives = new Map(); private ngModules = new Map(); private pipes = new Map(); + isAbstractDirective(ref: Reference): boolean { + return this.abstractDirectives.has(ref.node); + } getDirectiveMetadata(ref: Reference): DirectiveMeta|null { return this.directives.has(ref.node) ? this.directives.get(ref.node) ! : null; } @@ -30,6 +34,7 @@ export class LocalMetadataRegistry implements MetadataRegistry, MetadataReader { return this.pipes.has(ref.node) ? this.pipes.get(ref.node) ! : null; } + registerAbstractDirective(clazz: ClassDeclaration): void { this.abstractDirectives.add(clazz); } registerDirectiveMetadata(meta: DirectiveMeta): void { this.directives.set(meta.ref.node, meta); } registerNgModuleMetadata(meta: NgModuleMeta): void { this.ngModules.set(meta.ref.node, meta); } registerPipeMetadata(meta: PipeMeta): void { this.pipes.set(meta.ref.node, meta); } @@ -41,6 +46,12 @@ export class LocalMetadataRegistry implements MetadataRegistry, MetadataReader { export class CompoundMetadataRegistry implements MetadataRegistry { constructor(private registries: MetadataRegistry[]) {} + registerAbstractDirective(clazz: ClassDeclaration) { + for (const registry of this.registries) { + registry.registerAbstractDirective(clazz); + } + } + registerDirectiveMetadata(meta: DirectiveMeta): void { for (const registry of this.registries) { registry.registerDirectiveMetadata(meta); diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/util.ts b/packages/compiler-cli/src/ngtsc/metadata/src/util.ts index 31a39e9362..7f8a9d2678 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/util.ts @@ -125,6 +125,10 @@ function extractTemplateGuard(member: ClassMember): TemplateGuardMeta|null { export class CompoundMetadataReader implements MetadataReader { constructor(private readers: MetadataReader[]) {} + isAbstractDirective(node: Reference): boolean { + return this.readers.some(r => r.isAbstractDirective(node)); + } + getDirectiveMetadata(node: Reference>): DirectiveMeta|null { for (const reader of this.readers) { const meta = reader.getDirectiveMetadata(node); diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index aae2008225..c360bb52b8 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -514,9 +514,9 @@ export class NgtscProgram implements api.Program { this.reflector, this.defaultImportTracker, this.isCore, this.options.strictInjectionParameters || false), new NgModuleDecoratorHandler( - this.reflector, evaluator, metaRegistry, scopeRegistry, referencesRegistry, this.isCore, - this.routeAnalyzer, this.refEmitter, this.defaultImportTracker, - this.options.i18nInLocale), + this.reflector, evaluator, this.metaReader, metaRegistry, scopeRegistry, + referencesRegistry, this.isCore, this.routeAnalyzer, this.refEmitter, + this.defaultImportTracker, this.options.i18nInLocale), new PipeDecoratorHandler( this.reflector, evaluator, metaRegistry, this.defaultImportTracker, this.isCore), ]; diff --git a/packages/compiler-cli/src/ngtsc/scope/src/local.ts b/packages/compiler-cli/src/ngtsc/scope/src/local.ts index b29c0ff40a..6b1ca3a797 100644 --- a/packages/compiler-cli/src/ngtsc/scope/src/local.ts +++ b/packages/compiler-cli/src/ngtsc/scope/src/local.ts @@ -117,6 +117,8 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop } } + registerAbstractDirective(clazz: ClassDeclaration): void {} + registerDirectiveMetadata(directive: DirectiveMeta): void {} registerPipeMetadata(pipe: PipeMeta): void {} diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts index 0bd2c288d9..e11051b308 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -669,44 +669,6 @@ describe('compiler compliance', () => { source, EmptyOutletComponentDefinition, 'Incorrect EmptyOutletComponent.ngComponentDef'); }); - it('should not support directives without selector', () => { - const files = { - app: { - 'spec.ts': ` - import {Component, Directive, NgModule} from '@angular/core'; - - @Directive({}) - export class EmptyOutletDirective {} - - @NgModule({declarations: [EmptyOutletDirective]}) - export class MyModule{} - ` - } - }; - - expect(() => compile(files, angularFiles)) - .toThrowError('Directive EmptyOutletDirective has no selector, please add it!'); - }); - - it('should not support directives with empty selector', () => { - const files = { - app: { - 'spec.ts': ` - import {Component, Directive, NgModule} from '@angular/core'; - - @Directive({selector: ''}) - export class EmptyOutletDirective {} - - @NgModule({declarations: [EmptyOutletDirective]}) - export class MyModule{} - ` - } - }; - - expect(() => compile(files, angularFiles)) - .toThrowError('Directive EmptyOutletDirective has no selector, please add it!'); - }); - it('should not treat ElementRef, ViewContainerRef, or ChangeDetectorRef specially when injecting', () => { const files = { diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 50f0e7b02b..018353e709 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -1008,19 +1008,43 @@ runInEachFileSystem(os => { expect(jsContents).toContain('selectors: [["ng-component"]]'); }); - it('should throw if selector is missing in Directive decorator params', () => { - env.write('test.ts', ` - import {Directive} from '@angular/core'; + it('should allow directives with no selector that are not in NgModules', () => { + env.write('main.ts', ` + import {Directive} from '@angular/core'; - @Directive({ - inputs: ['a', 'b'] - }) - export class TestDir {} - `); + @Directive({}) + export class BaseDir {} + @Directive({}) + export abstract class AbstractBaseDir {} + + @Directive() + export abstract class EmptyDir {} + + @Directive({ + inputs: ['a', 'b'] + }) + export class TestDirWithInputs {} + `); + const errors = env.driveDiagnostics(); + expect(errors.length).toBe(0); + }); + + it('should not allow directives with no selector that are in NgModules', () => { + env.write('main.ts', ` + import {Directive, NgModule} from '@angular/core'; + + @Directive({}) + export class BaseDir {} + + @NgModule({ + declarations: [BaseDir], + }) + export class MyModule {} + `); const errors = env.driveDiagnostics(); expect(trim(errors[0].messageText as string)) - .toContain('Directive TestDir has no selector, please add it!'); + .toContain('Directive BaseDir has no selector, please add it!'); }); it('should throw if Directive selector is an empty string', () => { diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index f065c63a58..619abc0739 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -132,10 +132,6 @@ export function compileDirectiveFromMetadata( addFeatures(definitionMap, meta); const expression = o.importExpr(R3.defineDirective).callFn([definitionMap.toLiteralMap()]); - if (!meta.selector) { - throw new Error(`Directive ${meta.name} has no selector, please add it!`); - } - const type = createTypeForDef(meta, R3.DirectiveDefWithMeta); return {expression, type, statements}; } diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index 0ba5ca7296..48b9312479 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -117,7 +117,7 @@ function hasSelectorScope(component: Type): component is Type& * In the event that compilation is not immediate, `compileDirective` will return a `Promise` which * will resolve when compilation completes and the directive becomes usable. */ -export function compileDirective(type: Type, directive: Directive): void { +export function compileDirective(type: Type, directive: Directive | null): void { let ngDirectiveDef: any = null; Object.defineProperty(type, NG_DIRECTIVE_DEF, { get: () => { @@ -125,7 +125,10 @@ export function compileDirective(type: Type, directive: Directive): void { const name = type && type.name; const sourceMapUrl = `ng:///${name}/ngDirectiveDef.js`; const compiler = getCompilerFacade(); - const facade = directiveMetadata(type as ComponentType, directive); + // `directive` can be null in the case of abstract directives as a base class + // that use `@Directive()` with no selector. In that case, pass empty object to the + // `directiveMetadata` function instead of null. + const facade = directiveMetadata(type as ComponentType, directive || {}); facade.typeSourceSpan = compiler.createParseSourceSpan('Directive', name, sourceMapUrl); if (facade.usesInheritance) { addBaseDefToUndecoratedParents(type); diff --git a/packages/core/src/render3/jit/module.ts b/packages/core/src/render3/jit/module.ts index 4c7aa17e60..e4c8357a02 100644 --- a/packages/core/src/render3/jit/module.ts +++ b/packages/core/src/render3/jit/module.ts @@ -185,6 +185,7 @@ function verifySemanticsOfNgModuleDef( }); const exports = maybeUnwrapFn(ngModuleDef.exports); declarations.forEach(verifyDeclarationsHaveDefinitions); + declarations.forEach(verifyDirectivesHaveSelector); const combinedDeclarations: Type[] = [ ...declarations.map(resolveForwardRef), ...flatten(imports.map(computeCombinedExports)).map(resolveForwardRef), @@ -220,6 +221,14 @@ function verifySemanticsOfNgModuleDef( } } + function verifyDirectivesHaveSelector(type: Type): void { + type = resolveForwardRef(type); + const def = getDirectiveDef(type); + if (!getComponentDef(type) && def && def.selectors.length == 0) { + errors.push(`Directive ${stringifyForError(type)} has no selector, please add it!`); + } + } + function verifyExportsAreDeclaredOrReExported(type: Type) { type = resolveForwardRef(type); const kind = getComponentDef(type) && 'component' || getDirectiveDef(type) && 'directive' || diff --git a/packages/core/test/linker/integration_spec.ts b/packages/core/test/linker/integration_spec.ts index b6bb694786..6cd2df3da7 100644 --- a/packages/core/test/linker/integration_spec.ts +++ b/packages/core/test/linker/integration_spec.ts @@ -1411,7 +1411,7 @@ function declareTests(config?: {useJit: boolean}) { expect(getDOM().querySelectorAll(fixture.nativeElement, 'script').length).toEqual(0); }); - it('should throw when using directives without selector', () => { + it('should throw when using directives without selector in NgModule declarations', () => { @Directive({}) class SomeDirective { } @@ -1425,6 +1425,38 @@ function declareTests(config?: {useJit: boolean}) { .toThrowError(`Directive ${stringify(SomeDirective)} has no selector, please add it!`); }); + it('should not throw when using directives without selector as base class not in declarations', + () => { + @Directive({}) + abstract class Base { + constructor(readonly injector: Injector) {} + } + + @Directive() + abstract class EmptyDir { + } + + @Directive({inputs: ['a', 'b']}) + class TestDirWithInputs { + } + + @Component({selector: 'comp', template: ''}) + class SomeComponent extends Base { + } + + @Component({selector: 'comp2', template: ''}) + class SomeComponent2 extends EmptyDir { + } + + @Component({selector: 'comp3', template: ''}) + class SomeComponent3 extends TestDirWithInputs { + } + + TestBed.configureTestingModule( + {declarations: [MyComp, SomeComponent, SomeComponent2, SomeComponent3]}); + expect(() => TestBed.createComponent(MyComp)).not.toThrowError(); + }); + it('should throw when using directives with empty string selector', () => { @Directive({selector: ''}) class SomeDirective { diff --git a/packages/core/testing/src/r3_test_bed_compiler.ts b/packages/core/testing/src/r3_test_bed_compiler.ts index c18d5a7451..a0e2eeeb63 100644 --- a/packages/core/testing/src/r3_test_bed_compiler.ts +++ b/packages/core/testing/src/r3_test_bed_compiler.ts @@ -299,7 +299,7 @@ export class R3TestBedCompiler { this.pendingComponents.clear(); this.pendingDirectives.forEach(declaration => { - const metadata = this.resolvers.directive.resolve(declaration) !; + const metadata = this.resolvers.directive.resolve(declaration); this.maybeStoreNgDef(NG_DIRECTIVE_DEF, declaration); compileDirective(declaration, metadata); });