Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ivy): support abstract directives in template type checking #33131

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 26 additions & 24 deletions packages/compiler-cli/src/ngtsc/annotations/src/component.ts
Expand Up @@ -216,24 +216,20 @@ export class ComponentDecoratorHandler implements
`Errors parsing template: ${template.errors.map(e => e.toString()).join(', ')}`);
}

// If the component has a selector, it should be registered with the
// `LocalModuleScopeRegistry`
// so that when this component appears in an `@NgModule` scope, its selector can be
// determined.
if (metadata.selector !== null) {
const ref = new Reference(node);
this.metaRegistry.registerDirectiveMetadata({
ref,
name: node.name.text,
selector: metadata.selector,
exportAs: metadata.exportAs,
inputs: metadata.inputs,
outputs: metadata.outputs,
queries: metadata.queries.map(query => query.propertyName),
isComponent: true, ...extractDirectiveGuards(node, this.reflector),
baseClass: readBaseClass(node, this.reflector, this.evaluator),
});
}
// Register this component's information with the `MetadataRegistry`. This ensures that
// the information about the component is available during the compile() phase.
const ref = new Reference(node);
this.metaRegistry.registerDirectiveMetadata({
ref,
name: node.name.text,
selector: metadata.selector,
exportAs: metadata.exportAs,
inputs: metadata.inputs,
outputs: metadata.outputs,
queries: metadata.queries.map(query => query.propertyName),
isComponent: true, ...extractDirectiveGuards(node, this.reflector),
baseClass: readBaseClass(node, this.reflector, this.evaluator),
});

// Figure out the set of styles. The ordering here is important: external resources (styleUrls)
// precede inline styles, and styles defined in the template override styles defined in the
Expand Down Expand Up @@ -325,7 +321,9 @@ export class ComponentDecoratorHandler implements
const matcher = new SelectorMatcher<DirectiveMeta>();
if (scope !== null) {
for (const directive of scope.compilation.directives) {
matcher.addSelectables(CssSelector.parse(directive.selector), directive);
if (directive.selector !== null) {
matcher.addSelectables(CssSelector.parse(directive.selector), directive);
}
}
}
const binder = new R3TargetBinder(matcher);
Expand Down Expand Up @@ -368,8 +366,10 @@ export class ComponentDecoratorHandler implements
const scope = this.scopeReader.getScopeForComponent(node);
if (scope !== null) {
for (const meta of scope.compilation.directives) {
const extMeta = flattenInheritedDirectiveMetadata(this.metaReader, meta.ref);
matcher.addSelectables(CssSelector.parse(meta.selector), extMeta);
if (meta.selector !== null) {
const extMeta = flattenInheritedDirectiveMetadata(this.metaReader, meta.ref);
matcher.addSelectables(CssSelector.parse(meta.selector), extMeta);
}
}
for (const {name, ref} of scope.compilation.pipes) {
if (!ts.isClassDeclaration(ref.node)) {
Expand Down Expand Up @@ -422,9 +422,11 @@ export class ComponentDecoratorHandler implements

for (const dir of scope.compilation.directives) {
const {ref, selector} = dir;
const expression = this.refEmitter.emit(ref, context);
directives.push({selector, expression});
matcher.addSelectables(CssSelector.parse(selector), {...dir, expression});
if (selector !== null) {
const expression = this.refEmitter.emit(ref, context);
directives.push({selector, expression});
matcher.addSelectables(CssSelector.parse(selector), {...dir, expression});
}
}
const pipes = new Map<string, Expression>();
for (const pipe of scope.compilation.pipes) {
Expand Down
37 changes: 15 additions & 22 deletions packages/compiler-cli/src/ngtsc/annotations/src/directive.ts
Expand Up @@ -55,32 +55,25 @@ export class DirectiveDecoratorHandler implements
const directiveResult = extractDirectiveMetadata(
node, decorator, this.reflector, this.evaluator, this.defaultImportRecorder, this.isCore);
const analysis = directiveResult && directiveResult.metadata;

// If the directive has a selector, it should be registered with the `SelectorScopeRegistry` so
// when this directive appears in an `@NgModule` scope, its selector can be determined.
if (analysis && analysis.selector !== null) {
const ref = new Reference(node);
this.metaRegistry.registerDirectiveMetadata({
ref,
name: node.name.text,
selector: analysis.selector,
exportAs: analysis.exportAs,
inputs: analysis.inputs,
outputs: analysis.outputs,
queries: analysis.queries.map(query => query.propertyName),
isComponent: false, ...extractDirectiveGuards(node, this.reflector),
baseClass: readBaseClass(node, this.reflector, this.evaluator),
});
}

if (analysis && !analysis.selector) {
this.metaRegistry.registerAbstractDirective(node);
}

if (analysis === undefined) {
return {};
}

// Register this directive's information with the `MetadataRegistry`. This ensures that
// the information about the directive is available during the compile() phase.
const ref = new Reference(node);
this.metaRegistry.registerDirectiveMetadata({
ref,
name: node.name.text,
selector: analysis.selector,
exportAs: analysis.exportAs,
inputs: analysis.inputs,
outputs: analysis.outputs,
queries: analysis.queries.map(query => query.propertyName),
isComponent: false, ...extractDirectiveGuards(node, this.reflector),
baseClass: readBaseClass(node, this.reflector, this.evaluator),
});

return {
analysis: {
meta: analysis,
Expand Down
4 changes: 3 additions & 1 deletion packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts
Expand Up @@ -262,7 +262,9 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
}

for (const decl of analysis.declarations) {
if (this.metaReader.isAbstractDirective(decl)) {
const metadata = this.metaReader.getDirectiveMetadata(decl);

if (metadata !== null && metadata.selector === null) {
throw new FatalDiagnosticError(
ErrorCode.DIRECTIVE_MISSING_SELECTOR, decl.node,
`Directive ${decl.node.name.text} has no selector, please add it!`);
Expand Down
14 changes: 0 additions & 14 deletions packages/compiler-cli/src/ngtsc/incremental/src/state.ts
Expand Up @@ -93,19 +93,6 @@ 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;
Expand Down Expand Up @@ -200,7 +187,6 @@ 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>();
Expand Down
4 changes: 2 additions & 2 deletions packages/compiler-cli/src/ngtsc/indexer/src/context.ts
Expand Up @@ -13,9 +13,9 @@ import {ClassDeclaration} from '../../reflection';
export interface ComponentMeta extends DirectiveMeta {
ref: Reference<ClassDeclaration>;
/**
* Unparsed selector of the directive.
* Unparsed selector of the directive, or null if the directive does not have a selector.
*/
selector: string;
selector: string|null;
}

/**
Expand Down
6 changes: 2 additions & 4 deletions packages/compiler-cli/src/ngtsc/metadata/src/api.ts
Expand Up @@ -28,9 +28,9 @@ export interface NgModuleMeta {
export interface DirectiveMeta extends T2DirectiveMeta {
ref: Reference<ClassDeclaration>;
/**
* Unparsed selector of the directive.
* Unparsed selector of the directive, or null if the directive does not have a selector.
*/
selector: string;
selector: string|null;
queries: string[];
ngTemplateGuards: TemplateGuardMeta[];
hasNgTemplateContextGuard: boolean;
Expand Down Expand Up @@ -76,7 +76,6 @@ 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;
Expand All @@ -86,7 +85,6 @@ 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;
Expand Down
12 changes: 2 additions & 10 deletions packages/compiler-cli/src/ngtsc/metadata/src/dts.ts
Expand Up @@ -21,11 +21,6 @@ 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).
Expand Down Expand Up @@ -79,15 +74,12 @@ export class DtsMetadataReader implements MetadataReader {
// The type metadata was the wrong shape.
return null;
}
const selector = readStringType(def.type.typeArguments[1]);
if (selector === null) {
return null;
}

return {
ref,
name: clazz.name.text,
isComponent: def.name === 'ɵcmp', selector,
isComponent: def.name === 'ɵcmp',
selector: readStringType(def.type.typeArguments[1]),
exportAs: readStringArrayType(def.type.typeArguments[2]),
inputs: readStringMapType(def.type.typeArguments[3]),
outputs: readStringMapType(def.type.typeArguments[4]),
Expand Down
11 changes: 0 additions & 11 deletions packages/compiler-cli/src/ngtsc/metadata/src/registry.ts
Expand Up @@ -16,14 +16,10 @@ 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;
}
Expand All @@ -34,7 +30,6 @@ 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); }
Expand All @@ -46,12 +41,6 @@ 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);
Expand Down
4 changes: 0 additions & 4 deletions packages/compiler-cli/src/ngtsc/metadata/src/util.ts
Expand Up @@ -125,10 +125,6 @@ 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);
Expand Down
2 changes: 0 additions & 2 deletions packages/compiler-cli/src/ngtsc/scope/src/local.ts
Expand Up @@ -118,8 +118,6 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
}
}

registerAbstractDirective(clazz: ClassDeclaration): void {}

registerDirectiveMetadata(directive: DirectiveMeta): void {}

registerPipeMetadata(pipe: PipeMeta): void {}
Expand Down
44 changes: 44 additions & 0 deletions packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
Expand Up @@ -191,6 +191,50 @@ runInEachFileSystem(os => {
expect(jsContents).toContain('inject(Dep, 8)');
});

it('should compile Directives without errors', () => {
env.write('test.ts', `
import {Directive} from '@angular/core';
@Directive({selector: '[dir]'})
export class TestDir {}
`);

env.driveMain();

const jsContents = env.getContents('test.js');
expect(jsContents).toContain('TestDir.ɵdir = i0.ɵɵdefineDirective');
expect(jsContents).toContain('TestDir.ɵfac = function');
expect(jsContents).not.toContain('__decorate');

const dtsContents = env.getContents('test.d.ts');
expect(dtsContents)
.toContain(
'static ɵdir: i0.ɵɵDirectiveDefWithMeta<TestDir, "[dir]", never, {}, {}, never>');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestDir>');
});

it('should compile abstract Directives without errors', () => {
env.write('test.ts', `
import {Directive} from '@angular/core';
@Directive()
export class TestDir {}
`);

env.driveMain();

const jsContents = env.getContents('test.js');
expect(jsContents).toContain('TestDir.ɵdir = i0.ɵɵdefineDirective');
expect(jsContents).toContain('TestDir.ɵfac = function');
expect(jsContents).not.toContain('__decorate');

const dtsContents = env.getContents('test.d.ts');
expect(dtsContents)
.toContain(
'static ɵdir: i0.ɵɵDirectiveDefWithMeta<TestDir, never, never, {}, {}, never>');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestDir>');
});

it('should compile Components (inline template) without errors', () => {
env.write('test.ts', `
import {Component} from '@angular/core';
Expand Down