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
This commit is contained in:
		
							parent
							
								
									bb3c684b98
								
							
						
					
					
						commit
						cfed0c0cf1
					
				| @ -86,9 +86,9 @@ export class DecorationAnalyzer { | |||||||
|         this.reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore, |         this.reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore, | ||||||
|         /* strictCtorDeps */ false), |         /* strictCtorDeps */ false), | ||||||
|     new NgModuleDecoratorHandler( |     new NgModuleDecoratorHandler( | ||||||
|         this.reflectionHost, this.evaluator, this.fullRegistry, this.scopeRegistry, |         this.reflectionHost, this.evaluator, this.fullMetaReader, this.fullRegistry, | ||||||
|         this.referencesRegistry, this.isCore, /* routeAnalyzer */ null, this.refEmitter, |         this.scopeRegistry, this.referencesRegistry, this.isCore, /* routeAnalyzer */ null, | ||||||
|         NOOP_DEFAULT_IMPORT_RECORDER), |         this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER), | ||||||
|     new PipeDecoratorHandler( |     new PipeDecoratorHandler( | ||||||
|         this.reflectionHost, this.evaluator, this.metaRegistry, NOOP_DEFAULT_IMPORT_RECORDER, |         this.reflectionHost, this.evaluator, this.metaRegistry, NOOP_DEFAULT_IMPORT_RECORDER, | ||||||
|         this.isCore), |         this.isCore), | ||||||
|  | |||||||
| @ -72,6 +72,10 @@ export class DirectiveDecoratorHandler implements | |||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     if (analysis && !analysis.selector) { | ||||||
|  |       this.metaRegistry.registerAbstractDirective(node); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     if (analysis === undefined) { |     if (analysis === undefined) { | ||||||
|       return {}; |       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( | export function extractDirectiveMetadata( | ||||||
|     clazz: ClassDeclaration, decorator: Decorator, reflector: ReflectionHost, |     clazz: ClassDeclaration, decorator: Decorator, reflector: ReflectionHost, | ||||||
| @ -112,17 +119,22 @@ export function extractDirectiveMetadata( | |||||||
|   metadata: R3DirectiveMetadata, |   metadata: R3DirectiveMetadata, | ||||||
|   decoratedElements: ClassMember[], |   decoratedElements: ClassMember[], | ||||||
| }|undefined { | }|undefined { | ||||||
|   if (decorator.args === null || decorator.args.length !== 1) { |   let directive: Map<string, ts.Expression>; | ||||||
|  |   if (decorator.args === null || decorator.args.length === 0) { | ||||||
|  |     directive = new Map<string, ts.Expression>(); | ||||||
|  |   } else if (decorator.args.length !== 1) { | ||||||
|     throw new FatalDiagnosticError( |     throw new FatalDiagnosticError( | ||||||
|         ErrorCode.DECORATOR_ARITY_WRONG, decorator.node, |         ErrorCode.DECORATOR_ARITY_WRONG, decorator.node, | ||||||
|         `Incorrect number of arguments to @${decorator.name} decorator`); |         `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')) { |   if (directive.has('jit')) { | ||||||
|     // The only allowed value is true, so there's no need to expand further.
 |     // 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
 |     // use default selector in case selector is an empty string
 | ||||||
|     selector = resolved === '' ? defaultSelector : resolved; |     selector = resolved === '' ? defaultSelector : resolved; | ||||||
|   } |     if (!selector) { | ||||||
|   if (!selector) { |       throw new FatalDiagnosticError( | ||||||
|     throw new Error(`Directive ${clazz.name.text} has no selector, please add it!`); |           ErrorCode.DIRECTIVE_MISSING_SELECTOR, expr, | ||||||
|  |           `Directive ${clazz.name.text} has no selector, please add it!`); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const host = extractHostBindings(decoratedElements, evaluator, coreModule, directive); |   const host = extractHostBindings(decoratedElements, evaluator, coreModule, directive); | ||||||
|  | |||||||
| @ -11,7 +11,7 @@ import * as ts from 'typescript'; | |||||||
| 
 | 
 | ||||||
| import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; | import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; | ||||||
| import {DefaultImportRecorder, Reference, ReferenceEmitter} from '../../imports'; | import {DefaultImportRecorder, Reference, ReferenceEmitter} from '../../imports'; | ||||||
| import {MetadataRegistry} from '../../metadata'; | import {MetadataReader, MetadataRegistry} from '../../metadata'; | ||||||
| import {PartialEvaluator, ResolvedValue} from '../../partial_evaluator'; | import {PartialEvaluator, ResolvedValue} from '../../partial_evaluator'; | ||||||
| import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../reflection'; | import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../reflection'; | ||||||
| import {NgModuleRouteAnalyzer} from '../../routing'; | import {NgModuleRouteAnalyzer} from '../../routing'; | ||||||
| @ -40,7 +40,8 @@ export interface NgModuleAnalysis { | |||||||
| export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalysis, Decorator> { | export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalysis, Decorator> { | ||||||
|   constructor( |   constructor( | ||||||
|       private reflector: ReflectionHost, private evaluator: PartialEvaluator, |       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 referencesRegistry: ReferencesRegistry, private isCore: boolean, | ||||||
|       private routeAnalyzer: NgModuleRouteAnalyzer|null, private refEmitter: ReferenceEmitter, |       private routeAnalyzer: NgModuleRouteAnalyzer|null, private refEmitter: ReferenceEmitter, | ||||||
|       private defaultImportRecorder: DefaultImportRecorder, private localeId?: string) {} |       private defaultImportRecorder: DefaultImportRecorder, private localeId?: string) {} | ||||||
| @ -210,15 +211,23 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys | |||||||
|     const scope = this.scopeRegistry.getScopeOfModule(node); |     const scope = this.scopeRegistry.getScopeOfModule(node); | ||||||
|     const diagnostics = this.scopeRegistry.getDiagnosticsOfModule(node) || undefined; |     const diagnostics = this.scopeRegistry.getDiagnosticsOfModule(node) || undefined; | ||||||
| 
 | 
 | ||||||
|     // Using the scope information, extend the injector's imports using the modules that are
 |  | ||||||
|     // specified as module exports.
 |  | ||||||
|     if (scope !== null) { |     if (scope !== null) { | ||||||
|  |       // Using the scope information, extend the injector's imports using the modules that are
 | ||||||
|  |       // specified as module exports.
 | ||||||
|       const context = getSourceFile(node); |       const context = getSourceFile(node); | ||||||
|       for (const exportRef of analysis.exports) { |       for (const exportRef of analysis.exports) { | ||||||
|         if (isNgModule(exportRef.node, scope.compilation)) { |         if (isNgModule(exportRef.node, scope.compilation)) { | ||||||
|           analysis.ngInjectorDef.imports.push(this.refEmitter.emit(exportRef, context)); |           analysis.ngInjectorDef.imports.push(this.refEmitter.emit(exportRef, context)); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  | 
 | ||||||
|  |       for (const decl of analysis.declarations) { | ||||||
|  |         if (this.metaReader.isAbstractDirective(decl)) { | ||||||
|  |           throw new FatalDiagnosticError( | ||||||
|  |               ErrorCode.DIRECTIVE_MISSING_SELECTOR, decl.node, | ||||||
|  |               `Directive ${decl.node.name.text} has no selector, please add it!`); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (scope === null || scope.reexports === null) { |     if (scope === null || scope.reexports === null) { | ||||||
|  | |||||||
| @ -8,10 +8,11 @@ | |||||||
| import {WrappedNodeExpr} from '@angular/compiler'; | import {WrappedNodeExpr} from '@angular/compiler'; | ||||||
| import {R3Reference} from '@angular/compiler/src/compiler'; | import {R3Reference} from '@angular/compiler/src/compiler'; | ||||||
| import * as ts from 'typescript'; | import * as ts from 'typescript'; | ||||||
|  | 
 | ||||||
| import {absoluteFrom} from '../../file_system'; | import {absoluteFrom} from '../../file_system'; | ||||||
| import {runInEachFileSystem} from '../../file_system/testing'; | import {runInEachFileSystem} from '../../file_system/testing'; | ||||||
| import {LocalIdentifierStrategy, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports'; | import {LocalIdentifierStrategy, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports'; | ||||||
| import {DtsMetadataReader, LocalMetadataRegistry} from '../../metadata'; | import {CompoundMetadataReader, DtsMetadataReader, LocalMetadataRegistry} from '../../metadata'; | ||||||
| import {PartialEvaluator} from '../../partial_evaluator'; | import {PartialEvaluator} from '../../partial_evaluator'; | ||||||
| import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection'; | import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection'; | ||||||
| import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope'; | import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope'; | ||||||
| @ -59,6 +60,7 @@ runInEachFileSystem(() => { | |||||||
|       const evaluator = new PartialEvaluator(reflectionHost, checker); |       const evaluator = new PartialEvaluator(reflectionHost, checker); | ||||||
|       const referencesRegistry = new NoopReferencesRegistry(); |       const referencesRegistry = new NoopReferencesRegistry(); | ||||||
|       const metaRegistry = new LocalMetadataRegistry(); |       const metaRegistry = new LocalMetadataRegistry(); | ||||||
|  |       const metaReader = new CompoundMetadataReader([metaRegistry]); | ||||||
|       const dtsReader = new DtsMetadataReader(checker, reflectionHost); |       const dtsReader = new DtsMetadataReader(checker, reflectionHost); | ||||||
|       const scopeRegistry = new LocalModuleScopeRegistry( |       const scopeRegistry = new LocalModuleScopeRegistry( | ||||||
|           metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null), |           metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null), | ||||||
| @ -66,8 +68,8 @@ runInEachFileSystem(() => { | |||||||
|       const refEmitter = new ReferenceEmitter([new LocalIdentifierStrategy()]); |       const refEmitter = new ReferenceEmitter([new LocalIdentifierStrategy()]); | ||||||
| 
 | 
 | ||||||
|       const handler = new NgModuleDecoratorHandler( |       const handler = new NgModuleDecoratorHandler( | ||||||
|           reflectionHost, evaluator, metaRegistry, scopeRegistry, referencesRegistry, false, null, |           reflectionHost, evaluator, metaReader, metaRegistry, scopeRegistry, referencesRegistry, | ||||||
|           refEmitter, NOOP_DEFAULT_IMPORT_RECORDER); |           false, null, refEmitter, NOOP_DEFAULT_IMPORT_RECORDER); | ||||||
|       const TestModule = |       const TestModule = | ||||||
|           getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration); |           getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration); | ||||||
|       const detected = |       const detected = | ||||||
|  | |||||||
| @ -24,6 +24,7 @@ export enum ErrorCode { | |||||||
|   COMPONENT_MISSING_TEMPLATE = 2001, |   COMPONENT_MISSING_TEMPLATE = 2001, | ||||||
|   PIPE_MISSING_NAME = 2002, |   PIPE_MISSING_NAME = 2002, | ||||||
|   PARAM_MISSING_TOKEN = 2003, |   PARAM_MISSING_TOKEN = 2003, | ||||||
|  |   DIRECTIVE_MISSING_SELECTOR = 2004, | ||||||
| 
 | 
 | ||||||
|   SYMBOL_NOT_EXPORTED = 3001, |   SYMBOL_NOT_EXPORTED = 3001, | ||||||
|   SYMBOL_EXPORTED_UNDER_DIFFERENT_NAME = 3002, |   SYMBOL_EXPORTED_UNDER_DIFFERENT_NAME = 3002, | ||||||
|  | |||||||
| @ -93,6 +93,19 @@ export class IncrementalState implements DependencyTracker, MetadataReader, Meta | |||||||
|     metadata.ngModuleMeta.set(meta.ref.node, meta); |     metadata.ngModuleMeta.set(meta.ref.node, meta); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   isAbstractDirective(ref: Reference<ClassDeclaration>): 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<ClassDeclaration>): DirectiveMeta|null { |   getDirectiveMetadata(ref: Reference<ClassDeclaration>): DirectiveMeta|null { | ||||||
|     if (!this.metadata.has(ref.node.getSourceFile())) { |     if (!this.metadata.has(ref.node.getSourceFile())) { | ||||||
|       return null; |       return null; | ||||||
| @ -187,6 +200,7 @@ class FileMetadata { | |||||||
|   /** A set of source files that this file depends upon. */ |   /** A set of source files that this file depends upon. */ | ||||||
|   fileDependencies = new Set<ts.SourceFile>(); |   fileDependencies = new Set<ts.SourceFile>(); | ||||||
|   resourcePaths = new Set<string>(); |   resourcePaths = new Set<string>(); | ||||||
|  |   abstractDirectives = new Set<ClassDeclaration>(); | ||||||
|   directiveMeta = new Map<ClassDeclaration, DirectiveMeta>(); |   directiveMeta = new Map<ClassDeclaration, DirectiveMeta>(); | ||||||
|   ngModuleMeta = new Map<ClassDeclaration, NgModuleMeta>(); |   ngModuleMeta = new Map<ClassDeclaration, NgModuleMeta>(); | ||||||
|   pipeMeta = new Map<ClassDeclaration, PipeMeta>(); |   pipeMeta = new Map<ClassDeclaration, PipeMeta>(); | ||||||
|  | |||||||
| @ -75,6 +75,7 @@ export interface PipeMeta { | |||||||
|  * or a registry. |  * or a registry. | ||||||
|  */ |  */ | ||||||
| export interface MetadataReader { | export interface MetadataReader { | ||||||
|  |   isAbstractDirective(node: Reference<ClassDeclaration>): boolean; | ||||||
|   getDirectiveMetadata(node: Reference<ClassDeclaration>): DirectiveMeta|null; |   getDirectiveMetadata(node: Reference<ClassDeclaration>): DirectiveMeta|null; | ||||||
|   getNgModuleMetadata(node: Reference<ClassDeclaration>): NgModuleMeta|null; |   getNgModuleMetadata(node: Reference<ClassDeclaration>): NgModuleMeta|null; | ||||||
|   getPipeMetadata(node: Reference<ClassDeclaration>): PipeMeta|null; |   getPipeMetadata(node: Reference<ClassDeclaration>): PipeMeta|null; | ||||||
| @ -84,6 +85,7 @@ export interface MetadataReader { | |||||||
|  * Registers new metadata for directives, pipes, and modules. |  * Registers new metadata for directives, pipes, and modules. | ||||||
|  */ |  */ | ||||||
| export interface MetadataRegistry { | export interface MetadataRegistry { | ||||||
|  |   registerAbstractDirective(clazz: ClassDeclaration): void; | ||||||
|   registerDirectiveMetadata(meta: DirectiveMeta): void; |   registerDirectiveMetadata(meta: DirectiveMeta): void; | ||||||
|   registerNgModuleMetadata(meta: NgModuleMeta): void; |   registerNgModuleMetadata(meta: NgModuleMeta): void; | ||||||
|   registerPipeMetadata(meta: PipeMeta): void; |   registerPipeMetadata(meta: PipeMeta): void; | ||||||
|  | |||||||
| @ -21,6 +21,11 @@ import {extractDirectiveGuards, extractReferencesFromType, readStringArrayType, | |||||||
| export class DtsMetadataReader implements MetadataReader { | export class DtsMetadataReader implements MetadataReader { | ||||||
|   constructor(private checker: ts.TypeChecker, private reflector: ReflectionHost) {} |   constructor(private checker: ts.TypeChecker, private reflector: ReflectionHost) {} | ||||||
| 
 | 
 | ||||||
|  |   isAbstractDirective(ref: Reference<ClassDeclaration>): 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 |    * 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). |    * file, or in a .ts file with a handwritten definition). | ||||||
|  | |||||||
| @ -16,10 +16,14 @@ import {DirectiveMeta, MetadataReader, MetadataRegistry, NgModuleMeta, PipeMeta} | |||||||
|  * unit, which supports both reading and registering. |  * unit, which supports both reading and registering. | ||||||
|  */ |  */ | ||||||
| export class LocalMetadataRegistry implements MetadataRegistry, MetadataReader { | export class LocalMetadataRegistry implements MetadataRegistry, MetadataReader { | ||||||
|  |   private abstractDirectives = new Set<ClassDeclaration>(); | ||||||
|   private directives = new Map<ClassDeclaration, DirectiveMeta>(); |   private directives = new Map<ClassDeclaration, DirectiveMeta>(); | ||||||
|   private ngModules = new Map<ClassDeclaration, NgModuleMeta>(); |   private ngModules = new Map<ClassDeclaration, NgModuleMeta>(); | ||||||
|   private pipes = new Map<ClassDeclaration, PipeMeta>(); |   private pipes = new Map<ClassDeclaration, PipeMeta>(); | ||||||
| 
 | 
 | ||||||
|  |   isAbstractDirective(ref: Reference<ClassDeclaration>): boolean { | ||||||
|  |     return this.abstractDirectives.has(ref.node); | ||||||
|  |   } | ||||||
|   getDirectiveMetadata(ref: Reference<ClassDeclaration>): DirectiveMeta|null { |   getDirectiveMetadata(ref: Reference<ClassDeclaration>): DirectiveMeta|null { | ||||||
|     return this.directives.has(ref.node) ? this.directives.get(ref.node) ! : 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; |     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); } |   registerDirectiveMetadata(meta: DirectiveMeta): void { this.directives.set(meta.ref.node, meta); } | ||||||
|   registerNgModuleMetadata(meta: NgModuleMeta): void { this.ngModules.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); } |   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 { | export class CompoundMetadataRegistry implements MetadataRegistry { | ||||||
|   constructor(private registries: MetadataRegistry[]) {} |   constructor(private registries: MetadataRegistry[]) {} | ||||||
| 
 | 
 | ||||||
|  |   registerAbstractDirective(clazz: ClassDeclaration) { | ||||||
|  |     for (const registry of this.registries) { | ||||||
|  |       registry.registerAbstractDirective(clazz); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   registerDirectiveMetadata(meta: DirectiveMeta): void { |   registerDirectiveMetadata(meta: DirectiveMeta): void { | ||||||
|     for (const registry of this.registries) { |     for (const registry of this.registries) { | ||||||
|       registry.registerDirectiveMetadata(meta); |       registry.registerDirectiveMetadata(meta); | ||||||
|  | |||||||
| @ -125,6 +125,10 @@ function extractTemplateGuard(member: ClassMember): TemplateGuardMeta|null { | |||||||
| export class CompoundMetadataReader implements MetadataReader { | export class CompoundMetadataReader implements MetadataReader { | ||||||
|   constructor(private readers: MetadataReader[]) {} |   constructor(private readers: MetadataReader[]) {} | ||||||
| 
 | 
 | ||||||
|  |   isAbstractDirective(node: Reference<ClassDeclaration>): boolean { | ||||||
|  |     return this.readers.some(r => r.isAbstractDirective(node)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   getDirectiveMetadata(node: Reference<ClassDeclaration<ts.Declaration>>): DirectiveMeta|null { |   getDirectiveMetadata(node: Reference<ClassDeclaration<ts.Declaration>>): DirectiveMeta|null { | ||||||
|     for (const reader of this.readers) { |     for (const reader of this.readers) { | ||||||
|       const meta = reader.getDirectiveMetadata(node); |       const meta = reader.getDirectiveMetadata(node); | ||||||
|  | |||||||
| @ -514,9 +514,9 @@ export class NgtscProgram implements api.Program { | |||||||
|           this.reflector, this.defaultImportTracker, this.isCore, |           this.reflector, this.defaultImportTracker, this.isCore, | ||||||
|           this.options.strictInjectionParameters || false), |           this.options.strictInjectionParameters || false), | ||||||
|       new NgModuleDecoratorHandler( |       new NgModuleDecoratorHandler( | ||||||
|           this.reflector, evaluator, metaRegistry, scopeRegistry, referencesRegistry, this.isCore, |           this.reflector, evaluator, this.metaReader, metaRegistry, scopeRegistry, | ||||||
|           this.routeAnalyzer, this.refEmitter, this.defaultImportTracker, |           referencesRegistry, this.isCore, this.routeAnalyzer, this.refEmitter, | ||||||
|           this.options.i18nInLocale), |           this.defaultImportTracker, this.options.i18nInLocale), | ||||||
|       new PipeDecoratorHandler( |       new PipeDecoratorHandler( | ||||||
|           this.reflector, evaluator, metaRegistry, this.defaultImportTracker, this.isCore), |           this.reflector, evaluator, metaRegistry, this.defaultImportTracker, this.isCore), | ||||||
|     ]; |     ]; | ||||||
|  | |||||||
| @ -117,6 +117,8 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   registerAbstractDirective(clazz: ClassDeclaration): void {} | ||||||
|  | 
 | ||||||
|   registerDirectiveMetadata(directive: DirectiveMeta): void {} |   registerDirectiveMetadata(directive: DirectiveMeta): void {} | ||||||
| 
 | 
 | ||||||
|   registerPipeMetadata(pipe: PipeMeta): void {} |   registerPipeMetadata(pipe: PipeMeta): void {} | ||||||
|  | |||||||
| @ -669,44 +669,6 @@ describe('compiler compliance', () => { | |||||||
|           source, EmptyOutletComponentDefinition, 'Incorrect EmptyOutletComponent.ngComponentDef'); |           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', |     it('should not treat ElementRef, ViewContainerRef, or ChangeDetectorRef specially when injecting', | ||||||
|        () => { |        () => { | ||||||
|          const files = { |          const files = { | ||||||
|  | |||||||
| @ -1008,19 +1008,43 @@ runInEachFileSystem(os => { | |||||||
|         expect(jsContents).toContain('selectors: [["ng-component"]]'); |         expect(jsContents).toContain('selectors: [["ng-component"]]'); | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       it('should throw if selector is missing in Directive decorator params', () => { |       it('should allow directives with no selector that are not in NgModules', () => { | ||||||
|         env.write('test.ts', ` |         env.write('main.ts', ` | ||||||
|         import {Directive} from '@angular/core'; |           import {Directive} from '@angular/core'; | ||||||
| 
 | 
 | ||||||
|         @Directive({ |           @Directive({}) | ||||||
|           inputs: ['a', 'b'] |           export class BaseDir {} | ||||||
|         }) |  | ||||||
|         export class TestDir {} |  | ||||||
|       `);
 |  | ||||||
| 
 | 
 | ||||||
|  |           @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(); |         const errors = env.driveDiagnostics(); | ||||||
|         expect(trim(errors[0].messageText as string)) |         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', () => { |       it('should throw if Directive selector is an empty string', () => { | ||||||
|  | |||||||
| @ -132,10 +132,6 @@ export function compileDirectiveFromMetadata( | |||||||
|   addFeatures(definitionMap, meta); |   addFeatures(definitionMap, meta); | ||||||
|   const expression = o.importExpr(R3.defineDirective).callFn([definitionMap.toLiteralMap()]); |   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); |   const type = createTypeForDef(meta, R3.DirectiveDefWithMeta); | ||||||
|   return {expression, type, statements}; |   return {expression, type, statements}; | ||||||
| } | } | ||||||
|  | |||||||
| @ -117,7 +117,7 @@ function hasSelectorScope<T>(component: Type<T>): component is Type<T>& | |||||||
|  * In the event that compilation is not immediate, `compileDirective` will return a `Promise` which |  * In the event that compilation is not immediate, `compileDirective` will return a `Promise` which | ||||||
|  * will resolve when compilation completes and the directive becomes usable. |  * will resolve when compilation completes and the directive becomes usable. | ||||||
|  */ |  */ | ||||||
| export function compileDirective(type: Type<any>, directive: Directive): void { | export function compileDirective(type: Type<any>, directive: Directive | null): void { | ||||||
|   let ngDirectiveDef: any = null; |   let ngDirectiveDef: any = null; | ||||||
|   Object.defineProperty(type, NG_DIRECTIVE_DEF, { |   Object.defineProperty(type, NG_DIRECTIVE_DEF, { | ||||||
|     get: () => { |     get: () => { | ||||||
| @ -125,7 +125,10 @@ export function compileDirective(type: Type<any>, directive: Directive): void { | |||||||
|         const name = type && type.name; |         const name = type && type.name; | ||||||
|         const sourceMapUrl = `ng:///${name}/ngDirectiveDef.js`; |         const sourceMapUrl = `ng:///${name}/ngDirectiveDef.js`; | ||||||
|         const compiler = getCompilerFacade(); |         const compiler = getCompilerFacade(); | ||||||
|         const facade = directiveMetadata(type as ComponentType<any>, 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<any>, directive || {}); | ||||||
|         facade.typeSourceSpan = compiler.createParseSourceSpan('Directive', name, sourceMapUrl); |         facade.typeSourceSpan = compiler.createParseSourceSpan('Directive', name, sourceMapUrl); | ||||||
|         if (facade.usesInheritance) { |         if (facade.usesInheritance) { | ||||||
|           addBaseDefToUndecoratedParents(type); |           addBaseDefToUndecoratedParents(type); | ||||||
|  | |||||||
| @ -185,6 +185,7 @@ function verifySemanticsOfNgModuleDef( | |||||||
|   }); |   }); | ||||||
|   const exports = maybeUnwrapFn(ngModuleDef.exports); |   const exports = maybeUnwrapFn(ngModuleDef.exports); | ||||||
|   declarations.forEach(verifyDeclarationsHaveDefinitions); |   declarations.forEach(verifyDeclarationsHaveDefinitions); | ||||||
|  |   declarations.forEach(verifyDirectivesHaveSelector); | ||||||
|   const combinedDeclarations: Type<any>[] = [ |   const combinedDeclarations: Type<any>[] = [ | ||||||
|     ...declarations.map(resolveForwardRef), |     ...declarations.map(resolveForwardRef), | ||||||
|     ...flatten(imports.map(computeCombinedExports)).map(resolveForwardRef), |     ...flatten(imports.map(computeCombinedExports)).map(resolveForwardRef), | ||||||
| @ -220,6 +221,14 @@ function verifySemanticsOfNgModuleDef( | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   function verifyDirectivesHaveSelector(type: Type<any>): 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<any>) { |   function verifyExportsAreDeclaredOrReExported(type: Type<any>) { | ||||||
|     type = resolveForwardRef(type); |     type = resolveForwardRef(type); | ||||||
|     const kind = getComponentDef(type) && 'component' || getDirectiveDef(type) && 'directive' || |     const kind = getComponentDef(type) && 'component' || getDirectiveDef(type) && 'directive' || | ||||||
|  | |||||||
| @ -1411,7 +1411,7 @@ function declareTests(config?: {useJit: boolean}) { | |||||||
|         expect(getDOM().querySelectorAll(fixture.nativeElement, 'script').length).toEqual(0); |         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({}) |         @Directive({}) | ||||||
|         class SomeDirective { |         class SomeDirective { | ||||||
|         } |         } | ||||||
| @ -1425,6 +1425,38 @@ function declareTests(config?: {useJit: boolean}) { | |||||||
|             .toThrowError(`Directive ${stringify(SomeDirective)} has no selector, please add it!`); |             .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', () => { |       it('should throw when using directives with empty string selector', () => { | ||||||
|         @Directive({selector: ''}) |         @Directive({selector: ''}) | ||||||
|         class SomeDirective { |         class SomeDirective { | ||||||
|  | |||||||
| @ -299,7 +299,7 @@ export class R3TestBedCompiler { | |||||||
|     this.pendingComponents.clear(); |     this.pendingComponents.clear(); | ||||||
| 
 | 
 | ||||||
|     this.pendingDirectives.forEach(declaration => { |     this.pendingDirectives.forEach(declaration => { | ||||||
|       const metadata = this.resolvers.directive.resolve(declaration) !; |       const metadata = this.resolvers.directive.resolve(declaration); | ||||||
|       this.maybeStoreNgDef(NG_DIRECTIVE_DEF, declaration); |       this.maybeStoreNgDef(NG_DIRECTIVE_DEF, declaration); | ||||||
|       compileDirective(declaration, metadata); |       compileDirective(declaration, metadata); | ||||||
|     }); |     }); | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user