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

Recently it was made possible to have a directive without selector,
which are referred to as abstract directives. Such directives should not
be registered in an NgModule, but can still contain decorators for
inputs, outputs, queries, etc. The information from these decorators and
the `@Directive()` decorator itself needs to be registered with the
central `MetadataRegistry` so that other areas of the compiler can
request information about a given directive, an example of which is the
template type checker that needs to know about the inputs and outputs of
directives.

Prior to this change, however, abstract directives would only register
themselves with the `MetadataRegistry` as being an abstract directive,
without all of its other metadata like inputs and outputs. This meant
that the template type checker was unable to resolve the inputs and
outputs of these abstract directives, therefore failing to check them
correctly. The typical error would be that some property does not exist
on a DOM element, whereas said property should have been bound to the
abstract directive's input.

This commit fixes the problem by always registering the metadata of a
directive or component with the `MetadataRegistry`. Tests have been
added to ensure abstract directives are handled correctly in the
template type checker, together with tests to verify the form of
abstract directives in declaration files.

Fixes #30080

PR Close #33131
This commit is contained in:
JoostK
2019-10-13 17:00:13 +02:00
committed by Andrew Kushnir
parent 9a5e08f2a7
commit a42057d0f8
14 changed files with 166 additions and 110 deletions

View File

@ -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
@ -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);
@ -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)) {
@ -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) {

View File

@ -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,

View File

@ -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!`);