Skip to content

Commit 33b5707

Browse files
cexbrayatatscott
authored andcommitted
fix(compiler-cli): interpolatedSignalNotInvoked diagnostic (#53585)
The diagnostic was catching the following case: ```ts name = signal('Angular'); ``` but not the following ones: ```ts name = signal('Angular').asReadonly(); name = computed(() => 'Angular'); name!: Signal<string> ``` This was not catched in the tests because the type of `Signal` is different than the one actually used in core. It turns out the real type forces the diagnostic to check both the `symbol.tsType.symbol` and the `symbol.tsType.aliasSymbol`. PR Close #53585
1 parent 3302425 commit 33b5707

File tree

2 files changed

+43
-6
lines changed

2 files changed

+43
-6
lines changed

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,19 @@ class InterpolatedSignalCheck extends
3737
}
3838
}
3939

40+
function isSignal(symbol: ts.Symbol|undefined): boolean {
41+
return (symbol?.escapedName === 'WritableSignal' || symbol?.escapedName === 'Signal') &&
42+
(symbol as any).parent.escapedName.includes('@angular/core');
43+
}
44+
4045
function buildDiagnosticForSignal(
4146
ctx: TemplateContext<ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED>, node: PropertyRead,
4247
component: ts.ClassDeclaration):
4348
Array<NgTemplateDiagnostic<ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED>> {
4449
const symbol = ctx.templateTypeChecker.getSymbolOfNode(node, component);
50+
4551
if (symbol?.kind === SymbolKind.Expression &&
46-
/* can this condition be improved ? */
47-
(symbol.tsType.symbol?.escapedName === 'WritableSignal' ||
48-
symbol.tsType.symbol?.escapedName === 'Signal') &&
49-
(symbol.tsType.symbol as any).parent.escapedName.includes('@angular/core')) {
52+
(isSignal(symbol.tsType.symbol) || isSignal(symbol.tsType.aliasSymbol))) {
5053
const templateMapping =
5154
ctx.templateTypeChecker.getTemplateMappingAtTcbLocation(symbol.tcbLocation)!;
5255

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

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,16 @@ function coreDtsWithSignals() {
2020
return {
2121
fileName: absoluteFrom('/node_modules/@angular/core/index.d.ts'),
2222
source: `
23-
export class Signal<T> {};
23+
export declare const SIGNAL: unique symbol;
24+
export declare type Signal<T> = (() => T) & {
25+
[SIGNAL]: unknown;
26+
};
2427
export declare function signal<T>(initialValue: T): WritableSignal<T>;
2528
export declare function computed<T>(computation: () => T): Signal<T>;
2629
27-
export interface WritableSignal<T> extends Signal<T> {}
30+
export interface WritableSignal<T> extends Signal<T> {
31+
asReadonly(): Signal<T>;
32+
}
2833
`,
2934
templates: {},
3035
};
@@ -97,6 +102,35 @@ runInEachFileSystem(() => {
97102
expect(getSourceCodeForDiagnostic(diags[1])).toBe(`mySignal2`);
98103
});
99104

105+
it('should produce a warning when a readonly signal isn\'t invoked', () => {
106+
const fileName = absoluteFrom('/main.ts');
107+
const {program, templateTypeChecker} = setup([
108+
coreDtsWithSignals(),
109+
{
110+
fileName,
111+
templates: {
112+
'TestCmp': `<div>{{ count }}</div>`,
113+
},
114+
source: `
115+
import {signal} from '@angular/core';
116+
117+
export class TestCmp {
118+
count = signal(0).asReadonly();
119+
}`,
120+
},
121+
]);
122+
const sf = getSourceFileOrError(program, fileName);
123+
const component = getClass(sf, 'TestCmp');
124+
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
125+
templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} /* options */
126+
);
127+
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
128+
expect(diags.length).toBe(1);
129+
expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning);
130+
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED));
131+
expect(getSourceCodeForDiagnostic(diags[0])).toBe('count');
132+
});
133+
100134
it('should produce a warning when a computed signal isn\'t invoked', () => {
101135
const fileName = absoluteFrom('/main.ts');
102136
const {program, templateTypeChecker} = setup([

0 commit comments

Comments
 (0)