Skip to content

Commit

Permalink
feat(core): Provide a diagnostic for missing Signal invocation in tem…
Browse files Browse the repository at this point in the history
…plate interpolation. (#49660)

To improve DX for beginners, this commit adds an extended diagnostic for Signals in template interpolations.

PR Close #49660
  • Loading branch information
JeanMeche authored and atscott committed Oct 10, 2023
1 parent a645cec commit 8eef694
Show file tree
Hide file tree
Showing 10 changed files with 515 additions and 0 deletions.
34 changes: 34 additions & 0 deletions aio/content/extended-diagnostics/NG8109.md
@@ -0,0 +1,34 @@
@name Signals must be invoked in template interpolations.

@description

Angular Signals are zero-argument functions (`() => T`). When executed, they return the current value of the signal.
This means they are meant to be invoked when used in template interpolations to render its value.

## What should I do instead?

When you use a signal within a template interpolation, you need to invoke it to render its value.

<code-example format="typescript" language="typescript">

import {Component, signal, Signal} from '&commat;angular/core';

&commat;Component({
// &hellip;
})
class MyComponent {
mySignal: Signal<number> = signal(0)
}
</code-example>

<code-example format="html" language="html">
&lt;div>{{ mySignal() }}/div>
</code-example>

<!-- links -->

<!-- external links -->

<!-- end links -->

@reviewed 2023-04-02
1 change: 1 addition & 0 deletions goldens/public-api/compiler-cli/error_code.md
Expand Up @@ -53,6 +53,7 @@ export enum ErrorCode {
INJECTABLE_INHERITS_INVALID_CONSTRUCTOR = 2016,
INLINE_TCB_REQUIRED = 8900,
INLINE_TYPE_CTOR_REQUIRED = 8901,
INTERPOLATED_SIGNAL_NOT_INVOKED = 8109,
INVALID_BANANA_IN_BOX = 8101,
LOCAL_COMPILATION_IMPORTED_STYLES_STRING = 11002,
LOCAL_COMPILATION_IMPORTED_TEMPLATE_STRING = 11001,
Expand Down
Expand Up @@ -6,6 +6,8 @@

// @public
export enum ExtendedTemplateDiagnosticName {
// (undocumented)
INTERPOLATED_SIGNAL_NOT_INVOKED = "interpolatedSignalNotInvoked",
// (undocumented)
INVALID_BANANA_IN_BOX = "invalidBananaInBox",
// (undocumented)
Expand Down
10 changes: 10 additions & 0 deletions packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts
Expand Up @@ -377,6 +377,16 @@ export enum ErrorCode {
*/
SKIP_HYDRATION_NOT_STATIC = 8108,

/**
* Signal functions should be invoked when interpolated in templates.
*
* For example:
* ```
* {{ mySignal() }}
* ```
*/
INTERPOLATED_SIGNAL_NOT_INVOKED = 8109,

/**
* The template type-checking engine would need to generate an inline type check block for a
* component, but the current type-checking environment doesn't support it.
Expand Down
Expand Up @@ -24,4 +24,5 @@ export enum ExtendedTemplateDiagnosticName {
MISSING_NGFOROF_LET = 'missingNgForOfLet',
SUFFIX_NOT_SUPPORTED = 'suffixNotSupported',
SKIP_HYDRATION_NOT_STATIC = 'skipHydrationNotStatic',
INTERPOLATED_SIGNAL_NOT_INVOKED = 'interpolatedSignalNotInvoked'
}
Expand Up @@ -12,6 +12,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/typecheck/api",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/api",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/invalid_banana_in_box",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_control_flow_directive",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_ngforof_let",
Expand Down
@@ -0,0 +1,17 @@
load("//tools:defaults.bzl", "ts_library")

ts_library(
name = "interpolated_signal_not_invoked",
srcs = ["index.ts"],
visibility = [
"//packages/compiler-cli/src/ngtsc:__subpackages__",
"//packages/compiler-cli/test/ngtsc:__pkg__",
],
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/typecheck/api",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/api",
"@npm//typescript",
],
)
@@ -0,0 +1,67 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {AST, Interpolation, PropertyRead, TmplAstNode} from '@angular/compiler';
import ts from 'typescript';

import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../../diagnostics';
import {NgTemplateDiagnostic, SymbolKind} from '../../../api';
import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '../../api';

/**
* Ensures Signals are invoked when used in template interpolations.
*/
class InterpolatedSignalCheck extends
TemplateCheckWithVisitor<ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED> {
override code = ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED as const;

override visitNode(
ctx: TemplateContext<ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED>,
component: ts.ClassDeclaration,
node: TmplAstNode|AST): NgTemplateDiagnostic<ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED>[] {
if (node instanceof Interpolation) {
return node.expressions.filter((item): item is PropertyRead => item instanceof PropertyRead)
.flatMap((item) => {
if (item instanceof PropertyRead) {
return buildDiagnosticForSignal(ctx, item, component);
}
return [];
});
}
return [];
}
}

function buildDiagnosticForSignal(
ctx: TemplateContext<ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED>, node: PropertyRead,
component: ts.ClassDeclaration):
Array<NgTemplateDiagnostic<ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED>> {
const symbol = ctx.templateTypeChecker.getSymbolOfNode(node, component);
if (symbol?.kind === SymbolKind.Expression &&
/* can this condition be improved ? */
(symbol.tsType.symbol?.escapedName === 'WritableSignal' ||
symbol.tsType.symbol?.escapedName === 'Signal') &&
(symbol.tsType.symbol as any).parent.escapedName.includes('@angular/core')) {
const templateMapping =
ctx.templateTypeChecker.getTemplateMappingAtTcbLocation(symbol.tcbLocation)!;

const errorString = `${node.name} is a function and should be invoked : ${node.name}()`;
const diagnostic = ctx.makeTemplateDiagnostic(templateMapping.span, errorString);
return [diagnostic];
}

return [];
}

export const factory: TemplateCheckFactory<
ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED,
ExtendedTemplateDiagnosticName.INTERPOLATED_SIGNAL_NOT_INVOKED> = {
code: ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED,
name: ExtendedTemplateDiagnosticName.INTERPOLATED_SIGNAL_NOT_INVOKED,
create: () => new InterpolatedSignalCheck(),
};
@@ -0,0 +1,27 @@
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")

ts_library(
name = "test_lib",
testonly = True,
srcs = ["interpolated_signal_not_invoked_spec.ts"],
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/core:api",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/testing",
"//packages/compiler-cli/src/ngtsc/typecheck/extended",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked",
"//packages/compiler-cli/src/ngtsc/typecheck/testing",
"@npm//typescript",
],
)

jasmine_node_test(
name = "test",
bootstrap = ["//tools/testing:node_no_angular"],
deps = [
":test_lib",
],
)

0 comments on commit 8eef694

Please sign in to comment.