Skip to content

Commit 93c65e7

Browse files
JoostKthePunderWoman
authored andcommitted
feat(compiler-cli): add extended diagnostic for non-nullable optional chains (#46686)
This commit adds an extended diagnostics check that is similar to the nullish coalescing check, but targeting optional chains. If the receiver expression of the optional chain is non-nullable, then the extended diagnostic can report an error or warning that can be fixed by changing the optional chain into a regular access. Closes #44870 PR Close #46686
1 parent a6d5fe2 commit 93c65e7

File tree

11 files changed

+417
-3
lines changed

11 files changed

+417
-3
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export enum ErrorCode {
6060
NGMODULE_REEXPORT_NAME_COLLISION = 6006,
6161
NGMODULE_VE_DEPENDENCY_ON_IVY_LIB = 6999,
6262
NULLISH_COALESCING_NOT_NULLABLE = 8102,
63+
OPTIONAL_CHAIN_NOT_NULLABLE = 8107,
6364
// (undocumented)
6465
PARAM_MISSING_TOKEN = 2003,
6566
// (undocumented)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export enum ExtendedTemplateDiagnosticName {
1515
// (undocumented)
1616
NULLISH_COALESCING_NOT_NULLABLE = "nullishCoalescingNotNullable",
1717
// (undocumented)
18+
OPTIONAL_CHAIN_NOT_NULLABLE = "optionalChainNotNullable",
19+
// (undocumented)
1820
SUFFIX_NOT_SUPPORTED = "suffixNotSupported",
1921
// (undocumented)
2022
TEXT_ATTRIBUTE_NOT_BINDING = "textAttributeNotBinding"

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,18 @@ export enum ErrorCode {
278278
*/
279279
SUFFIX_NOT_SUPPORTED = 8106,
280280

281+
/**
282+
* The left side of an optional chain operation is not nullable.
283+
*
284+
* ```
285+
* {{ foo?.bar }}
286+
* {{ foo?.['bar'] }}
287+
* {{ foo?.() }}
288+
* ```
289+
* When the type of foo doesn't include `null` or `undefined`.
290+
*/
291+
OPTIONAL_CHAIN_NOT_NULLABLE = 8107,
292+
281293
/**
282294
* The template type-checking engine would need to generate an inline type check block for a
283295
* 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
@@ -18,6 +18,7 @@
1818
export enum ExtendedTemplateDiagnosticName {
1919
INVALID_BANANA_IN_BOX = 'invalidBananaInBox',
2020
NULLISH_COALESCING_NOT_NULLABLE = 'nullishCoalescingNotNullable',
21+
OPTIONAL_CHAIN_NOT_NULLABLE = 'optionalChainNotNullable',
2122
MISSING_CONTROL_FLOW_DIRECTIVE = 'missingControlFlowDirective',
2223
TEXT_ATTRIBUTE_NOT_BINDING = 'textAttributeNotBinding',
2324
MISSING_NGFOROF_LET = 'missingNgForOfLet',

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ ts_library(
1616
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_control_flow_directive",
1717
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_ngforof_let",
1818
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/nullish_coalescing_not_nullable",
19+
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/optional_chain_not_nullable",
1920
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/suffix_not_supported",
2021
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/text_attribute_not_binding",
2122
"@npm//typescript",
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
load("//tools:defaults.bzl", "ts_library")
2+
3+
ts_library(
4+
name = "optional_chain_not_nullable",
5+
srcs = ["index.ts"],
6+
visibility = ["//packages/compiler-cli/src/ngtsc:__subpackages__"],
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/typecheck/api",
12+
"//packages/compiler-cli/src/ngtsc/typecheck/extended/api",
13+
"@npm//typescript",
14+
],
15+
)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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, SafeCall, SafeKeyedRead, SafePropertyRead, TmplAstNode} from '@angular/compiler';
10+
import ts from 'typescript';
11+
12+
import {NgCompilerOptions} from '../../../../core/api';
13+
import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../../diagnostics';
14+
import {NgTemplateDiagnostic, SymbolKind} from '../../../api';
15+
import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '../../api';
16+
17+
/**
18+
* Ensures the left side of an optional chain operation is nullable.
19+
* Returns diagnostics for the cases where the operator is useless.
20+
* This check should only be use if `strictNullChecks` is enabled,
21+
* otherwise it would produce inaccurate results.
22+
*/
23+
class OptionalChainNotNullableCheck extends
24+
TemplateCheckWithVisitor<ErrorCode.OPTIONAL_CHAIN_NOT_NULLABLE> {
25+
override code = ErrorCode.OPTIONAL_CHAIN_NOT_NULLABLE as const;
26+
27+
override visitNode(
28+
ctx: TemplateContext<ErrorCode.OPTIONAL_CHAIN_NOT_NULLABLE>, component: ts.ClassDeclaration,
29+
node: TmplAstNode|AST): NgTemplateDiagnostic<ErrorCode.OPTIONAL_CHAIN_NOT_NULLABLE>[] {
30+
if (!(node instanceof SafeCall) && !(node instanceof SafePropertyRead) &&
31+
!(node instanceof SafeKeyedRead))
32+
return [];
33+
34+
const symbolLeft = ctx.templateTypeChecker.getSymbolOfNode(node.receiver, component);
35+
if (symbolLeft === null || symbolLeft.kind !== SymbolKind.Expression) {
36+
return [];
37+
}
38+
const typeLeft = symbolLeft.tsType;
39+
if (typeLeft.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
40+
// We should not make assumptions about the any and unknown types; using a nullish coalescing
41+
// operator is acceptable for those.
42+
return [];
43+
}
44+
45+
// If the left operand's type is different from its non-nullable self, then it must
46+
// contain a null or undefined so this nullish coalescing operator is useful. No diagnostic to
47+
// report.
48+
if (typeLeft.getNonNullableType() !== typeLeft) return [];
49+
50+
const symbol = ctx.templateTypeChecker.getSymbolOfNode(node, component)!;
51+
if (symbol.kind !== SymbolKind.Expression) {
52+
return [];
53+
}
54+
const templateMapping =
55+
ctx.templateTypeChecker.getTemplateMappingAtTcbLocation(symbol.tcbLocation);
56+
if (templateMapping === null) {
57+
return [];
58+
}
59+
const advice = node instanceof SafePropertyRead ?
60+
`the '?.' operator can be replaced with the '.' operator` :
61+
`the '?.' operator can be safely removed`;
62+
const diagnostic = ctx.makeTemplateDiagnostic(
63+
templateMapping.span,
64+
`The left side of this optional chain operation does not include 'null' or 'undefined' in its type, therefore ${
65+
advice}.`);
66+
return [diagnostic];
67+
}
68+
}
69+
70+
export const factory: TemplateCheckFactory<
71+
ErrorCode.OPTIONAL_CHAIN_NOT_NULLABLE,
72+
ExtendedTemplateDiagnosticName.OPTIONAL_CHAIN_NOT_NULLABLE> = {
73+
code: ErrorCode.OPTIONAL_CHAIN_NOT_NULLABLE,
74+
name: ExtendedTemplateDiagnosticName.OPTIONAL_CHAIN_NOT_NULLABLE,
75+
create: (options: NgCompilerOptions) => {
76+
// Require `strictNullChecks` to be enabled.
77+
const strictNullChecks =
78+
options.strictNullChecks === undefined ? !!options.strict : !!options.strictNullChecks;
79+
if (!strictNullChecks) {
80+
return null;
81+
}
82+
83+
return new OptionalChainNotNullableCheck();
84+
},
85+
};

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {factory as invalidBananaInBoxFactory} from './checks/invalid_banana_in_b
1313
import {factory as missingControlFlowDirectiveFactory} from './checks/missing_control_flow_directive';
1414
import {factory as missingNgForOfLetFactory} from './checks/missing_ngforof_let';
1515
import {factory as nullishCoalescingNotNullableFactory} from './checks/nullish_coalescing_not_nullable';
16+
import {factory as optionalChainNotNullableFactory} from './checks/optional_chain_not_nullable';
1617
import {factory as suffixNotSupportedFactory} from './checks/suffix_not_supported';
1718
import {factory as textAttributeNotBindingFactory} from './checks/text_attribute_not_binding';
1819

@@ -22,6 +23,7 @@ export const ALL_DIAGNOSTIC_FACTORIES:
2223
readonly TemplateCheckFactory<ErrorCode, ExtendedTemplateDiagnosticName>[] = [
2324
invalidBananaInBoxFactory,
2425
nullishCoalescingNotNullableFactory,
26+
optionalChainNotNullableFactory,
2527
missingControlFlowDirectiveFactory,
2628
textAttributeNotBindingFactory,
2729
missingNgForOfLetFactory,
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 = ["optional_chain_not_nullable_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/optional_chain_not_nullable",
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_es2015"],
24+
deps = [
25+
":test_lib",
26+
],
27+
)

0 commit comments

Comments
 (0)