Skip to content

Commit 4ffa736

Browse files
cexbrayatpkozlowski-opensource
authored andcommitted
fix(compiler-cli): interpolatedSignalNotInvoked diagnostic for class, style, attribute and animation bindings (#55969)
This extends the diagnostic to catch issues like: ```html <div [class.green]="mySignal"></div> <div [style.width]="mySignal"></div> <div [attr.role]="mySignal"></div> <div [@TriggerName]="mySignal"></div> ``` PR Close #55969
1 parent b94adb9 commit 4ffa736

File tree

2 files changed

+65
-48
lines changed

2 files changed

+65
-48
lines changed

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,18 @@ class InterpolatedSignalCheck extends TemplateCheckWithVisitor<ErrorCode.INTERPO
5555
if (symbol !== null && symbol.kind === SymbolKind.Input) {
5656
return [];
5757
}
58-
// otherwise, we check if the node is a bound property like `[prop]="mySignal"`
58+
// otherwise, we check if the node is
5959
if (
60-
node.type === BindingType.Property &&
60+
// a bound property like `[prop]="mySignal"`
61+
(node.type === BindingType.Property ||
62+
// or a class binding like `[class.myClass]="mySignal"`
63+
node.type === BindingType.Class ||
64+
// or a style binding like `[style.width]="mySignal"`
65+
node.type === BindingType.Style ||
66+
// or an attribute binding like `[attr.role]="mySignal"`
67+
node.type === BindingType.Attribute ||
68+
// or an animation binding like `[@myAnimation]="mySignal"`
69+
node.type === BindingType.Animation) &&
6170
node.value instanceof ASTWithSource &&
6271
node.value.ast instanceof PropertyRead
6372
) {

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

Lines changed: 54 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -625,63 +625,71 @@ runInEachFileSystem(() => {
625625
expect(getSourceCodeForDiagnostic(diags[0])).toBe(`myNestedSignal`);
626626
});
627627

628-
it("should produce a warning when signal isn't invoked on dom property binding", () => {
629-
const fileName = absoluteFrom('/main.ts');
630-
const {program, templateTypeChecker} = setup([
631-
{
632-
fileName,
633-
templates: {
634-
'TestCmp': `<div [id]="mySignal"></div>`,
635-
},
636-
source: `
628+
[
629+
['dom property', 'id'],
630+
['class', 'class.green'],
631+
['style', 'style.width'],
632+
['attribute', 'attr.role'],
633+
['animation', '@triggerName'],
634+
].forEach(([name, binding]) => {
635+
it(`should produce a warning when signal isn't invoked on ${name} binding`, () => {
636+
const fileName = absoluteFrom('/main.ts');
637+
const {program, templateTypeChecker} = setup([
638+
{
639+
fileName,
640+
templates: {
641+
'TestCmp': `<div [${binding}]="mySignal"></div>`,
642+
},
643+
source: `
637644
import {signal} from '@angular/core';
638645
639646
export class TestCmp {
640647
mySignal = signal<number>(0);
641648
}`,
642-
},
643-
]);
644-
const sf = getSourceFileOrError(program, fileName);
645-
const component = getClass(sf, 'TestCmp');
646-
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
647-
templateTypeChecker,
648-
program.getTypeChecker(),
649-
[interpolatedSignalFactory],
650-
{} /* options */,
651-
);
652-
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
653-
expect(diags.length).toBe(1);
654-
expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning);
655-
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED));
656-
expect(getSourceCodeForDiagnostic(diags[0])).toBe(`mySignal`);
657-
});
658-
659-
it('should not produce a warning when signal is invoked on dom property binding', () => {
660-
const fileName = absoluteFrom('/main.ts');
661-
const {program, templateTypeChecker} = setup([
662-
{
663-
fileName,
664-
templates: {
665-
'TestCmp': `<div [id]="mySignal()"></div>`,
666649
},
667-
source: `
650+
]);
651+
const sf = getSourceFileOrError(program, fileName);
652+
const component = getClass(sf, 'TestCmp');
653+
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
654+
templateTypeChecker,
655+
program.getTypeChecker(),
656+
[interpolatedSignalFactory],
657+
{} /* options */,
658+
);
659+
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
660+
expect(diags.length).toBe(1);
661+
expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning);
662+
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED));
663+
expect(getSourceCodeForDiagnostic(diags[0])).toBe(`mySignal`);
664+
});
665+
666+
it(`should not produce a warning when signal is invoked on ${name} binding`, () => {
667+
const fileName = absoluteFrom('/main.ts');
668+
const {program, templateTypeChecker} = setup([
669+
{
670+
fileName,
671+
templates: {
672+
'TestCmp': `<div [${binding}]="mySignal()"></div>`,
673+
},
674+
source: `
668675
import {signal} from '@angular/core';
669676
670677
export class TestCmp {
671678
mySignal = signal<number>(0);
672679
}`,
673-
},
674-
]);
675-
const sf = getSourceFileOrError(program, fileName);
676-
const component = getClass(sf, 'TestCmp');
677-
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
678-
templateTypeChecker,
679-
program.getTypeChecker(),
680-
[interpolatedSignalFactory],
681-
{} /* options */,
682-
);
683-
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
684-
expect(diags.length).toBe(0);
680+
},
681+
]);
682+
const sf = getSourceFileOrError(program, fileName);
683+
const component = getClass(sf, 'TestCmp');
684+
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
685+
templateTypeChecker,
686+
program.getTypeChecker(),
687+
[interpolatedSignalFactory],
688+
{} /* options */,
689+
);
690+
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
691+
expect(diags.length).toBe(0);
692+
});
685693
});
686694

687695
it('should not produce a warning with other Signal type', () => {

0 commit comments

Comments
 (0)