Skip to content

Commit 8eef694

Browse files
JeanMecheatscott
authored andcommitted
feat(core): Provide a diagnostic for missing Signal invocation in template interpolation. (#49660)
To improve DX for beginners, this commit adds an extended diagnostic for Signals in template interpolations. PR Close #49660
1 parent a645cec commit 8eef694

File tree

10 files changed

+515
-0
lines changed

10 files changed

+515
-0
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
@name Signals must be invoked in template interpolations.
2+
3+
@description
4+
5+
Angular Signals are zero-argument functions (`() => T`). When executed, they return the current value of the signal.
6+
This means they are meant to be invoked when used in template interpolations to render its value.
7+
8+
## What should I do instead?
9+
10+
When you use a signal within a template interpolation, you need to invoke it to render its value.
11+
12+
<code-example format="typescript" language="typescript">
13+
14+
import {Component, signal, Signal} from '&commat;angular/core';
15+
16+
&commat;Component({
17+
// &hellip;
18+
})
19+
class MyComponent {
20+
mySignal: Signal<number> = signal(0)
21+
}
22+
</code-example>
23+
24+
<code-example format="html" language="html">
25+
&lt;div>{{ mySignal() }}/div>
26+
</code-example>
27+
28+
<!-- links -->
29+
30+
<!-- external links -->
31+
32+
<!-- end links -->
33+
34+
@reviewed 2023-04-02

goldens/public-api/compiler-cli/error_code.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export enum ErrorCode {
5353
INJECTABLE_INHERITS_INVALID_CONSTRUCTOR = 2016,
5454
INLINE_TCB_REQUIRED = 8900,
5555
INLINE_TYPE_CTOR_REQUIRED = 8901,
56+
INTERPOLATED_SIGNAL_NOT_INVOKED = 8109,
5657
INVALID_BANANA_IN_BOX = 8101,
5758
LOCAL_COMPILATION_IMPORTED_STYLES_STRING = 11002,
5859
LOCAL_COMPILATION_IMPORTED_TEMPLATE_STRING = 11001,

goldens/public-api/compiler-cli/extended_template_diagnostic_name.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
// @public
88
export enum ExtendedTemplateDiagnosticName {
9+
// (undocumented)
10+
INTERPOLATED_SIGNAL_NOT_INVOKED = "interpolatedSignalNotInvoked",
911
// (undocumented)
1012
INVALID_BANANA_IN_BOX = "invalidBananaInBox",
1113
// (undocumented)

packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,16 @@ export enum ErrorCode {
377377
*/
378378
SKIP_HYDRATION_NOT_STATIC = 8108,
379379

380+
/**
381+
* Signal functions should be invoked when interpolated in templates.
382+
*
383+
* For example:
384+
* ```
385+
* {{ mySignal() }}
386+
* ```
387+
*/
388+
INTERPOLATED_SIGNAL_NOT_INVOKED = 8109,
389+
380390
/**
381391
* The template type-checking engine would need to generate an inline type check block for a
382392
* component, but the current type-checking environment doesn't support it.

packages/compiler-cli/src/ngtsc/diagnostics/src/extended_template_diagnostic_name.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ export enum ExtendedTemplateDiagnosticName {
2424
MISSING_NGFOROF_LET = 'missingNgForOfLet',
2525
SUFFIX_NOT_SUPPORTED = 'suffixNotSupported',
2626
SKIP_HYDRATION_NOT_STATIC = 'skipHydrationNotStatic',
27+
INTERPOLATED_SIGNAL_NOT_INVOKED = 'interpolatedSignalNotInvoked'
2728
}

packages/compiler-cli/src/ngtsc/typecheck/extended/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ ts_library(
1212
"//packages/compiler-cli/src/ngtsc/diagnostics",
1313
"//packages/compiler-cli/src/ngtsc/typecheck/api",
1414
"//packages/compiler-cli/src/ngtsc/typecheck/extended/api",
15+
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked",
1516
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/invalid_banana_in_box",
1617
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_control_flow_directive",
1718
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_ngforof_let",
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
load("//tools:defaults.bzl", "ts_library")
2+
3+
ts_library(
4+
name = "interpolated_signal_not_invoked",
5+
srcs = ["index.ts"],
6+
visibility = [
7+
"//packages/compiler-cli/src/ngtsc:__subpackages__",
8+
"//packages/compiler-cli/test/ngtsc:__pkg__",
9+
],
10+
deps = [
11+
"//packages/compiler",
12+
"//packages/compiler-cli/src/ngtsc/diagnostics",
13+
"//packages/compiler-cli/src/ngtsc/typecheck/api",
14+
"//packages/compiler-cli/src/ngtsc/typecheck/extended/api",
15+
"@npm//typescript",
16+
],
17+
)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {AST, Interpolation, PropertyRead, TmplAstNode} from '@angular/compiler';
10+
import ts from 'typescript';
11+
12+
import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../../diagnostics';
13+
import {NgTemplateDiagnostic, SymbolKind} from '../../../api';
14+
import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '../../api';
15+
16+
/**
17+
* Ensures Signals are invoked when used in template interpolations.
18+
*/
19+
class InterpolatedSignalCheck extends
20+
TemplateCheckWithVisitor<ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED> {
21+
override code = ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED as const;
22+
23+
override visitNode(
24+
ctx: TemplateContext<ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED>,
25+
component: ts.ClassDeclaration,
26+
node: TmplAstNode|AST): NgTemplateDiagnostic<ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED>[] {
27+
if (node instanceof Interpolation) {
28+
return node.expressions.filter((item): item is PropertyRead => item instanceof PropertyRead)
29+
.flatMap((item) => {
30+
if (item instanceof PropertyRead) {
31+
return buildDiagnosticForSignal(ctx, item, component);
32+
}
33+
return [];
34+
});
35+
}
36+
return [];
37+
}
38+
}
39+
40+
function buildDiagnosticForSignal(
41+
ctx: TemplateContext<ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED>, node: PropertyRead,
42+
component: ts.ClassDeclaration):
43+
Array<NgTemplateDiagnostic<ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED>> {
44+
const symbol = ctx.templateTypeChecker.getSymbolOfNode(node, component);
45+
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')) {
50+
const templateMapping =
51+
ctx.templateTypeChecker.getTemplateMappingAtTcbLocation(symbol.tcbLocation)!;
52+
53+
const errorString = `${node.name} is a function and should be invoked : ${node.name}()`;
54+
const diagnostic = ctx.makeTemplateDiagnostic(templateMapping.span, errorString);
55+
return [diagnostic];
56+
}
57+
58+
return [];
59+
}
60+
61+
export const factory: TemplateCheckFactory<
62+
ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED,
63+
ExtendedTemplateDiagnosticName.INTERPOLATED_SIGNAL_NOT_INVOKED> = {
64+
code: ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED,
65+
name: ExtendedTemplateDiagnosticName.INTERPOLATED_SIGNAL_NOT_INVOKED,
66+
create: () => new InterpolatedSignalCheck(),
67+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
2+
3+
ts_library(
4+
name = "test_lib",
5+
testonly = True,
6+
srcs = ["interpolated_signal_not_invoked_spec.ts"],
7+
deps = [
8+
"//packages/compiler",
9+
"//packages/compiler-cli/src/ngtsc/core:api",
10+
"//packages/compiler-cli/src/ngtsc/diagnostics",
11+
"//packages/compiler-cli/src/ngtsc/file_system",
12+
"//packages/compiler-cli/src/ngtsc/file_system/testing",
13+
"//packages/compiler-cli/src/ngtsc/testing",
14+
"//packages/compiler-cli/src/ngtsc/typecheck/extended",
15+
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked",
16+
"//packages/compiler-cli/src/ngtsc/typecheck/testing",
17+
"@npm//typescript",
18+
],
19+
)
20+
21+
jasmine_node_test(
22+
name = "test",
23+
bootstrap = ["//tools/testing:node_no_angular"],
24+
deps = [
25+
":test_lib",
26+
],
27+
)

0 commit comments

Comments
 (0)