diff --git a/adev/src/app/sub-navigation-data.ts b/adev/src/app/sub-navigation-data.ts
index f13802af57c..91dedbdb2e7 100644
--- a/adev/src/app/sub-navigation-data.ts
+++ b/adev/src/app/sub-navigation-data.ts
@@ -1388,6 +1388,11 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [
path: 'extended-diagnostics/NG8109',
contentPath: 'reference/extended-diagnostics/NG8109',
},
+ {
+ label: 'NG8111: Functions must be invoked in event bindings',
+ path: 'extended-diagnostics/NG8111',
+ contentPath: 'reference/extended-diagnostics/NG8111',
+ },
],
},
{
diff --git a/adev/src/content/reference/extended-diagnostics/NG8111.md b/adev/src/content/reference/extended-diagnostics/NG8111.md
new file mode 100644
index 00000000000..16abd97595b
--- /dev/null
+++ b/adev/src/content/reference/extended-diagnostics/NG8111.md
@@ -0,0 +1,65 @@
+# Functions should be invoked in event bindings.
+
+This diagnostic detects uninvoked functions in event bindings.
+
+
+
+import {Component, signal, Signal} from '@angular/core';
+
+@Component({
+ template: ``,
+})
+class MyComponent {
+ onClick() {
+ console.log('clicked');
+ }
+}
+
+
+
+## What's wrong with that?
+
+Functions in event bindings should be invoked when the event is triggered.
+If the function is not invoked, it will not execute when the event is triggered.
+
+## What should I do instead?
+
+Ensure to invoke the function when you use it in an event binding to execute the function when the event is triggered.
+
+
+
+import {Component} from '@angular/core';
+
+@Component({
+ template: ``,
+})
+class MyComponent {
+ onClick() {
+ console.log('clicked');
+ }
+}
+
+
+
+## Configuration requirements
+
+[`strictTemplates`](tools/cli/template-typecheck#strict-mode) must be enabled for any extended diagnostic to emit.
+`uninvokedFunctionInEventBinding` has no additional requirements beyond `strictTemplates`.
+
+## What if I can't avoid this?
+
+This diagnostic can be disabled by editing the project's `tsconfig.json` file:
+
+
+{
+ "angularCompilerOptions": {
+ "extendedDiagnostics": {
+ "checks": {
+ "uninvokedFunctionInEventBinding": "suppress"
+ }
+ }
+ }
+}
+
+
+See [extended diagnostic configuration](extended-diagnostics#configuration) for more info.
diff --git a/adev/src/content/reference/extended-diagnostics/overview.md b/adev/src/content/reference/extended-diagnostics/overview.md
index 639ee928c22..d64df7a3614 100644
--- a/adev/src/content/reference/extended-diagnostics/overview.md
+++ b/adev/src/content/reference/extended-diagnostics/overview.md
@@ -8,17 +8,18 @@ The Angular compiler includes "extended diagnostics" which identify many of thes
Currently, Angular supports the following extended diagnostics:
-| Code | Name |
-|:--- |:--- |
-| `NG8101` | [`invalidBananaInBox`](extended-diagnostics/NG8101) |
-| `NG8102` | [`nullishCoalescingNotNullable`](extended-diagnostics/NG8102) |
-| `NG8103` | [`missingControlFlowDirective`](extended-diagnostics/NG8103) |
-| `NG8104` | [`textAttributeNotBinding`](extended-diagnostics/NG8104) |
-| `NG8105` | [`missingNgForOfLet`](extended-diagnostics/NG8105) |
-| `NG8106` | [`suffixNotSupported`](extended-diagnostics/NG8106) |
-| `NG8107` | [`optionalChainNotNullable`](extended-diagnostics/NG8107) |
-| `NG8108` | [`skipHydrationNotStatic`](extended-diagnostics/NG8108) |
-| `NG8109` | [`interpolatedSignalNotInvoked`](extended-diagnostics/NG8109) |
+| Code | Name |
+|:---------|:-----------------------------------------------------------------|
+| `NG8101` | [`invalidBananaInBox`](extended-diagnostics/NG8101) |
+| `NG8102` | [`nullishCoalescingNotNullable`](extended-diagnostics/NG8102) |
+| `NG8103` | [`missingControlFlowDirective`](extended-diagnostics/NG8103) |
+| `NG8104` | [`textAttributeNotBinding`](extended-diagnostics/NG8104) |
+| `NG8105` | [`missingNgForOfLet`](extended-diagnostics/NG8105) |
+| `NG8106` | [`suffixNotSupported`](extended-diagnostics/NG8106) |
+| `NG8107` | [`optionalChainNotNullable`](extended-diagnostics/NG8107) |
+| `NG8108` | [`skipHydrationNotStatic`](extended-diagnostics/NG8108) |
+| `NG8109` | [`interpolatedSignalNotInvoked`](extended-diagnostics/NG8109) |
+| `NG8111` | [`uninvokedFunctionInEventBinding`](extended-diagnostics/NG8111) |
## Configuration
diff --git a/goldens/public-api/compiler-cli/error_code.api.md b/goldens/public-api/compiler-cli/error_code.api.md
index 46966ffc41a..e1ad1d87bde 100644
--- a/goldens/public-api/compiler-cli/error_code.api.md
+++ b/goldens/public-api/compiler-cli/error_code.api.md
@@ -103,6 +103,7 @@ export enum ErrorCode {
TEXT_ATTRIBUTE_NOT_BINDING = 8104,
UNDECORATED_CLASS_USING_ANGULAR_FEATURES = 2007,
UNDECORATED_PROVIDER = 2005,
+ UNINVOKED_FUNCTION_IN_EVENT_BINDING = 8111,
UNSUPPORTED_INITIALIZER_API_USAGE = 8110,
// (undocumented)
VALUE_HAS_WRONG_TYPE = 1010,
diff --git a/goldens/public-api/compiler-cli/extended_template_diagnostic_name.api.md b/goldens/public-api/compiler-cli/extended_template_diagnostic_name.api.md
index 125b562667c..2f57f98462b 100644
--- a/goldens/public-api/compiler-cli/extended_template_diagnostic_name.api.md
+++ b/goldens/public-api/compiler-cli/extended_template_diagnostic_name.api.md
@@ -25,7 +25,9 @@ export enum ExtendedTemplateDiagnosticName {
// (undocumented)
SUFFIX_NOT_SUPPORTED = "suffixNotSupported",
// (undocumented)
- TEXT_ATTRIBUTE_NOT_BINDING = "textAttributeNotBinding"
+ TEXT_ATTRIBUTE_NOT_BINDING = "textAttributeNotBinding",
+ // (undocumented)
+ UNINVOKED_FUNCTION_IN_EVENT_BINDING = "uninvokedFunctionInEventBinding"
}
// (No @packageDocumentation comment for this package)
diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts b/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts
index d72965b200f..9b3fb28331e 100644
--- a/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts
+++ b/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts
@@ -482,6 +482,19 @@ export enum ErrorCode {
*/
UNSUPPORTED_INITIALIZER_API_USAGE = 8110,
+ /**
+ * A function in an event binding is not called.
+ *
+ * For example:
+ * ```
+ *
+ * ```
+ *
+ * This will not call `myFunc` when the button is clicked. Instead, it should be
+ * ``.
+ */
+ UNINVOKED_FUNCTION_IN_EVENT_BINDING = 8111,
+
/**
* 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.
diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/src/extended_template_diagnostic_name.ts b/packages/compiler-cli/src/ngtsc/diagnostics/src/extended_template_diagnostic_name.ts
index 677d6b56bad..c04d2b4f959 100644
--- a/packages/compiler-cli/src/ngtsc/diagnostics/src/extended_template_diagnostic_name.ts
+++ b/packages/compiler-cli/src/ngtsc/diagnostics/src/extended_template_diagnostic_name.ts
@@ -21,6 +21,7 @@ export enum ExtendedTemplateDiagnosticName {
OPTIONAL_CHAIN_NOT_NULLABLE = 'optionalChainNotNullable',
MISSING_CONTROL_FLOW_DIRECTIVE = 'missingControlFlowDirective',
TEXT_ATTRIBUTE_NOT_BINDING = 'textAttributeNotBinding',
+ UNINVOKED_FUNCTION_IN_EVENT_BINDING = 'uninvokedFunctionInEventBinding',
MISSING_NGFOROF_LET = 'missingNgForOfLet',
SUFFIX_NOT_SUPPORTED = 'suffixNotSupported',
SKIP_HYDRATION_NOT_STATIC = 'skipHydrationNotStatic',
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/extended/BUILD.bazel
index 78848b17d50..d49bed313ec 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/extended/BUILD.bazel
+++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/BUILD.bazel
@@ -21,6 +21,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/skip_hydration_not_static",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/suffix_not_supported",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/text_attribute_not_binding",
+ "//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/uninvoked_function_in_event_binding",
"@npm//typescript",
],
)
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/uninvoked_function_in_event_binding/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/uninvoked_function_in_event_binding/BUILD.bazel
new file mode 100644
index 00000000000..2b254f0054f
--- /dev/null
+++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/uninvoked_function_in_event_binding/BUILD.bazel
@@ -0,0 +1,17 @@
+load("//tools:defaults.bzl", "ts_library")
+
+ts_library(
+ name = "uninvoked_function_in_event_binding",
+ 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",
+ ],
+)
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/uninvoked_function_in_event_binding/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/uninvoked_function_in_event_binding/index.ts
new file mode 100644
index 00000000000..0d41ca1cb7a
--- /dev/null
+++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/uninvoked_function_in_event_binding/index.ts
@@ -0,0 +1,114 @@
+/**
+ * @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,
+ ASTWithSource,
+ Call,
+ Chain,
+ Conditional,
+ ParsedEventType,
+ PropertyRead,
+ SafeCall,
+ SafePropertyRead,
+ TmplAstBoundEvent,
+ 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 that function in event bindings are called. For example, ``
+ * will not call `myFunc` when the button is clicked. Instead, it should be ``.
+ * This is likely not the intent of the developer. Instead, the intent is likely to call `myFunc`.
+ */
+class UninvokedFunctionInEventBindingSpec extends TemplateCheckWithVisitor {
+ override code = ErrorCode.UNINVOKED_FUNCTION_IN_EVENT_BINDING as const;
+
+ override visitNode(
+ ctx: TemplateContext,
+ component: ts.ClassDeclaration,
+ node: TmplAstNode | AST,
+ ): NgTemplateDiagnostic[] {
+ // If the node is not a bound event, skip it.
+ if (!(node instanceof TmplAstBoundEvent)) return [];
+
+ // If the node is not a regular or animation event, skip it.
+ if (node.type !== ParsedEventType.Regular && node.type !== ParsedEventType.Animation) return [];
+
+ if (!(node.handler instanceof ASTWithSource)) return [];
+
+ const sourceExpressionText = node.handler.source || '';
+
+ if (node.handler.ast instanceof Chain) {
+ // (click)="increment; decrement"
+ return node.handler.ast.expressions.flatMap((expression) =>
+ assertExpressionInvoked(expression, component, node, sourceExpressionText, ctx),
+ );
+ }
+
+ if (node.handler.ast instanceof Conditional) {
+ // (click)="true ? increment : decrement"
+ const {trueExp, falseExp} = node.handler.ast;
+ return [trueExp, falseExp].flatMap((expression) =>
+ assertExpressionInvoked(expression, component, node, sourceExpressionText, ctx),
+ );
+ }
+
+ // (click)="increment"
+ return assertExpressionInvoked(node.handler.ast, component, node, sourceExpressionText, ctx);
+ }
+}
+
+/**
+ * Asserts that the expression is invoked.
+ * If the expression is a property read, and it has a call signature, a diagnostic is generated.
+ */
+function assertExpressionInvoked(
+ expression: AST,
+ component: ts.ClassDeclaration,
+ node: TmplAstBoundEvent,
+ expressionText: string,
+ ctx: TemplateContext,
+): NgTemplateDiagnostic[] {
+ if (expression instanceof Call || expression instanceof SafeCall) {
+ return []; // If the method is called, skip it.
+ }
+
+ if (!(expression instanceof PropertyRead) && !(expression instanceof SafePropertyRead)) {
+ return []; // If the expression is not a property read, skip it.
+ }
+
+ const symbol = ctx.templateTypeChecker.getSymbolOfNode(expression, component);
+
+ if (symbol !== null && symbol.kind === SymbolKind.Expression) {
+ if (symbol.tsType.getCallSignatures()?.length > 0) {
+ const fullExpressionText = generateStringFromExpression(expression, expressionText);
+ const errorString = `Function in event binding should be invoked: ${fullExpressionText}()`;
+ return [ctx.makeTemplateDiagnostic(node.sourceSpan, errorString)];
+ }
+ }
+
+ return [];
+}
+
+function generateStringFromExpression(expression: AST, source: string): string {
+ return source.substring(expression.span.start, expression.span.end);
+}
+
+export const factory: TemplateCheckFactory<
+ ErrorCode.UNINVOKED_FUNCTION_IN_EVENT_BINDING,
+ ExtendedTemplateDiagnosticName.UNINVOKED_FUNCTION_IN_EVENT_BINDING
+> = {
+ code: ErrorCode.UNINVOKED_FUNCTION_IN_EVENT_BINDING,
+ name: ExtendedTemplateDiagnosticName.UNINVOKED_FUNCTION_IN_EVENT_BINDING,
+ create: () => new UninvokedFunctionInEventBindingSpec(),
+};
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts
index 2a38248efb7..0dc597df050 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts
@@ -17,6 +17,7 @@ import {factory as nullishCoalescingNotNullableFactory} from './checks/nullish_c
import {factory as optionalChainNotNullableFactory} from './checks/optional_chain_not_nullable';
import {factory as suffixNotSupportedFactory} from './checks/suffix_not_supported';
import {factory as textAttributeNotBindingFactory} from './checks/text_attribute_not_binding';
+import {factory as uninvokedFunctionInEventBindingFactory} from './checks/uninvoked_function_in_event_binding';
export {ExtendedTemplateCheckerImpl} from './src/extended_template_checker';
@@ -32,6 +33,7 @@ export const ALL_DIAGNOSTIC_FACTORIES: readonly TemplateCheckFactory<
missingNgForOfLetFactory,
suffixNotSupportedFactory,
interpolatedSignalNotInvoked,
+ uninvokedFunctionInEventBindingFactory,
];
export const SUPPORTED_DIAGNOSTIC_NAMES = new Set([
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/uninvoked_function_in_event_binding/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/uninvoked_function_in_event_binding/BUILD.bazel
new file mode 100644
index 00000000000..469821d4739
--- /dev/null
+++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/uninvoked_function_in_event_binding/BUILD.bazel
@@ -0,0 +1,30 @@
+load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
+
+ts_library(
+ name = "test_lib",
+ testonly = True,
+ srcs = ["uninvoked_function_in_event_binding_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/uninvoked_function_in_event_binding",
+ "//packages/compiler-cli/src/ngtsc/typecheck/testing",
+ "@npm//typescript",
+ ],
+)
+
+jasmine_node_test(
+ name = "test",
+ bootstrap = ["//tools/testing:node_no_angular"],
+ data = [
+ "//packages/core:npm_package",
+ ],
+ deps = [
+ ":test_lib",
+ ],
+)
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/uninvoked_function_in_event_binding/uninvoked_function_in_event_binding_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/uninvoked_function_in_event_binding/uninvoked_function_in_event_binding_spec.ts
new file mode 100644
index 00000000000..72a5d999ca8
--- /dev/null
+++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/uninvoked_function_in_event_binding/uninvoked_function_in_event_binding_spec.ts
@@ -0,0 +1,222 @@
+/**
+ * @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 ts from 'typescript';
+
+import {ErrorCode, ExtendedTemplateDiagnosticName, ngErrorCode} from '../../../../../diagnostics';
+import {absoluteFrom, getSourceFileOrError} from '../../../../../file_system';
+import {runInEachFileSystem} from '../../../../../file_system/testing';
+import {getSourceCodeForDiagnostic} from '../../../../../testing';
+import {getClass, setup} from '../../../../testing';
+import {factory as uninvokedFunctionInEventBindingFactory} from '../../../checks/uninvoked_function_in_event_binding';
+import {ExtendedTemplateCheckerImpl} from '../../../src/extended_template_checker';
+
+runInEachFileSystem(() => {
+ describe('UninvokedFunctionInEventBindingFactoryCheck', () => {
+ it('binds the error code to its extended template diagnostic name', () => {
+ expect(uninvokedFunctionInEventBindingFactory.code).toBe(
+ ErrorCode.UNINVOKED_FUNCTION_IN_EVENT_BINDING,
+ );
+ expect(uninvokedFunctionInEventBindingFactory.name).toBe(
+ ExtendedTemplateDiagnosticName.UNINVOKED_FUNCTION_IN_EVENT_BINDING,
+ );
+ });
+
+ it('should produce a diagnostic when a function in an event binding is not invoked', () => {
+ const diags = setupTestComponent(``, `increment() { }`);
+
+ expect(diags.length).toBe(1);
+ expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning);
+ expect(diags[0].code).toBe(ngErrorCode(ErrorCode.UNINVOKED_FUNCTION_IN_EVENT_BINDING));
+ expect(getSourceCodeForDiagnostic(diags[0])).toBe(`(click)="increment"`);
+ expect(diags[0].messageText).toBe(generateDiagnosticText('increment()'));
+ });
+
+ it('should produce a diagnostic when a nested function in an event binding is not invoked', () => {
+ const diags = setupTestComponent(
+ ``,
+ `nested = { nested1: { nested2: { increment() { } } } }`,
+ );
+
+ expect(diags.length).toBe(1);
+ expect(getSourceCodeForDiagnostic(diags[0])).toBe(
+ `(click)="nested.nested1.nested2.increment"`,
+ );
+ expect(diags[0].messageText).toBe(
+ generateDiagnosticText('nested.nested1.nested2.increment()'),
+ );
+ });
+
+ it('should produce a diagnostic when a nested function that uses key read in an event binding is not invoked', () => {
+ const diags = setupTestComponent(
+ ``,
+ `nested = { nested1: { nested2: { increment() { } } } }`,
+ );
+
+ expect(diags.length).toBe(1);
+ expect(getSourceCodeForDiagnostic(diags[0])).toBe(
+ `(click)="nested.nested1['nested2'].increment"`,
+ );
+ expect(diags[0].messageText).toBe(
+ generateDiagnosticText(`nested.nested1['nested2'].increment()`),
+ );
+ });
+
+ it('should produce a diagnostic when a function in a chain is not invoked', () => {
+ const diags = setupTestComponent(
+ `
+
+
+
+ `,
+ `increment() { } decrement() { }`,
+ );
+
+ expect(diags.length).toBe(4);
+ expect(getSourceCodeForDiagnostic(diags[0])).toBe(`(click)="increment; decrement"`);
+ expect(diags[0].messageText).toBe(generateDiagnosticText('increment()'));
+ expect(getSourceCodeForDiagnostic(diags[1])).toBe(`(click)="increment; decrement"`);
+ expect(diags[1].messageText).toBe(generateDiagnosticText('decrement()'));
+ expect(getSourceCodeForDiagnostic(diags[2])).toBe(`(click)="increment; decrement()"`);
+ expect(diags[2].messageText).toBe(generateDiagnosticText('increment()'));
+ expect(getSourceCodeForDiagnostic(diags[3])).toBe(`(click)="increment(); decrement"`);
+ expect(diags[3].messageText).toBe(generateDiagnosticText('decrement()'));
+ });
+
+ it('should produce a diagnostic when a function in a conditional is not invoked', () => {
+ const diags = setupTestComponent(
+ ``,
+ `increment() { } decrement() { }`,
+ );
+
+ expect(diags.length).toBe(2);
+ expect(getSourceCodeForDiagnostic(diags[0])).toBe(`(click)="true ? increment : decrement"`);
+ expect(diags[0].messageText).toBe(generateDiagnosticText('increment()'));
+ expect(getSourceCodeForDiagnostic(diags[1])).toBe(`(click)="true ? increment : decrement"`);
+ expect(diags[1].messageText).toBe(generateDiagnosticText('decrement()'));
+ });
+
+ it('should produce a diagnostic when a function in a conditional is not invoked', () => {
+ const diags = setupTestComponent(
+ ``,
+ `increment() { } decrement() { }`,
+ );
+
+ expect(diags.length).toBe(1);
+ expect(getSourceCodeForDiagnostic(diags[0])).toBe(`(click)="true ? increment() : decrement"`);
+ expect(diags[0].messageText).toBe(generateDiagnosticText('decrement()'));
+ });
+
+ it('should produce a diagnostic when a nested function in a conditional is not invoked', () => {
+ const diags = setupTestComponent(
+ ``,
+ `
+ counter = { increment() { } }
+ nested = { nested1: { nested2?: { source() { return { decrement() } { } } } } }
+ `,
+ );
+
+ expect(diags.length).toBe(2);
+ expect(getSourceCodeForDiagnostic(diags[0])).toBe(
+ `(click)="true ? counter.increment : nested['nested1'].nested2?.source().decrement"`,
+ );
+ expect(diags[0].messageText).toBe(generateDiagnosticText('counter.increment()'));
+ expect(getSourceCodeForDiagnostic(diags[1])).toBe(
+ `(click)="true ? counter.increment : nested['nested1'].nested2?.source().decrement"`,
+ );
+ expect(diags[1].messageText).toBe(
+ generateDiagnosticText(`nested['nested1'].nested2?.source().decrement()`),
+ );
+ });
+
+ it('should produce a diagnostic when a function in a function is not invoked', () => {
+ const diags = setupTestComponent(
+ ``,
+ `nested = { nested1: { nested2: { source() { return { decrement() { } } } } } }`,
+ );
+
+ expect(diags.length).toBe(1);
+ expect(getSourceCodeForDiagnostic(diags[0])).toBe(
+ `(click)="nested.nested1.nested2.source().decrement"`,
+ );
+ expect(diags[0].messageText).toBe(
+ generateDiagnosticText('nested.nested1.nested2.source().decrement()'),
+ );
+ });
+
+ it('should produce a diagnostic when a function that returns a function is not invoked', () => {
+ const diags = setupTestComponent(
+ ``,
+ `incrementAndLaterDecrement(): () => void { return () => {} }`,
+ );
+
+ expect(diags.length).toBe(1);
+ expect(getSourceCodeForDiagnostic(diags[0])).toBe(`(click)="incrementAndLaterDecrement"`);
+ expect(diags[0].messageText).toBe(generateDiagnosticText('incrementAndLaterDecrement()'));
+ });
+
+ it('should not produce a diagnostic when an invoked function returns a function', () => {
+ const diags = setupTestComponent(
+ ``,
+ `incrementAndLaterDecrement(): () => void { return () => {} }`,
+ );
+
+ expect(diags.length).toBe(0);
+ });
+
+ it('should not produce a warning when the function is not invoked in two-way-binding', () => {
+ const diags = setupTestComponent(
+ ``,
+ `increment() { }`,
+ );
+
+ expect(diags.length).toBe(0);
+ });
+
+ it('should not produce a warning when the function is invoked', () => {
+ const diags = setupTestComponent(
+ `
+
+
+
+ `,
+ `
+ counter = { increment() { } }
+ increment() { }
+ `,
+ );
+
+ expect(diags.length).toBe(0);
+ });
+ });
+});
+
+function setupTestComponent(template: string, classField: string) {
+ const fileName = absoluteFrom('/main.ts');
+ const {program, templateTypeChecker} = setup([
+ {
+ fileName,
+ templates: {'TestCmp': template},
+ source: `export class TestCmp { ${classField} }`,
+ },
+ ]);
+ const sf = getSourceFileOrError(program, fileName);
+ const component = getClass(sf, 'TestCmp');
+ const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
+ templateTypeChecker,
+ program.getTypeChecker(),
+ [uninvokedFunctionInEventBindingFactory],
+ {} /* options */,
+ );
+
+ return extendedTemplateChecker.getDiagnosticsForComponent(component);
+}
+
+function generateDiagnosticText(text: string): string {
+ return `Function in event binding should be invoked: ${text}`;
+}