From c55bf4a4a3127936b6714d9397416086f06df79a Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Mon, 7 Dec 2020 17:15:37 -0800 Subject: [PATCH] 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 --- .../src/ngtsc/annotations/src/component.ts | 1 + .../src/ngtsc/annotations/src/directive.ts | 18 +++- .../ngtsc/annotations/test/directive_spec.ts | 25 +++++ .../src/ngtsc/indexer/test/util.ts | 1 + .../src/ngtsc/metadata/src/api.ts | 5 + .../src/ngtsc/metadata/src/dts.ts | 18 +++- .../src/ngtsc/metadata/src/inheritance.ts | 4 + .../src/ngtsc/metadata/test/BUILD.bazel | 35 +++++++ .../src/ngtsc/metadata/test/dts_spec.ts | 93 +++++++++++++++++++ .../src/ngtsc/reflection/src/typescript.ts | 10 +- .../src/ngtsc/scope/test/local_spec.ts | 1 + .../src/ngtsc/typecheck/api/checker.ts | 3 +- .../src/ngtsc/typecheck/api/scope.ts | 5 + .../src/ngtsc/typecheck/src/checker.ts | 4 +- .../typecheck/src/template_symbol_builder.ts | 6 +- .../src/ngtsc/typecheck/test/test_utils.ts | 2 + packages/compiler/src/render3/view/t2_api.ts | 2 + .../test/render3/view/binding_spec.ts | 5 + 18 files changed, 227 insertions(+), 11 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/metadata/test/BUILD.bazel create mode 100644 packages/compiler-cli/src/ngtsc/metadata/test/dts_spec.ts diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 172276dee0..7ec79625eb 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -396,6 +396,7 @@ export class ComponentDecoratorHandler implements baseClass: analysis.baseClass, ...analysis.typeCheckMeta, isPoisoned: analysis.isPoisoned, + isStructural: false, }); this.resourceRegistry.registerResources(analysis.resources, node); diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index f07b3d0411..c0214ad069 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -6,7 +6,7 @@ * 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 {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; @@ -42,6 +42,7 @@ export interface DirectiveHandlerData { inputs: ClassPropertyMapping; outputs: ClassPropertyMapping; isPoisoned: boolean; + isStructural: boolean; } export class DirectiveDecoratorHandler implements @@ -109,6 +110,7 @@ export class DirectiveDecoratorHandler implements typeCheckMeta: extractDirectiveTypeCheckMeta(node, directiveResult.inputs, this.reflector), providersRequiringFactory, isPoisoned: false, + isStructural: directiveResult.isStructural, } }; } @@ -129,6 +131,7 @@ export class DirectiveDecoratorHandler implements baseClass: analysis.baseClass, ...analysis.typeCheckMeta, isPoisoned: analysis.isPoisoned, + isStructural: analysis.isStructural, }); this.injectableRegistry.registerInjectable(node); @@ -226,6 +229,7 @@ export function extractDirectiveMetadata( metadata: R3DirectiveMetadata, inputs: ClassPropertyMapping, outputs: ClassPropertyMapping, + isStructural: boolean; }|undefined { let directive: Map; if (decorator === null || decorator.args === null || decorator.args.length === 0) { @@ -352,6 +356,17 @@ export function extractDirectiveMetadata( 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 const usesInheritance = reflector.hasBaseClass(clazz); const type = wrapTypeReference(reflector, clazz); @@ -386,6 +401,7 @@ export function extractDirectiveMetadata( metadata, inputs, outputs, + isStructural, }; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts index 630980dc0d..fbe877eea1 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts @@ -105,6 +105,7 @@ runInEachFileSystem(() => { isComponent: false, name: 'Dir', selector: '[dir]', + isStructural: false, }; matcher.addSelectables(CssSelector.parse('[dir]'), dirMeta); @@ -118,6 +119,30 @@ runInEachFileSystem(() => { // and field names. 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 diff --git a/packages/compiler-cli/src/ngtsc/indexer/test/util.ts b/packages/compiler-cli/src/ngtsc/indexer/test/util.ts index 7986cab520..0ec5238cb3 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/test/util.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/test/util.ts @@ -54,6 +54,7 @@ export function getBoundTemplate( inputs: ClassPropertyMapping.fromMappedObject({}), outputs: ClassPropertyMapping.fromMappedObject({}), exportAs: null, + isStructural: false, }); }); const binder = new R3TargetBinder(matcher); diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/api.ts b/packages/compiler-cli/src/ngtsc/metadata/src/api.ts index 37accc62bb..0b48090de9 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/api.ts @@ -114,6 +114,11 @@ export interface DirectiveMeta extends T2DirectiveMeta, DirectiveTypeCheckMeta { * and reliable metadata. */ isPoisoned: boolean; + + /** + * Whether the directive is likely a structural directive (injects `TemplateRef`). + */ + isStructural: boolean; } /** diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts b/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts index 5c693a92e4..d88bf1a296 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts @@ -9,7 +9,7 @@ import * as ts from 'typescript'; 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 {ClassPropertyMapping} from './property_mapping'; @@ -77,6 +77,19 @@ export class DtsMetadataReader implements MetadataReader { 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 = ClassPropertyMapping.fromMappedObject(readStringMapType(def.type.typeArguments[3])); const outputs = @@ -84,7 +97,7 @@ export class DtsMetadataReader implements MetadataReader { return { ref, name: clazz.name.text, - isComponent: def.name === 'ɵcmp', + isComponent, selector: readStringType(def.type.typeArguments[1]), exportAs: readStringArrayType(def.type.typeArguments[2]), inputs, @@ -93,6 +106,7 @@ export class DtsMetadataReader implements MetadataReader { ...extractDirectiveTypeCheckMeta(clazz, inputs, this.reflector), baseClass: readBaseClass(clazz, this.checker, this.reflector), isPoisoned: false, + isStructural, }; } diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts b/packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts index b923de6e2a..097e1ef422 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts @@ -37,6 +37,7 @@ export function flattenInheritedDirectiveMetadata( let isDynamic = false; let inputs = ClassPropertyMapping.empty(); let outputs = ClassPropertyMapping.empty(); + let isStructural: boolean = false; const addMetadata = (meta: DirectiveMeta): void => { if (meta.baseClass === 'dynamic') { @@ -51,6 +52,8 @@ export function flattenInheritedDirectiveMetadata( } } + isStructural = isStructural || meta.isStructural; + inputs = ClassPropertyMapping.merge(inputs, meta.inputs); outputs = ClassPropertyMapping.merge(outputs, meta.outputs); @@ -79,5 +82,6 @@ export function flattenInheritedDirectiveMetadata( restrictedInputFields, stringLiteralInputFields, baseClass: isDynamic ? 'dynamic' : null, + isStructural, }; } diff --git a/packages/compiler-cli/src/ngtsc/metadata/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/metadata/test/BUILD.bazel new file mode 100644 index 0000000000..a1e12f6bd7 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/metadata/test/BUILD.bazel @@ -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", + ], +) diff --git a/packages/compiler-cli/src/ngtsc/metadata/test/dts_spec.ts b/packages/compiler-cli/src/ngtsc/metadata/test/dts_spec.ts new file mode 100644 index 0000000000..bb785afa46 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/metadata/test/dts_spec.ts @@ -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 + } + ` + }], + { + 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 + } + ` + }], + { + 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(); + }); + }); +}); diff --git a/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts b/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts index 263a31d534..451d0c9ae3 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts @@ -36,12 +36,14 @@ export class TypeScriptReflectionHost implements ReflectionHost { getConstructorParameters(clazz: ClassDeclaration): CtorParameter[]|null { const tsClazz = castDeclarationToClassOrDie(clazz); - // First, find the constructor with a `body`. The constructors without a `body` are overloads - // whereas we want the implementation since it's the one that'll be executed and which can - // have decorators. + const isDeclaration = tsClazz.getSourceFile().isDeclarationFile; + // For non-declaration files, we want to find the constructor with a `body`. The constructors + // 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( (member): member is ts.ConstructorDeclaration => - ts.isConstructorDeclaration(member) && member.body !== undefined); + ts.isConstructorDeclaration(member) && (isDeclaration || member.body !== undefined)); if (ctor === undefined) { return null; } diff --git a/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts b/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts index dd289671fe..38a8ad79dd 100644 --- a/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts +++ b/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts @@ -249,6 +249,7 @@ function fakeDirective(ref: Reference): DirectiveMeta { isGeneric: false, baseClass: null, isPoisoned: false, + isStructural: false, }; } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts index 69dcaebb4e..c65016b284 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts @@ -13,7 +13,7 @@ import * as ts from 'typescript'; import {FullTemplateMapping, TypeCheckableDirectiveMeta} from './api'; import {GlobalCompletion} from './completion'; 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 @@ -111,6 +111,7 @@ export interface TemplateTypeChecker { * @see Symbol */ 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; /** diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts index 54763b77a3..6593d0a63b 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts @@ -32,6 +32,11 @@ export interface DirectiveInScope { * `true` if this directive is a component. */ isComponent: boolean; + + /** + * `true` if this directive is a structural directive. + */ + isStructural: boolean; } /** diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts index ba92ab7e07..0cd7b9de3b 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts @@ -16,7 +16,7 @@ import {ClassDeclaration, isNamedClassDeclaration, ReflectionHost} from '../../r import {ComponentScopeReader, TypeCheckScopeRegistry} from '../../scope'; import {isShim} from '../../shims'; 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 {CompletionEngine} from './completion'; @@ -472,6 +472,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { } return this.state.get(path)!; } + getSymbolOfNode(node: TmplAstTemplate, component: ts.ClassDeclaration): TemplateSymbol|null; getSymbolOfNode(node: TmplAstElement, component: ts.ClassDeclaration): ElementSymbol|null; getSymbolOfNode(node: AST|TmplAstNode, component: ts.ClassDeclaration): Symbol|null { const builder = this.getOrCreateSymbolBuilder(component); @@ -598,6 +599,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { data.directives.push({ isComponent: dir.isComponent, + isStructural: dir.isStructural, selector: dir.selector, tsSymbol, ngModule, diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts index a3c2e51336..0342b11066 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts @@ -140,7 +140,8 @@ export class SymbolBuilder { selector: meta.selector, isComponent, ngModule, - kind: SymbolKind.Directive + kind: SymbolKind.Directive, + isStructural: meta.isStructural, }; return directiveSymbol; }) @@ -281,7 +282,7 @@ export class SymbolBuilder { private getDirectiveSymbolForAccessExpression( 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. // The retrieved symbol for _t1 will be the variable declaration. const tsSymbol = this.getTypeChecker().getSymbolAtLocation(node.expression); @@ -313,6 +314,7 @@ export class SymbolBuilder { tsType: symbol.tsType, shimLocation: symbol.shimLocation, isComponent, + isStructural, selector, ngModule, }; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts index 320efaf75b..862c8c2a0c 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts @@ -505,6 +505,7 @@ function prepareDeclarations( isGeneric: decl.isGeneric ?? false, outputs: ClassPropertyMapping.fromMappedObject(decl.outputs || {}), queries: decl.queries || [], + isStructural: false, }; matcher.addSelectables(selector, meta); } @@ -567,6 +568,7 @@ function makeScope(program: ts.Program, sf: ts.SourceFile, decls: TestDeclaratio undeclaredInputFields: new Set(decl.undeclaredInputFields ?? []), isGeneric: decl.isGeneric ?? false, isPoisoned: false, + isStructural: false, }); } else if (decl.type === 'pipe') { scope.pipes.push({ diff --git a/packages/compiler/src/render3/view/t2_api.ts b/packages/compiler/src/render3/view/t2_api.ts index e87ee6cb94..7946b3b5a7 100644 --- a/packages/compiler/src/render3/view/t2_api.ts +++ b/packages/compiler/src/render3/view/t2_api.ts @@ -74,6 +74,8 @@ export interface DirectiveMeta { * Null otherwise */ exportAs: string[]|null; + + isStructural: boolean; } /** diff --git a/packages/compiler/test/render3/view/binding_spec.ts b/packages/compiler/test/render3/view/binding_spec.ts index 5d792c338e..fe2f98863b 100644 --- a/packages/compiler/test/render3/view/binding_spec.ts +++ b/packages/compiler/test/render3/view/binding_spec.ts @@ -38,6 +38,7 @@ function makeSelectorMatcher(): SelectorMatcher { inputs: new IdentityInputMapping(['ngForOf']), outputs: new IdentityInputMapping([]), isComponent: false, + isStructural: true, selector: '[ngFor][ngForOf]', }); matcher.addSelectables(CssSelector.parse('[dir]'), { @@ -46,6 +47,7 @@ function makeSelectorMatcher(): SelectorMatcher { inputs: new IdentityInputMapping([]), outputs: new IdentityInputMapping([]), isComponent: false, + isStructural: false, selector: '[dir]' }); matcher.addSelectables(CssSelector.parse('[hasOutput]'), { @@ -54,6 +56,7 @@ function makeSelectorMatcher(): SelectorMatcher { inputs: new IdentityInputMapping([]), outputs: new IdentityInputMapping(['outputBinding']), isComponent: false, + isStructural: false, selector: '[hasOutput]' }); matcher.addSelectables(CssSelector.parse('[hasInput]'), { @@ -62,6 +65,7 @@ function makeSelectorMatcher(): SelectorMatcher { inputs: new IdentityInputMapping(['inputBinding']), outputs: new IdentityInputMapping([]), isComponent: false, + isStructural: false, selector: '[hasInput]' }); return matcher; @@ -107,6 +111,7 @@ describe('t2 binding', () => { inputs: new IdentityInputMapping([]), outputs: new IdentityInputMapping([]), isComponent: false, + isStructural: false, selector: 'text[dir]' }); const binder = new R3TargetBinder(matcher);