Skip to content

Commit 49377d2

Browse files
JoostKAndrewKushnir
authored andcommitted
perf(compiler-cli): fix performance of "interpolated signal not invoked" check (#64410)
This fixes a performance regression from #63754, which is almost a revert of the prior performance fix in #57291; the latter was provided as quick fix to address the severe performance overhead this extended diagnostic used to have, with #57337 as follow-up change to address the false negatives that were introduced in #57291. That follow-up never landed, though, so this commit is re-applying the changes from #57337 to fix the performance regression. Fixes #64403 PR Close #64410
1 parent 0ed6c93 commit 49377d2

File tree

4 files changed

+108
-47
lines changed

4 files changed

+108
-47
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,14 @@ export interface TemplateTypeChecker {
312312
*/
313313
getPipeMetadata(pipe: ts.ClassDeclaration): PipeMeta | null;
314314

315+
/**
316+
* Gets the directives that apply to the given template node in a component's template.
317+
*/
318+
getDirectivesOfNode(
319+
component: ts.ClassDeclaration,
320+
node: TmplAstElement | TmplAstTemplate,
321+
): TypeCheckableDirectiveMeta[] | null;
322+
315323
/**
316324
* Gets the directives that have been used in a component's template.
317325
*/

packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked/index.ts

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@ import {
1414
PrefixNot,
1515
PropertyRead,
1616
TmplAstBoundAttribute,
17+
TmplAstElement,
1718
TmplAstIfBlock,
1819
TmplAstNode,
1920
TmplAstSwitchBlock,
21+
TmplAstTemplate,
2022
} from '@angular/compiler';
2123
import ts from 'typescript';
2224

2325
import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../../diagnostics';
24-
import {NgTemplateDiagnostic, SymbolKind} from '../../../api';
26+
import {NgTemplateDiagnostic, SymbolKind, TypeCheckableDirectiveMeta} from '../../../api';
2527
import {isSignalReference} from '../../../src/symbol_util';
2628
import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '../../api';
2729

@@ -52,37 +54,24 @@ class InterpolatedSignalCheck extends TemplateCheckWithVisitor<ErrorCode.INTERPO
5254
.filter((item): item is PropertyRead => item instanceof PropertyRead)
5355
.flatMap((item) => buildDiagnosticForSignal(ctx, item, component));
5456
}
55-
// bound properties like `[prop]="mySignal"`
56-
else if (node instanceof TmplAstBoundAttribute) {
57-
const symbol = ctx.templateTypeChecker.getSymbolOfNode(node, component);
58-
59-
if (
60-
symbol?.kind === SymbolKind.Input &&
61-
symbol.bindings.length > 0 &&
62-
symbol.bindings.some((binding) => binding.target.kind === SymbolKind.Directive)
63-
) {
64-
return [];
65-
}
66-
67-
// otherwise, we check if the node is
68-
const nodeAst = isPropertyReadNodeAst(node);
69-
if (
70-
// a bound property like `[prop]="mySignal"`
71-
(node.type === BindingType.Property ||
72-
// or a class binding like `[class.myClass]="mySignal"`
73-
node.type === BindingType.Class ||
74-
// or a style binding like `[style.width]="mySignal"`
75-
node.type === BindingType.Style ||
76-
// or an attribute binding like `[attr.role]="mySignal"`
77-
node.type === BindingType.Attribute ||
78-
// or an animation binding like `[animate.enter]="mySignal"`
79-
node.type === BindingType.Animation ||
80-
// or an animation binding like `[@myAnimation]="mySignal"`
81-
node.type === BindingType.LegacyAnimation) &&
82-
nodeAst
83-
) {
84-
return buildDiagnosticForSignal(ctx, nodeAst, component);
85-
}
57+
// check bound inputs like `[prop]="mySignal"` on an element or inline template
58+
else if (node instanceof TmplAstElement && node.inputs.length > 0) {
59+
const directivesOfElement = ctx.templateTypeChecker.getDirectivesOfNode(component, node);
60+
return node.inputs.flatMap((input) =>
61+
checkBoundAttribute(ctx, component, directivesOfElement, input),
62+
);
63+
} else if (node instanceof TmplAstTemplate && node.tagName === 'ng-template') {
64+
const directivesOfElement = ctx.templateTypeChecker.getDirectivesOfNode(component, node);
65+
const inputDiagnostics = node.inputs.flatMap((input) => {
66+
return checkBoundAttribute(ctx, component, directivesOfElement, input);
67+
});
68+
const templateAttrDiagnostics = node.templateAttrs.flatMap((attr) => {
69+
if (!(attr instanceof TmplAstBoundAttribute)) {
70+
return [];
71+
}
72+
return checkBoundAttribute(ctx, component, directivesOfElement, attr);
73+
});
74+
return inputDiagnostics.concat(templateAttrDiagnostics);
8675
}
8776
// if blocks like `@if(mySignal) { ... }`
8877
else if (node instanceof TmplAstIfBlock) {
@@ -111,6 +100,43 @@ class InterpolatedSignalCheck extends TemplateCheckWithVisitor<ErrorCode.INTERPO
111100
}
112101
}
113102

103+
function checkBoundAttribute(
104+
ctx: TemplateContext<ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED>,
105+
component: ts.ClassDeclaration,
106+
directivesOfElement: Array<TypeCheckableDirectiveMeta> | null,
107+
node: TmplAstBoundAttribute,
108+
): Array<NgTemplateDiagnostic<ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED>> {
109+
// we skip the check if the node is an input binding
110+
if (
111+
directivesOfElement !== null &&
112+
directivesOfElement.some((dir) => dir.inputs.getByBindingPropertyName(node.name) !== null)
113+
) {
114+
return [];
115+
}
116+
117+
// otherwise, we check if the node is
118+
const nodeAst = isPropertyReadNodeAst(node);
119+
if (
120+
// a bound property like `[prop]="mySignal"`
121+
(node.type === BindingType.Property ||
122+
// or a class binding like `[class.myClass]="mySignal"`
123+
node.type === BindingType.Class ||
124+
// or a style binding like `[style.width]="mySignal"`
125+
node.type === BindingType.Style ||
126+
// or an attribute binding like `[attr.role]="mySignal"`
127+
node.type === BindingType.Attribute ||
128+
// or an animation binding like `[animate.enter]="mySignal"`
129+
node.type === BindingType.Animation ||
130+
// or an animation binding like `[@myAnimation]="mySignal"`
131+
node.type === BindingType.LegacyAnimation) &&
132+
nodeAst
133+
) {
134+
return buildDiagnosticForSignal(ctx, nodeAst, component);
135+
}
136+
137+
return [];
138+
}
139+
114140
function isPropertyReadNodeAst(node: TmplAstBoundAttribute): PropertyRead | undefined {
115141
if (node.value instanceof ASTWithSource === false) {
116142
return undefined;
@@ -153,9 +179,11 @@ function buildDiagnosticForSignal(
153179
// error.
154180
// We also check for `{{ mySignal.set }}` or `{{ mySignal.update }}` or
155181
// `{{ mySignal.asReadonly }}` as these are the names of instance properties of Signal
182+
if (!isFunctionInstanceProperty(node.name) && !isSignalInstanceProperty(node.name)) {
183+
return [];
184+
}
156185
const symbolOfReceiver = ctx.templateTypeChecker.getSymbolOfNode(node.receiver, component);
157186
if (
158-
(isFunctionInstanceProperty(node.name) || isSignalInstanceProperty(node.name)) &&
159187
symbolOfReceiver !== null &&
160188
symbolOfReceiver.kind === SymbolKind.Expression &&
161189
isSignalReference(symbolOfReceiver)

packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/interpolated_signal_not_invoked/interpolated_signal_not_invoked_spec.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -634,29 +634,39 @@ runInEachFileSystem(() => {
634634
{
635635
fileName,
636636
templates: {
637-
'TestCmp': `<child /> <div [id]="title"></div>`,
637+
'TestCmp': `
638+
<!-- The below "myInput" binding should be ignored, as it corresponds with TestDir -->
639+
<div dir [myInput]="dirSignal"></div>
640+
641+
<!-- The below "myInput" binding should be reported, as it does not correspond with TestDir -->
642+
<div [myInput]="divSignal"></div>
643+
644+
<!-- The below "myInput" binding applies to the "div" element so it should be reported -->
645+
<div *dir [myInput]="structuralSignal"></div>
646+
`,
638647
},
639648
source: `
640649
import {signal, input} from '@angular/core';
641650
642-
export class Child {
643-
id = input(0);
651+
export class TestDir {
652+
myInput = input.required();
644653
}
645-
646654
export class TestCmp {
647-
title = signal('');
655+
dirSignal = signal(0);
656+
divSignal = signal(0);
657+
structuralSignal = signal(0);
648658
}`,
649659
declarations: [
650660
{
651661
type: 'directive',
652-
name: 'Child',
653-
selector: 'child',
662+
name: 'TestDir',
663+
selector: '[dir]',
654664
inputs: {
655-
id: {
665+
myInput: {
656666
isSignal: true,
657-
bindingPropertyName: 'id',
658-
classPropertyName: 'id',
659-
required: false,
667+
bindingPropertyName: 'myInput',
668+
classPropertyName: 'myInput',
669+
required: true,
660670
transform: null,
661671
},
662672
},
@@ -670,11 +680,17 @@ runInEachFileSystem(() => {
670680
templateTypeChecker,
671681
program.getTypeChecker(),
672682
[interpolatedSignalFactory],
673-
{} /* options */,
683+
{},
684+
/* options */
674685
);
675686
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
676-
expect(diags.length).toBe(1);
677-
expect(diags[0].messageText).toBe('title is a function and should be invoked: title()');
687+
expect(diags.length).toBe(2);
688+
expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning);
689+
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED));
690+
expect(getSourceCodeForDiagnostic(diags[0])).toBe(`divSignal`);
691+
expect(diags[1].category).toBe(ts.DiagnosticCategory.Warning);
692+
expect(diags[1].code).toBe(ngErrorCode(ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED));
693+
expect(getSourceCodeForDiagnostic(diags[1])).toBe(`structuralSignal`);
678694
});
679695

680696
[

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,15 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
186186
return data?.hostElement ?? null;
187187
}
188188

189+
getDirectivesOfNode(
190+
component: ts.ClassDeclaration,
191+
node: TmplAstElement | TmplAstTemplate,
192+
): TypeCheckableDirectiveMeta[] | null {
193+
return (
194+
this.getLatestComponentState(component).data?.boundTarget.getDirectivesOfNode(node) ?? null
195+
);
196+
}
197+
189198
getUsedDirectives(component: ts.ClassDeclaration): TypeCheckableDirectiveMeta[] | null {
190199
return this.getLatestComponentState(component).data?.boundTarget.getUsedDirectives() ?? null;
191200
}

0 commit comments

Comments
 (0)