Skip to content

Commit 4b0f733

Browse files
crisbetommalerba
authored andcommitted
refactor(compiler): add more information to template binder (angular#60977)
Updates the template binder to include information about directives owned by a specific component/directive node and the names of template symbols that don't exist. These will be used when generating the type check block. PR Close angular#60977
1 parent 26cba08 commit 4b0f733

File tree

5 files changed

+136
-27
lines changed

5 files changed

+136
-27
lines changed

packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -826,7 +826,7 @@ function prepareDeclarations(
826826
for (const meta of directives) {
827827
registry.set(meta.name, [meta, ...hostDirectiveResolder.resolve(meta)]);
828828
}
829-
return {matcher: new SelectorlessMatcher<DirectiveMeta[]>(registry), pipes};
829+
return {matcher: new SelectorlessMatcher<DirectiveMeta>(registry), pipes};
830830
} else {
831831
const matcher = new SelectorMatcher<DirectiveMeta[]>();
832832
for (const meta of directives) {

packages/compiler/src/directive_matching.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -474,9 +474,9 @@ export class SelectorContext<T = any> {
474474
}
475475

476476
export class SelectorlessMatcher<T = unknown> {
477-
constructor(private registry: Map<string, T>) {}
477+
constructor(private registry: Map<string, T[]>) {}
478478

479-
match(name: string): T | null {
480-
return this.registry.get(name) ?? null;
479+
match(name: string): T[] {
480+
return this.registry.has(name) ? this.registry.get(name)! : [];
481481
}
482482
}

packages/compiler/src/render3/view/t2_api.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,5 +272,18 @@ export interface BoundTarget<DirectiveT extends DirectiveMeta> {
272272
/**
273273
* Whether a given node is located in a `@defer` block.
274274
*/
275-
isDeferred(node: Element | Component): boolean;
275+
isDeferred(node: Element): boolean;
276+
277+
/**
278+
* Returns the directives that are owned by a specific component/directive node. This is either
279+
* the directive being referenced itself or its host directives.
280+
* @param node Node for which to retrieve the owned directives.
281+
*/
282+
getOwnedDirectives(node: Component | Directive): DirectiveT[] | null;
283+
284+
/**
285+
* Checks whether a component/directive that was referenced directly in the template exists.
286+
* @param name Name of the component/directive.
287+
*/
288+
referencedDirectiveExists(name: string): boolean;
276289
}

packages/compiler/src/render3/view/t2_binder.ts

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ type ReferenceMap<DirectiveT> = Map<
9393
>;
9494

9595
/** Mapping between AST nodes and the directives that have been matched on them. */
96-
type MatchedDirectives<DirectiveT> = Map<Template | Element | Component, DirectiveT[]>;
96+
type MatchedDirectives<DirectiveT> = Map<Template | Element | Component | Directive, DirectiveT[]>;
97+
98+
/** Mapping between AST nodes and the directives that they own. */
99+
type OwnedDirectives<DirectiveT> = Map<Component | Directive, DirectiveT[]>;
97100

98101
/**
99102
* Mapping between a scoped not and the template entities that exist in it.
@@ -162,7 +165,7 @@ export function findMatchingDirectivesAndPipes(template: string, directiveSelect
162165
/** Object used to match template nodes to directives. */
163166
export type DirectiveMatcher<DirectiveT extends DirectiveMeta> =
164167
| SelectorMatcher<DirectiveT[]>
165-
| SelectorlessMatcher<DirectiveT[]>;
168+
| SelectorlessMatcher<DirectiveT>;
166169

167170
/**
168171
* Processes `Target`s with a given set of directives and performs a binding operation, which
@@ -182,7 +185,9 @@ export class R3TargetBinder<DirectiveT extends DirectiveMeta> implements TargetB
182185
}
183186

184187
const directives: MatchedDirectives<DirectiveT> = new Map();
188+
const ownedDirectives: OwnedDirectives<DirectiveT> = new Map();
185189
const eagerDirectives: DirectiveT[] = [];
190+
const missingDirectives = new Set<string>();
186191
const bindings: BindingsMap<DirectiveT> = new Map();
187192
const references: ReferenceMap<DirectiveT> = new Map();
188193
const scopedNodeEntities: ScopedNodeEntities = new Map();
@@ -210,7 +215,9 @@ export class R3TargetBinder<DirectiveT extends DirectiveMeta> implements TargetB
210215
target.template,
211216
this.directiveMatcher,
212217
directives,
218+
ownedDirectives,
213219
eagerDirectives,
220+
missingDirectives,
214221
bindings,
215222
references,
216223
);
@@ -247,7 +254,9 @@ export class R3TargetBinder<DirectiveT extends DirectiveMeta> implements TargetB
247254
return new R3BoundTarget(
248255
target,
249256
directives,
257+
ownedDirectives,
250258
eagerDirectives,
259+
missingDirectives,
251260
bindings,
252261
references,
253262
expressions,
@@ -503,7 +512,9 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
503512
private constructor(
504513
private directiveMatcher: DirectiveMatcher<DirectiveT>,
505514
private directives: MatchedDirectives<DirectiveT>,
515+
private ownedDirectives: OwnedDirectives<DirectiveT>,
506516
private eagerDirectives: DirectiveT[],
517+
private missingDirectives: Set<string>,
507518
private bindings: BindingsMap<DirectiveT>,
508519
private references: ReferenceMap<DirectiveT>,
509520
) {}
@@ -524,14 +535,18 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
524535
template: Node[],
525536
directiveMatcher: DirectiveMatcher<DirectiveT>,
526537
directives: MatchedDirectives<DirectiveT>,
538+
ownedDirectives: OwnedDirectives<DirectiveT>,
527539
eagerDirectives: DirectiveT[],
540+
missingDirectives: Set<string>,
528541
bindings: BindingsMap<DirectiveT>,
529542
references: ReferenceMap<DirectiveT>,
530543
): void {
531544
const matcher = new DirectiveBinder(
532545
directiveMatcher,
533546
directives,
547+
ownedDirectives,
534548
eagerDirectives,
549+
missingDirectives,
535550
bindings,
536551
references,
537552
);
@@ -606,29 +621,30 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
606621
}
607622

608623
visitComponent(node: Component): void {
609-
const directives: DirectiveT[] = [];
610-
let componentMetas: DirectiveT[] | null = null;
611-
612624
if (this.directiveMatcher instanceof SelectorlessMatcher) {
613-
componentMetas = this.directiveMatcher.match(node.componentName);
625+
const componentMatches = this.directiveMatcher.match(node.componentName);
626+
const directives: DirectiveT[] = [];
614627

615-
if (componentMetas !== null) {
616-
directives.push(...componentMetas);
628+
if (componentMatches.length > 0) {
629+
directives.push(...componentMatches);
630+
this.ownedDirectives.set(node, componentMatches);
631+
} else {
632+
this.missingDirectives.add(node.componentName);
617633
}
618634

619635
for (const directive of node.directives) {
620636
const directiveMetas = this.directiveMatcher.match(directive.name);
621637

622-
if (directiveMetas !== null) {
638+
if (directiveMetas.length > 0) {
623639
directives.push(...directiveMetas);
640+
this.ownedDirectives.set(directive, directiveMetas);
641+
} else {
642+
this.missingDirectives.add(directive.name);
624643
}
625644
}
626-
}
627-
628-
this.trackMatchedDirectives(node, directives);
629645

630-
if (componentMetas !== null) {
631-
this.trackSelectorlessBindings(node, componentMetas);
646+
this.trackMatchedDirectives(node, directives);
647+
this.trackSelectorlessBindings(node, componentMatches);
632648
}
633649

634650
node.directives.forEach((directive) => directive.visit(this));
@@ -641,7 +657,7 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
641657
? this.directiveMatcher.match(node.name)
642658
: null;
643659

644-
if (directives !== null) {
660+
if (directives !== null && directives.length > 0) {
645661
this.trackSelectorlessBindings(node, directives);
646662
}
647663
}
@@ -663,8 +679,11 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
663679
for (const directive of node.directives) {
664680
const matchedDirectives = this.directiveMatcher.match(directive.name);
665681

666-
if (matchedDirectives !== null) {
682+
if (matchedDirectives.length > 0) {
667683
directives.push(...matchedDirectives);
684+
this.ownedDirectives.set(directive, matchedDirectives);
685+
} else {
686+
this.missingDirectives.add(directive.name);
668687
}
669688
}
670689
}
@@ -687,6 +706,10 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
687706
}
688707

689708
private trackSelectorlessBindings(node: Component | Directive, metas: DirectiveT[]): void {
709+
if (metas.length === 0) {
710+
return;
711+
}
712+
690713
const setBinding = (
691714
meta: DirectiveT,
692715
attribute: BoundAttribute | BoundEvent | TextAttribute,
@@ -706,9 +729,7 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
706729
// TODO(crisbeto): currently it's unclear how references should behave under selectorless,
707730
// given that there's one named class which can bring in multiple host directives.
708731
// For the time being only register the first directive as the reference target.
709-
if (metas.length > 0) {
710-
node.references.forEach((ref) => this.references.set(ref, {directive: metas[0], node: node}));
711-
}
732+
node.references.forEach((ref) => this.references.set(ref, {directive: metas[0], node: node}));
712733
}
713734

714735
private trackSelectorMatchedBindings(node: Element | Template, directives: DirectiveT[]): void {
@@ -1118,7 +1139,9 @@ class R3BoundTarget<DirectiveT extends DirectiveMeta> implements BoundTarget<Dir
11181139
constructor(
11191140
readonly target: Target,
11201141
private directives: MatchedDirectives<DirectiveT>,
1142+
private ownedDirectives: OwnedDirectives<DirectiveT>,
11211143
private eagerDirectives: DirectiveT[],
1144+
private missingDirectives: Set<string>,
11221145
private bindings: BindingsMap<DirectiveT>,
11231146
private references: ReferenceMap<DirectiveT>,
11241147
private exprTargets: Map<AST, TemplateEntity>,
@@ -1137,7 +1160,7 @@ class R3BoundTarget<DirectiveT extends DirectiveMeta> implements BoundTarget<Dir
11371160
return this.scopedNodeEntities.get(node) ?? new Set();
11381161
}
11391162

1140-
getDirectivesOfNode(node: Element | Template): DirectiveT[] | null {
1163+
getDirectivesOfNode(node: Element | Template | Component): DirectiveT[] | null {
11411164
return this.directives.get(node) || null;
11421165
}
11431166

@@ -1251,7 +1274,7 @@ class R3BoundTarget<DirectiveT extends DirectiveMeta> implements BoundTarget<Dir
12511274
return null;
12521275
}
12531276

1254-
isDeferred(element: Element | Component): boolean {
1277+
isDeferred(element: Element): boolean {
12551278
for (const block of this.deferredBlocks) {
12561279
if (!this.deferredScopes.has(block)) {
12571280
continue;
@@ -1273,6 +1296,14 @@ class R3BoundTarget<DirectiveT extends DirectiveMeta> implements BoundTarget<Dir
12731296
return false;
12741297
}
12751298

1299+
getOwnedDirectives(node: Component | Directive): DirectiveT[] | null {
1300+
return this.ownedDirectives.get(node) || null;
1301+
}
1302+
1303+
referencedDirectiveExists(name: string): boolean {
1304+
return !this.missingDirectives.has(name);
1305+
}
1306+
12761307
/**
12771308
* Finds an entity with a specific name in a scope.
12781309
* @param rootNode Root node of the scope.

packages/compiler/test/render3/view/binding_spec.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1032,7 +1032,7 @@ describe('t2 binding', () => {
10321032

10331033
function makeSelectorlessMatcher(
10341034
directives: DirectiveMeta[],
1035-
): SelectorlessMatcher<DirectiveMeta[]> {
1035+
): SelectorlessMatcher<DirectiveMeta> {
10361036
const registry = new Map<string, DirectiveMeta[]>();
10371037

10381038
for (const dir of directives) {
@@ -1282,5 +1282,70 @@ describe('t2 binding', () => {
12821282
expect(res.getUsedDirectives().map((dir) => dir.name)).toEqual(['MyComp', 'Dir']);
12831283
expect(res.getEagerlyUsedDirectives().map((dir) => dir.name)).toEqual(['MyComp', 'Dir']);
12841284
});
1285+
1286+
it('should get the directives that are owned by a specific directive', () => {
1287+
const template = parseTemplate('<MyComp @Dir/>', '', options);
1288+
const registry = new Map<string, DirectiveMeta[]>([
1289+
[
1290+
'MyComp',
1291+
[
1292+
{
1293+
...baseMeta,
1294+
name: 'MyComp',
1295+
isComponent: true,
1296+
},
1297+
{
1298+
...baseMeta,
1299+
name: 'CompHostDir',
1300+
},
1301+
],
1302+
],
1303+
[
1304+
'Dir',
1305+
[
1306+
{
1307+
...baseMeta,
1308+
name: 'Dir',
1309+
},
1310+
{
1311+
...baseMeta,
1312+
name: 'HostDir',
1313+
},
1314+
],
1315+
],
1316+
]);
1317+
1318+
const binder = new R3TargetBinder(new SelectorlessMatcher(registry));
1319+
const res = binder.bind({template: template.nodes});
1320+
const component = template.nodes[0] as a.Component;
1321+
const directive = component.directives[0];
1322+
expect(res.getOwnedDirectives(component)?.map((d) => d.name)).toEqual([
1323+
'MyComp',
1324+
'CompHostDir',
1325+
]);
1326+
expect(res.getOwnedDirectives(directive)?.map((d) => d.name)).toEqual(['Dir', 'HostDir']);
1327+
});
1328+
1329+
it('should check whether a referenced directive exists', () => {
1330+
const template = parseTemplate('<MyComp @MissingDir/><MissingComp @Dir/>', '', options);
1331+
const binder = new R3TargetBinder(
1332+
makeSelectorlessMatcher([
1333+
{
1334+
...baseMeta,
1335+
name: 'MyComp',
1336+
isComponent: true,
1337+
},
1338+
{
1339+
...baseMeta,
1340+
name: 'Dir',
1341+
},
1342+
]),
1343+
);
1344+
const res = binder.bind({template: template.nodes});
1345+
expect(res.referencedDirectiveExists('MyComp')).toBe(true);
1346+
expect(res.referencedDirectiveExists('Dir')).toBe(true);
1347+
expect(res.referencedDirectiveExists('MissingDir')).toBe(false);
1348+
expect(res.referencedDirectiveExists('MissingComp')).toBe(false);
1349+
});
12851350
});
12861351
});

0 commit comments

Comments
 (0)