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:
@ -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<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(
|
||||
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);
|
||||
|
@ -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<NgModuleAnalysis, Decorator> {
|
||||
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<NgModuleAnalys
|
||||
const scope = this.scopeRegistry.getScopeOfModule(node);
|
||||
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) {
|
||||
// Using the scope information, extend the injector's imports using the modules that are
|
||||
// specified as module exports.
|
||||
const context = getSourceFile(node);
|
||||
for (const exportRef of analysis.exports) {
|
||||
if (isNgModule(exportRef.node, scope.compilation)) {
|
||||
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) {
|
||||
|
@ -8,10 +8,11 @@
|
||||
import {WrappedNodeExpr} from '@angular/compiler';
|
||||
import {R3Reference} from '@angular/compiler/src/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {absoluteFrom} from '../../file_system';
|
||||
import {runInEachFileSystem} from '../../file_system/testing';
|
||||
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 {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection';
|
||||
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope';
|
||||
@ -59,6 +60,7 @@ runInEachFileSystem(() => {
|
||||
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 =
|
||||
|
@ -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,
|
||||
|
@ -93,6 +93,19 @@ export class IncrementalState implements DependencyTracker, MetadataReader, 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 {
|
||||
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<ts.SourceFile>();
|
||||
resourcePaths = new Set<string>();
|
||||
abstractDirectives = new Set<ClassDeclaration>();
|
||||
directiveMeta = new Map<ClassDeclaration, DirectiveMeta>();
|
||||
ngModuleMeta = new Map<ClassDeclaration, NgModuleMeta>();
|
||||
pipeMeta = new Map<ClassDeclaration, PipeMeta>();
|
||||
|
@ -75,6 +75,7 @@ export interface PipeMeta {
|
||||
* or a registry.
|
||||
*/
|
||||
export interface MetadataReader {
|
||||
isAbstractDirective(node: Reference<ClassDeclaration>): boolean;
|
||||
getDirectiveMetadata(node: Reference<ClassDeclaration>): DirectiveMeta|null;
|
||||
getNgModuleMetadata(node: Reference<ClassDeclaration>): NgModuleMeta|null;
|
||||
getPipeMetadata(node: Reference<ClassDeclaration>): 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;
|
||||
|
@ -21,6 +21,11 @@ import {extractDirectiveGuards, extractReferencesFromType, readStringArrayType,
|
||||
export class DtsMetadataReader implements MetadataReader {
|
||||
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
|
||||
* 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.
|
||||
*/
|
||||
export class LocalMetadataRegistry implements MetadataRegistry, MetadataReader {
|
||||
private abstractDirectives = new Set<ClassDeclaration>();
|
||||
private directives = new Map<ClassDeclaration, DirectiveMeta>();
|
||||
private ngModules = new Map<ClassDeclaration, NgModuleMeta>();
|
||||
private pipes = new Map<ClassDeclaration, PipeMeta>();
|
||||
|
||||
isAbstractDirective(ref: Reference<ClassDeclaration>): boolean {
|
||||
return this.abstractDirectives.has(ref.node);
|
||||
}
|
||||
getDirectiveMetadata(ref: Reference<ClassDeclaration>): 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);
|
||||
|
@ -125,6 +125,10 @@ function extractTemplateGuard(member: ClassMember): TemplateGuardMeta|null {
|
||||
export class CompoundMetadataReader implements 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 {
|
||||
for (const reader of this.readers) {
|
||||
const meta = reader.getDirectiveMetadata(node);
|
||||
|
@ -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),
|
||||
];
|
||||
|
@ -117,6 +117,8 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
|
||||
}
|
||||
}
|
||||
|
||||
registerAbstractDirective(clazz: ClassDeclaration): void {}
|
||||
|
||||
registerDirectiveMetadata(directive: DirectiveMeta): void {}
|
||||
|
||||
registerPipeMetadata(pipe: PipeMeta): void {}
|
||||
|
Reference in New Issue
Block a user