Skip to content

Commit fb1fc82

Browse files
committed
fix(compiler-cli): correctly type check host listeners to own outputs (#62965)
Currently the code that type checks host bindings assumes that all listeners are bound to the DOM, however that's not the case since host bindings can also bind to own outputs. These changes update the TCB to generate the proper code for type checking such outputs. Fixes #62783. PR Close #62965
1 parent 0950524 commit fb1fc82

File tree

7 files changed

+277
-49
lines changed

7 files changed

+277
-49
lines changed

packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,7 +1116,8 @@ export class ComponentDecoratorHandler
11161116
if (!ts.isClassDeclaration(node) || (meta.isPoisoned && !this.usePoisonedData)) {
11171117
return;
11181118
}
1119-
const scope = this.typeCheckScopeRegistry.getTypeCheckScope(node);
1119+
const ref = new Reference(node);
1120+
const scope = this.typeCheckScopeRegistry.getTypeCheckScope(ref);
11201121
if (scope.isPoisoned && !this.usePoisonedData) {
11211122
// Don't type-check components that had errors in their scopes, unless requested.
11221123
return;
@@ -1143,15 +1144,16 @@ export class ComponentDecoratorHandler
11431144
)
11441145
: null;
11451146
const hostBindingsContext: HostBindingsContext | null =
1146-
hostElement === null
1147+
hostElement === null || scope.directivesOnHost === null
11471148
? null
11481149
: {
11491150
node: hostElement,
1151+
directives: scope.directivesOnHost,
11501152
sourceMapping: {type: 'direct', node},
11511153
};
11521154

11531155
ctx.addDirective(
1154-
new Reference(node),
1156+
ref,
11551157
binder,
11561158
scope.schemas,
11571159
templateContext,

packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,8 @@ export class DirectiveDecoratorHandler
331331
if (!ts.isClassDeclaration(node) || (meta.isPoisoned && !this.usePoisonedData)) {
332332
return;
333333
}
334-
const scope = this.typeCheckScopeRegistry.getTypeCheckScope(node);
334+
const ref = new Reference(node);
335+
const scope = this.typeCheckScopeRegistry.getTypeCheckScope(ref);
335336
if (scope.isPoisoned && !this.usePoisonedData) {
336337
// Don't type-check components that had errors in their scopes, unless requested.
337338
return;
@@ -346,15 +347,16 @@ export class DirectiveDecoratorHandler
346347
meta.hostBindingNodes.listenerDecorators,
347348
);
348349

349-
if (hostElement !== null) {
350+
if (hostElement !== null && scope.directivesOnHost !== null) {
350351
const binder = new R3TargetBinder<TypeCheckableDirectiveMeta>(scope.matcher);
351352
const hostBindingsContext: HostBindingsContext = {
352353
node: hostElement,
354+
directives: scope.directivesOnHost,
353355
sourceMapping: {type: 'direct', node},
354356
};
355357

356358
ctx.addDirective(
357-
new Reference(node),
359+
ref,
358360
binder,
359361
scope.schemas,
360362
null,

packages/compiler-cli/src/ngtsc/scope/src/typecheck.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ export interface TypeCheckScope {
5757
* (contained semantic errors during its production).
5858
*/
5959
isPoisoned: boolean;
60+
61+
/**
62+
* Directives that have been set on the host of the scope.
63+
*/
64+
directivesOnHost: DirectiveMeta[] | null;
6065
}
6166

6267
/**
@@ -85,10 +90,12 @@ export class TypeCheckScopeRegistry {
8590
* contains an error, then 'error' is returned. If the component is not declared in any NgModule,
8691
* an empty type-check scope is returned.
8792
*/
88-
getTypeCheckScope(node: ClassDeclaration): TypeCheckScope {
93+
getTypeCheckScope(ref: Reference<ClassDeclaration>): TypeCheckScope {
8994
const directives: DirectiveMeta[] = [];
9095
const pipes = new Map<string, PipeMeta>();
91-
const scope = this.scopeReader.getScopeForComponent(node);
96+
const scope = this.scopeReader.getScopeForComponent(ref.node);
97+
const hostMeta = this.getTypeCheckDirectiveMetadata(ref);
98+
const directivesOnHost = hostMeta === null ? null : this.combineWithHostDirectives(hostMeta);
9299

93100
if (scope === null) {
94101
return {
@@ -97,6 +104,7 @@ export class TypeCheckScopeRegistry {
97104
pipes,
98105
schemas: [],
99106
isPoisoned: false,
107+
directivesOnHost,
100108
};
101109
}
102110

@@ -149,11 +157,13 @@ export class TypeCheckScopeRegistry {
149157
directives,
150158
pipes,
151159
schemas: scope.schemas,
160+
directivesOnHost,
152161
isPoisoned:
153162
scope.kind === ComponentScopeKind.NgModule
154163
? scope.compilation.isPoisoned || scope.exported.isPoisoned
155164
: scope.isPoisoned,
156165
};
166+
157167
this.scopeCache.set(cacheKey, typeCheckScope);
158168
return typeCheckScope;
159169
}
@@ -193,10 +203,10 @@ export class TypeCheckScopeRegistry {
193203

194204
// Carry over the `isExplicitlyDeferred` flag from the dependency info.
195205
const directiveMeta = this.applyExplicitlyDeferredFlag(extMeta, meta.isExplicitlyDeferred);
196-
matcher.addSelectables(CssSelector.parse(meta.selector), [
197-
...this.hostDirectivesResolver.resolve(directiveMeta),
198-
directiveMeta,
199-
]);
206+
matcher.addSelectables(
207+
CssSelector.parse(meta.selector),
208+
this.combineWithHostDirectives(directiveMeta),
209+
);
200210
}
201211
}
202212

@@ -210,10 +220,14 @@ export class TypeCheckScopeRegistry {
210220
const extMeta =
211221
dep.kind === MetaKind.Directive ? this.getTypeCheckDirectiveMetadata(dep.ref) : null;
212222
if (extMeta !== null) {
213-
registry.set(name, [extMeta, ...this.hostDirectivesResolver.resolve(extMeta)]);
223+
registry.set(name, this.combineWithHostDirectives(extMeta));
214224
}
215225
}
216226

217227
return new SelectorlessMatcher(registry);
218228
}
229+
230+
private combineWithHostDirectives(meta: DirectiveMeta): DirectiveMeta[] {
231+
return [...this.hostDirectivesResolver.resolve(meta), meta];
232+
}
219233
}

packages/compiler-cli/src/ngtsc/typecheck/api/context.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
import ts from 'typescript';
1818

1919
import {Reference} from '../../imports';
20-
import {PipeMeta} from '../../metadata';
20+
import {DirectiveMeta, PipeMeta} from '../../metadata';
2121
import {ClassDeclaration} from '../../reflection';
2222

2323
import {SourceMapping, TypeCheckableDirectiveMeta} from './api';
@@ -48,6 +48,9 @@ export interface HostBindingsContext {
4848
/** AST node representing the host element of the directive. */
4949
node: TmplAstHostElement;
5050

51+
/** Directives present on the host element. */
52+
directives: DirectiveMeta[];
53+
5154
/** Describes the source of the host bindings. Used for mapping errors back. */
5255
sourceMapping: SourceMapping;
5356
}

packages/compiler-cli/src/ngtsc/typecheck/src/context.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,13 @@ export class TypeCheckContextImpl implements TypeCheckContext {
259259

260260
const boundTarget = binder.bind({
261261
template: templateContext?.nodes,
262-
host: hostBindingContext?.node,
262+
host:
263+
hostBindingContext === null
264+
? undefined
265+
: {
266+
node: hostBindingContext.node,
267+
directives: hostBindingContext.directives,
268+
},
263269
});
264270

265271
if (this.inlining === InliningMode.InlineOps) {

0 commit comments

Comments
 (0)