refactor(compiler-cli): identify structural directives (#40032)

This commit introduces an `isStructural` flag on directive metadata, which
is `true` if the directive injects `TemplateRef` (and thus is at least
theoretically usable as a structural directive). The flag is not used for
anything currently, but will be utilized by the Language Service to offer
better autocompletion results for structural directives.

PR Close #40032
This commit is contained in:
Alex Rickabaugh 2020-12-07 17:15:37 -08:00
parent cbb6eae4a9
commit c55bf4a4a3
18 changed files with 227 additions and 11 deletions

View File

@ -396,6 +396,7 @@ export class ComponentDecoratorHandler implements
baseClass: analysis.baseClass, baseClass: analysis.baseClass,
...analysis.typeCheckMeta, ...analysis.typeCheckMeta,
isPoisoned: analysis.isPoisoned, isPoisoned: analysis.isPoisoned,
isStructural: false,
}); });
this.resourceRegistry.registerResources(analysis.resources, node); this.resourceRegistry.registerResources(analysis.resources, node);

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {compileDeclareDirectiveFromMetadata, compileDirectiveFromMetadata, ConstantPool, Expression, Identifiers, makeBindingParser, ParsedHostBindings, ParseError, parseHostBindings, R3DependencyMetadata, R3DirectiveDef, R3DirectiveMetadata, R3FactoryTarget, R3QueryMetadata, Statement, verifyHostBindings, WrappedNodeExpr} from '@angular/compiler'; import {compileDeclareDirectiveFromMetadata, compileDirectiveFromMetadata, ConstantPool, Expression, ExternalExpr, Identifiers, makeBindingParser, ParsedHostBindings, ParseError, parseHostBindings, R3DependencyMetadata, R3DirectiveDef, R3DirectiveMetadata, R3FactoryTarget, R3QueryMetadata, R3ResolvedDependencyType, Statement, verifyHostBindings, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
@ -42,6 +42,7 @@ export interface DirectiveHandlerData {
inputs: ClassPropertyMapping; inputs: ClassPropertyMapping;
outputs: ClassPropertyMapping; outputs: ClassPropertyMapping;
isPoisoned: boolean; isPoisoned: boolean;
isStructural: boolean;
} }
export class DirectiveDecoratorHandler implements export class DirectiveDecoratorHandler implements
@ -109,6 +110,7 @@ export class DirectiveDecoratorHandler implements
typeCheckMeta: extractDirectiveTypeCheckMeta(node, directiveResult.inputs, this.reflector), typeCheckMeta: extractDirectiveTypeCheckMeta(node, directiveResult.inputs, this.reflector),
providersRequiringFactory, providersRequiringFactory,
isPoisoned: false, isPoisoned: false,
isStructural: directiveResult.isStructural,
} }
}; };
} }
@ -129,6 +131,7 @@ export class DirectiveDecoratorHandler implements
baseClass: analysis.baseClass, baseClass: analysis.baseClass,
...analysis.typeCheckMeta, ...analysis.typeCheckMeta,
isPoisoned: analysis.isPoisoned, isPoisoned: analysis.isPoisoned,
isStructural: analysis.isStructural,
}); });
this.injectableRegistry.registerInjectable(node); this.injectableRegistry.registerInjectable(node);
@ -226,6 +229,7 @@ export function extractDirectiveMetadata(
metadata: R3DirectiveMetadata, metadata: R3DirectiveMetadata,
inputs: ClassPropertyMapping, inputs: ClassPropertyMapping,
outputs: ClassPropertyMapping, outputs: ClassPropertyMapping,
isStructural: boolean;
}|undefined { }|undefined {
let directive: Map<string, ts.Expression>; let directive: Map<string, ts.Expression>;
if (decorator === null || decorator.args === null || decorator.args.length === 0) { if (decorator === null || decorator.args === null || decorator.args.length === 0) {
@ -352,6 +356,17 @@ export function extractDirectiveMetadata(
ctorDeps = unwrapConstructorDependencies(rawCtorDeps); ctorDeps = unwrapConstructorDependencies(rawCtorDeps);
} }
const isStructural = ctorDeps !== null && ctorDeps !== 'invalid' && ctorDeps.some(dep => {
if (dep.resolved !== R3ResolvedDependencyType.Token || !(dep.token instanceof ExternalExpr)) {
return false;
}
if (dep.token.value.moduleName !== '@angular/core' || dep.token.value.name !== 'TemplateRef') {
return false;
}
return true;
});
// Detect if the component inherits from another class // Detect if the component inherits from another class
const usesInheritance = reflector.hasBaseClass(clazz); const usesInheritance = reflector.hasBaseClass(clazz);
const type = wrapTypeReference(reflector, clazz); const type = wrapTypeReference(reflector, clazz);
@ -386,6 +401,7 @@ export function extractDirectiveMetadata(
metadata, metadata,
inputs, inputs,
outputs, outputs,
isStructural,
}; };
} }

View File

@ -105,6 +105,7 @@ runInEachFileSystem(() => {
isComponent: false, isComponent: false,
name: 'Dir', name: 'Dir',
selector: '[dir]', selector: '[dir]',
isStructural: false,
}; };
matcher.addSelectables(CssSelector.parse('[dir]'), dirMeta); matcher.addSelectables(CssSelector.parse('[dir]'), dirMeta);
@ -118,6 +119,30 @@ runInEachFileSystem(() => {
// and field names. // and field names.
expect(propBindingConsumer).toBe(dirMeta); expect(propBindingConsumer).toBe(dirMeta);
}); });
it('should identify a structural directive', () => {
const src = `
import {Directive, TemplateRef} from '@angular/core';
@Directive({selector: 'test-dir'})
export class TestDir {
constructor(private ref: TemplateRef) {}
}
`;
const {program} = makeProgram([
{
name: _('/node_modules/@angular/core/index.d.ts'),
contents: 'export const Directive: any; export declare class TemplateRef {}',
},
{
name: _('/entry.ts'),
contents: src,
},
]);
const analysis = analyzeDirective(program, 'TestDir');
expect(analysis.isStructural).toBeTrue();
});
}); });
// Helpers // Helpers

View File

@ -54,6 +54,7 @@ export function getBoundTemplate(
inputs: ClassPropertyMapping.fromMappedObject({}), inputs: ClassPropertyMapping.fromMappedObject({}),
outputs: ClassPropertyMapping.fromMappedObject({}), outputs: ClassPropertyMapping.fromMappedObject({}),
exportAs: null, exportAs: null,
isStructural: false,
}); });
}); });
const binder = new R3TargetBinder(matcher); const binder = new R3TargetBinder(matcher);

View File

@ -114,6 +114,11 @@ export interface DirectiveMeta extends T2DirectiveMeta, DirectiveTypeCheckMeta {
* and reliable metadata. * and reliable metadata.
*/ */
isPoisoned: boolean; isPoisoned: boolean;
/**
* Whether the directive is likely a structural directive (injects `TemplateRef`).
*/
isStructural: boolean;
} }
/** /**

View File

@ -9,7 +9,7 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {Reference} from '../../imports'; import {Reference} from '../../imports';
import {ClassDeclaration, isNamedClassDeclaration, ReflectionHost} from '../../reflection'; import {ClassDeclaration, isNamedClassDeclaration, ReflectionHost, TypeValueReferenceKind} from '../../reflection';
import {DirectiveMeta, MetadataReader, NgModuleMeta, PipeMeta} from './api'; import {DirectiveMeta, MetadataReader, NgModuleMeta, PipeMeta} from './api';
import {ClassPropertyMapping} from './property_mapping'; import {ClassPropertyMapping} from './property_mapping';
@ -77,6 +77,19 @@ export class DtsMetadataReader implements MetadataReader {
return null; return null;
} }
const isComponent = def.name === 'ɵcmp';
const ctorParams = this.reflector.getConstructorParameters(clazz);
// A directive is considered to be structural if:
// 1) it's a directive, not a component, and
// 2) it injects `TemplateRef`
const isStructural = !isComponent && ctorParams !== null && ctorParams.some(param => {
return param.typeValueReference.kind === TypeValueReferenceKind.IMPORTED &&
param.typeValueReference.moduleName === '@angular/core' &&
param.typeValueReference.importedName === 'TemplateRef';
});
const inputs = const inputs =
ClassPropertyMapping.fromMappedObject(readStringMapType(def.type.typeArguments[3])); ClassPropertyMapping.fromMappedObject(readStringMapType(def.type.typeArguments[3]));
const outputs = const outputs =
@ -84,7 +97,7 @@ export class DtsMetadataReader implements MetadataReader {
return { return {
ref, ref,
name: clazz.name.text, name: clazz.name.text,
isComponent: def.name === 'ɵcmp', isComponent,
selector: readStringType(def.type.typeArguments[1]), selector: readStringType(def.type.typeArguments[1]),
exportAs: readStringArrayType(def.type.typeArguments[2]), exportAs: readStringArrayType(def.type.typeArguments[2]),
inputs, inputs,
@ -93,6 +106,7 @@ export class DtsMetadataReader implements MetadataReader {
...extractDirectiveTypeCheckMeta(clazz, inputs, this.reflector), ...extractDirectiveTypeCheckMeta(clazz, inputs, this.reflector),
baseClass: readBaseClass(clazz, this.checker, this.reflector), baseClass: readBaseClass(clazz, this.checker, this.reflector),
isPoisoned: false, isPoisoned: false,
isStructural,
}; };
} }

View File

@ -37,6 +37,7 @@ export function flattenInheritedDirectiveMetadata(
let isDynamic = false; let isDynamic = false;
let inputs = ClassPropertyMapping.empty(); let inputs = ClassPropertyMapping.empty();
let outputs = ClassPropertyMapping.empty(); let outputs = ClassPropertyMapping.empty();
let isStructural: boolean = false;
const addMetadata = (meta: DirectiveMeta): void => { const addMetadata = (meta: DirectiveMeta): void => {
if (meta.baseClass === 'dynamic') { if (meta.baseClass === 'dynamic') {
@ -51,6 +52,8 @@ export function flattenInheritedDirectiveMetadata(
} }
} }
isStructural = isStructural || meta.isStructural;
inputs = ClassPropertyMapping.merge(inputs, meta.inputs); inputs = ClassPropertyMapping.merge(inputs, meta.inputs);
outputs = ClassPropertyMapping.merge(outputs, meta.outputs); outputs = ClassPropertyMapping.merge(outputs, meta.outputs);
@ -79,5 +82,6 @@ export function flattenInheritedDirectiveMetadata(
restrictedInputFields, restrictedInputFields,
stringLiteralInputFields, stringLiteralInputFields,
baseClass: isDynamic ? 'dynamic' : null, baseClass: isDynamic ? 'dynamic' : null,
isStructural,
}; };
} }

View File

@ -0,0 +1,35 @@
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "test_lib",
testonly = True,
srcs = glob([
"**/*.ts",
]),
deps =
[
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/testing",
"@npm//typescript",
],
)
jasmine_node_test(
name = "test",
bootstrap = ["//tools/testing:node_no_angular_es5"],
data = [
"//packages/compiler-cli/src/ngtsc/testing/fake_core:npm_package",
],
deps =
[
":test_lib",
],
)

View File

@ -0,0 +1,93 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ExternalExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {Reference} from '../../imports';
import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
import {loadFakeCore, makeProgram} from '../../testing';
import {DtsMetadataReader} from '../src/dts';
runInEachFileSystem(() => {
beforeEach(() => {
loadFakeCore(getFileSystem());
});
describe('DtsMetadataReader', () => {
it('should not assume directives are structural', () => {
const mainPath = absoluteFrom('/main.d.ts');
const {program} = makeProgram(
[{
name: mainPath,
contents: `
import {ViewContainerRef} from '@angular/core';
import * as i0 from '@angular/core';
export declare class TestDir {
constructor(p0: ViewContainerRef);
static ɵdir: i0.ɵɵDirectiveDefWithMeta<TestDir, "[test]", never, {}, {}, never>
}
`
}],
{
skipLibCheck: true,
lib: ['es6', 'dom'],
});
const sf = getSourceFileOrError(program, mainPath);
const clazz = sf.statements[2];
if (!isNamedClassDeclaration(clazz)) {
return fail('Expected class declaration');
}
const typeChecker = program.getTypeChecker();
const dtsReader =
new DtsMetadataReader(typeChecker, new TypeScriptReflectionHost(typeChecker));
const meta = dtsReader.getDirectiveMetadata(new Reference(clazz))!;
expect(meta.isStructural).toBeFalse();
});
it('should identify a structural directive by its constructor', () => {
const mainPath = absoluteFrom('/main.d.ts');
const {program} = makeProgram(
[{
name: mainPath,
contents: `
import {TemplateRef, ViewContainerRef} from '@angular/core';
import * as i0 from '@angular/core';
export declare class TestDir {
constructor(p0: ViewContainerRef, p1: TemplateRef);
static ɵdir: i0.ɵɵDirectiveDefWithMeta<TestDir, "[test]", never, {}, {}, never>
}
`
}],
{
skipLibCheck: true,
lib: ['es6', 'dom'],
});
const sf = getSourceFileOrError(program, mainPath);
const clazz = sf.statements[2];
if (!isNamedClassDeclaration(clazz)) {
return fail('Expected class declaration');
}
const typeChecker = program.getTypeChecker();
const dtsReader =
new DtsMetadataReader(typeChecker, new TypeScriptReflectionHost(typeChecker));
const meta = dtsReader.getDirectiveMetadata(new Reference(clazz))!;
expect(meta.isStructural).toBeTrue();
});
});
});

View File

@ -36,12 +36,14 @@ export class TypeScriptReflectionHost implements ReflectionHost {
getConstructorParameters(clazz: ClassDeclaration): CtorParameter[]|null { getConstructorParameters(clazz: ClassDeclaration): CtorParameter[]|null {
const tsClazz = castDeclarationToClassOrDie(clazz); const tsClazz = castDeclarationToClassOrDie(clazz);
// First, find the constructor with a `body`. The constructors without a `body` are overloads const isDeclaration = tsClazz.getSourceFile().isDeclarationFile;
// whereas we want the implementation since it's the one that'll be executed and which can // For non-declaration files, we want to find the constructor with a `body`. The constructors
// have decorators. // without a `body` are overloads whereas we want the implementation since it's the one that'll
// be executed and which can have decorators. For declaration files, we take the first one that
// we get.
const ctor = tsClazz.members.find( const ctor = tsClazz.members.find(
(member): member is ts.ConstructorDeclaration => (member): member is ts.ConstructorDeclaration =>
ts.isConstructorDeclaration(member) && member.body !== undefined); ts.isConstructorDeclaration(member) && (isDeclaration || member.body !== undefined));
if (ctor === undefined) { if (ctor === undefined) {
return null; return null;
} }

View File

@ -249,6 +249,7 @@ function fakeDirective(ref: Reference<ClassDeclaration>): DirectiveMeta {
isGeneric: false, isGeneric: false,
baseClass: null, baseClass: null,
isPoisoned: false, isPoisoned: false,
isStructural: false,
}; };
} }

View File

@ -13,7 +13,7 @@ import * as ts from 'typescript';
import {FullTemplateMapping, TypeCheckableDirectiveMeta} from './api'; import {FullTemplateMapping, TypeCheckableDirectiveMeta} from './api';
import {GlobalCompletion} from './completion'; import {GlobalCompletion} from './completion';
import {DirectiveInScope, PipeInScope} from './scope'; import {DirectiveInScope, PipeInScope} from './scope';
import {DirectiveSymbol, ElementSymbol, ShimLocation, Symbol} from './symbols'; import {DirectiveSymbol, ElementSymbol, ShimLocation, Symbol, TemplateSymbol} from './symbols';
/** /**
* Interface to the Angular Template Type Checker to extract diagnostics and intelligence from the * Interface to the Angular Template Type Checker to extract diagnostics and intelligence from the
@ -111,6 +111,7 @@ export interface TemplateTypeChecker {
* @see Symbol * @see Symbol
*/ */
getSymbolOfNode(node: TmplAstElement, component: ts.ClassDeclaration): ElementSymbol|null; getSymbolOfNode(node: TmplAstElement, component: ts.ClassDeclaration): ElementSymbol|null;
getSymbolOfNode(node: TmplAstTemplate, component: ts.ClassDeclaration): TemplateSymbol|null;
getSymbolOfNode(node: AST|TmplAstNode, component: ts.ClassDeclaration): Symbol|null; getSymbolOfNode(node: AST|TmplAstNode, component: ts.ClassDeclaration): Symbol|null;
/** /**

View File

@ -32,6 +32,11 @@ export interface DirectiveInScope {
* `true` if this directive is a component. * `true` if this directive is a component.
*/ */
isComponent: boolean; isComponent: boolean;
/**
* `true` if this directive is a structural directive.
*/
isStructural: boolean;
} }
/** /**

View File

@ -16,7 +16,7 @@ import {ClassDeclaration, isNamedClassDeclaration, ReflectionHost} from '../../r
import {ComponentScopeReader, TypeCheckScopeRegistry} from '../../scope'; import {ComponentScopeReader, TypeCheckScopeRegistry} from '../../scope';
import {isShim} from '../../shims'; import {isShim} from '../../shims';
import {getSourceFileOrNull} from '../../util/src/typescript'; import {getSourceFileOrNull} from '../../util/src/typescript';
import {DirectiveInScope, ElementSymbol, FullTemplateMapping, GlobalCompletion, OptimizeFor, PipeInScope, ProgramTypeCheckAdapter, ShimLocation, Symbol, TemplateId, TemplateTypeChecker, TypeCheckableDirectiveMeta, TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from '../api'; import {DirectiveInScope, ElementSymbol, FullTemplateMapping, GlobalCompletion, OptimizeFor, PipeInScope, ProgramTypeCheckAdapter, ShimLocation, Symbol, TemplateId, TemplateSymbol, TemplateTypeChecker, TypeCheckableDirectiveMeta, TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from '../api';
import {TemplateDiagnostic} from '../diagnostics'; import {TemplateDiagnostic} from '../diagnostics';
import {CompletionEngine} from './completion'; import {CompletionEngine} from './completion';
@ -472,6 +472,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
} }
return this.state.get(path)!; return this.state.get(path)!;
} }
getSymbolOfNode(node: TmplAstTemplate, component: ts.ClassDeclaration): TemplateSymbol|null;
getSymbolOfNode(node: TmplAstElement, component: ts.ClassDeclaration): ElementSymbol|null; getSymbolOfNode(node: TmplAstElement, component: ts.ClassDeclaration): ElementSymbol|null;
getSymbolOfNode(node: AST|TmplAstNode, component: ts.ClassDeclaration): Symbol|null { getSymbolOfNode(node: AST|TmplAstNode, component: ts.ClassDeclaration): Symbol|null {
const builder = this.getOrCreateSymbolBuilder(component); const builder = this.getOrCreateSymbolBuilder(component);
@ -598,6 +599,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
data.directives.push({ data.directives.push({
isComponent: dir.isComponent, isComponent: dir.isComponent,
isStructural: dir.isStructural,
selector: dir.selector, selector: dir.selector,
tsSymbol, tsSymbol,
ngModule, ngModule,

View File

@ -140,7 +140,8 @@ export class SymbolBuilder {
selector: meta.selector, selector: meta.selector,
isComponent, isComponent,
ngModule, ngModule,
kind: SymbolKind.Directive kind: SymbolKind.Directive,
isStructural: meta.isStructural,
}; };
return directiveSymbol; return directiveSymbol;
}) })
@ -281,7 +282,7 @@ export class SymbolBuilder {
private getDirectiveSymbolForAccessExpression( private getDirectiveSymbolForAccessExpression(
node: ts.ElementAccessExpression|ts.PropertyAccessExpression, node: ts.ElementAccessExpression|ts.PropertyAccessExpression,
{isComponent, selector}: TypeCheckableDirectiveMeta): DirectiveSymbol|null { {isComponent, selector, isStructural}: TypeCheckableDirectiveMeta): DirectiveSymbol|null {
// In either case, `_t1["index"]` or `_t1.index`, `node.expression` is _t1. // In either case, `_t1["index"]` or `_t1.index`, `node.expression` is _t1.
// The retrieved symbol for _t1 will be the variable declaration. // The retrieved symbol for _t1 will be the variable declaration.
const tsSymbol = this.getTypeChecker().getSymbolAtLocation(node.expression); const tsSymbol = this.getTypeChecker().getSymbolAtLocation(node.expression);
@ -313,6 +314,7 @@ export class SymbolBuilder {
tsType: symbol.tsType, tsType: symbol.tsType,
shimLocation: symbol.shimLocation, shimLocation: symbol.shimLocation,
isComponent, isComponent,
isStructural,
selector, selector,
ngModule, ngModule,
}; };

View File

@ -505,6 +505,7 @@ function prepareDeclarations(
isGeneric: decl.isGeneric ?? false, isGeneric: decl.isGeneric ?? false,
outputs: ClassPropertyMapping.fromMappedObject(decl.outputs || {}), outputs: ClassPropertyMapping.fromMappedObject(decl.outputs || {}),
queries: decl.queries || [], queries: decl.queries || [],
isStructural: false,
}; };
matcher.addSelectables(selector, meta); matcher.addSelectables(selector, meta);
} }
@ -567,6 +568,7 @@ function makeScope(program: ts.Program, sf: ts.SourceFile, decls: TestDeclaratio
undeclaredInputFields: new Set(decl.undeclaredInputFields ?? []), undeclaredInputFields: new Set(decl.undeclaredInputFields ?? []),
isGeneric: decl.isGeneric ?? false, isGeneric: decl.isGeneric ?? false,
isPoisoned: false, isPoisoned: false,
isStructural: false,
}); });
} else if (decl.type === 'pipe') { } else if (decl.type === 'pipe') {
scope.pipes.push({ scope.pipes.push({

View File

@ -74,6 +74,8 @@ export interface DirectiveMeta {
* Null otherwise * Null otherwise
*/ */
exportAs: string[]|null; exportAs: string[]|null;
isStructural: boolean;
} }
/** /**

View File

@ -38,6 +38,7 @@ function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta> {
inputs: new IdentityInputMapping(['ngForOf']), inputs: new IdentityInputMapping(['ngForOf']),
outputs: new IdentityInputMapping([]), outputs: new IdentityInputMapping([]),
isComponent: false, isComponent: false,
isStructural: true,
selector: '[ngFor][ngForOf]', selector: '[ngFor][ngForOf]',
}); });
matcher.addSelectables(CssSelector.parse('[dir]'), { matcher.addSelectables(CssSelector.parse('[dir]'), {
@ -46,6 +47,7 @@ function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta> {
inputs: new IdentityInputMapping([]), inputs: new IdentityInputMapping([]),
outputs: new IdentityInputMapping([]), outputs: new IdentityInputMapping([]),
isComponent: false, isComponent: false,
isStructural: false,
selector: '[dir]' selector: '[dir]'
}); });
matcher.addSelectables(CssSelector.parse('[hasOutput]'), { matcher.addSelectables(CssSelector.parse('[hasOutput]'), {
@ -54,6 +56,7 @@ function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta> {
inputs: new IdentityInputMapping([]), inputs: new IdentityInputMapping([]),
outputs: new IdentityInputMapping(['outputBinding']), outputs: new IdentityInputMapping(['outputBinding']),
isComponent: false, isComponent: false,
isStructural: false,
selector: '[hasOutput]' selector: '[hasOutput]'
}); });
matcher.addSelectables(CssSelector.parse('[hasInput]'), { matcher.addSelectables(CssSelector.parse('[hasInput]'), {
@ -62,6 +65,7 @@ function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta> {
inputs: new IdentityInputMapping(['inputBinding']), inputs: new IdentityInputMapping(['inputBinding']),
outputs: new IdentityInputMapping([]), outputs: new IdentityInputMapping([]),
isComponent: false, isComponent: false,
isStructural: false,
selector: '[hasInput]' selector: '[hasInput]'
}); });
return matcher; return matcher;
@ -107,6 +111,7 @@ describe('t2 binding', () => {
inputs: new IdentityInputMapping([]), inputs: new IdentityInputMapping([]),
outputs: new IdentityInputMapping([]), outputs: new IdentityInputMapping([]),
isComponent: false, isComponent: false,
isStructural: false,
selector: 'text[dir]' selector: 'text[dir]'
}); });
const binder = new R3TargetBinder(matcher); const binder = new R3TargetBinder(matcher);