Skip to content

Commit 8eacb2d

Browse files
committed
fix(linter/typescript/no-non-null-asserted-optional-chain): detect assertions after continued optional chains (#25659)
## Summary - detect non-null assertions after continued optional chains - preserve optional-chain labels and parenthesized chain boundaries - add focused diagnostic and suggestion regression coverage Closes #25622.
1 parent db44651 commit 8eacb2d

2 files changed

Lines changed: 103 additions & 34 deletions

File tree

‎crates/oxc_linter/src/rules/typescript/no_non_null_asserted_optional_chain.rs‎

Lines changed: 76 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -68,40 +68,16 @@ impl Rule for NoNonNullAssertedOptionalChain {
6868
return;
6969
};
7070

71-
let chain_span = match non_null_expr.expression.get_inner_expression() {
72-
Expression::ChainExpression(chain) => match &chain.expression {
73-
ChainElement::ComputedMemberExpression(member) if member.optional => {
74-
Some(member.object.span())
75-
}
76-
ChainElement::StaticMemberExpression(member) if member.optional => {
77-
Some(member.object.span())
78-
}
79-
ChainElement::PrivateFieldExpression(member) if member.optional => {
80-
Some(member.object.span())
81-
}
82-
ChainElement::CallExpression(call) if call.optional => Some(call.callee.span()),
83-
_ => None,
84-
},
85-
Expression::CallExpression(call) => {
86-
if call.optional && !is_parent_member_or_call(node, ctx) {
87-
Some(call.callee.span())
88-
} else if let Some(member) = call.callee.as_member_expression() {
89-
if member.optional() && !is_parent_member_or_call(node, ctx) {
90-
Some(member.object().span())
91-
} else {
92-
None
93-
}
94-
} else {
95-
None
96-
}
97-
}
98-
expr @ match_member_expression!(Expression) => {
99-
let member_expr = expr.to_member_expression();
100-
if member_expr.optional() && !is_parent_member_or_call(node, ctx) {
101-
Some(member_expr.object().span())
102-
} else {
103-
None
104-
}
71+
let expression = non_null_expr.expression.get_inner_expression();
72+
let chain_span = match expression {
73+
Expression::ChainExpression(chain) => find_optional_chain_span(&chain.expression),
74+
Expression::CallExpression(_)
75+
| Expression::ComputedMemberExpression(_)
76+
| Expression::StaticMemberExpression(_)
77+
| Expression::PrivateFieldExpression(_)
78+
if !is_parent_member_or_call(node, ctx) =>
79+
{
80+
find_optional_chain_span_in_expression(expression)
10581
}
10682
_ => None,
10783
};
@@ -124,6 +100,66 @@ impl Rule for NoNonNullAssertedOptionalChain {
124100
}
125101
}
126102

103+
fn find_optional_chain_span(chain: &ChainElement<'_>) -> Option<Span> {
104+
let expression = match chain {
105+
ChainElement::CallExpression(call) => {
106+
if call.optional {
107+
return Some(call.callee.span());
108+
}
109+
&call.callee
110+
}
111+
ChainElement::TSNonNullExpression(non_null) => &non_null.expression,
112+
ChainElement::ComputedMemberExpression(member) => {
113+
if member.optional {
114+
return Some(member.object.span());
115+
}
116+
&member.object
117+
}
118+
ChainElement::StaticMemberExpression(member) => {
119+
if member.optional {
120+
return Some(member.object.span());
121+
}
122+
&member.object
123+
}
124+
ChainElement::PrivateFieldExpression(member) => {
125+
if member.optional {
126+
return Some(member.object.span());
127+
}
128+
&member.object
129+
}
130+
};
131+
132+
find_optional_chain_span_in_expression(expression)
133+
}
134+
135+
fn find_optional_chain_span_in_expression(mut expression: &Expression<'_>) -> Option<Span> {
136+
loop {
137+
if matches!(expression, Expression::ParenthesizedExpression(_)) {
138+
return None;
139+
}
140+
141+
match expression.get_inner_expression() {
142+
Expression::ChainExpression(chain) => {
143+
return find_optional_chain_span(&chain.expression);
144+
}
145+
Expression::CallExpression(call) => {
146+
if call.optional {
147+
return Some(call.callee.span());
148+
}
149+
expression = &call.callee;
150+
}
151+
expr @ match_member_expression!(Expression) => {
152+
let member = expr.to_member_expression();
153+
if member.optional() {
154+
return Some(member.object().span());
155+
}
156+
expression = member.object();
157+
}
158+
_ => return None,
159+
}
160+
}
161+
}
162+
127163
fn is_parent_member_or_call(node: &AstNode<'_>, ctx: &LintContext<'_>) -> bool {
128164
let parent_kind = ctx.nodes().parent_kind(node.id());
129165
matches!(parent_kind, AstKind::CallExpression(_)) || parent_kind.is_member_expression_kind()
@@ -154,7 +190,10 @@ fn test() {
154190
"foo?.bar!;",
155191
"foo?.['bar']!;",
156192
"foo?.bar()!;",
193+
"foo?.bar().baz()!;",
194+
"foo?.bar.baz!;",
157195
"foo.bar?.()!;",
196+
"foo.bar?.().baz!;",
158197
"(foo?.bar)!.baz",
159198
"(foo?.bar)!().baz",
160199
"(foo?.bar)!",
@@ -167,6 +206,9 @@ fn test() {
167206
("foo?.bar!", "foo?.bar"),
168207
("foo?.['bar']!", "foo?.['bar']"),
169208
("foo?.bar()!", "foo?.bar()"),
209+
("foo?.bar().baz()!", "foo?.bar().baz()"),
210+
("foo?.bar.baz!", "foo?.bar.baz"),
211+
("foo.bar?.().baz!", "foo.bar?.().baz"),
170212
("(foo?.bar)!.baz", "(foo?.bar).baz"),
171213
("(foo?.bar)!().baz", "(foo?.bar)().baz"),
172214
("(foo?.bar)!", "(foo?.bar)"),

‎crates/oxc_linter/src/snapshots/typescript_no_non_null_asserted_optional_chain.snap‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,24 @@ source: crates/oxc_linter/src/tester.rs
2929
╰────
3030
help: Remove the non-null assertion.
3131

32+
typescript(no-non-null-asserted-optional-chain): Optional chain expressions can return undefined by design: using a non-null assertion is unsafe and wrong.
33+
╭─[no_non_null_asserted_optional_chain.tsx:1:17]
34+
1foo?.bar().baz()!;
35+
· ┬ ┬
36+
· │ ╰── non-null assertion made after optional chain
37+
· ╰── optional chain used
38+
╰────
39+
help: Remove the non-null assertion.
40+
41+
typescript(no-non-null-asserted-optional-chain): Optional chain expressions can return undefined by design: using a non-null assertion is unsafe and wrong.
42+
╭─[no_non_null_asserted_optional_chain.tsx:1:13]
43+
1foo?.bar.baz!;
44+
· ┬ ┬
45+
· │ ╰── non-null assertion made after optional chain
46+
· ╰── optional chain used
47+
╰────
48+
help: Remove the non-null assertion.
49+
3250
typescript(no-non-null-asserted-optional-chain): Optional chain expressions can return undefined by design: using a non-null assertion is unsafe and wrong.
3351
╭─[no_non_null_asserted_optional_chain.tsx:1:12]
3452
1foo.bar?.()!;
@@ -38,6 +56,15 @@ source: crates/oxc_linter/src/tester.rs
3856
╰────
3957
help: Remove the non-null assertion.
4058

59+
typescript(no-non-null-asserted-optional-chain): Optional chain expressions can return undefined by design: using a non-null assertion is unsafe and wrong.
60+
╭─[no_non_null_asserted_optional_chain.tsx:1:16]
61+
1foo.bar?.().baz!;
62+
· ┬ ┬
63+
· │ ╰── non-null assertion made after optional chain
64+
· ╰── optional chain used
65+
╰────
66+
help: Remove the non-null assertion.
67+
4168
typescript(no-non-null-asserted-optional-chain): Optional chain expressions can return undefined by design: using a non-null assertion is unsafe and wrong.
4269
╭─[no_non_null_asserted_optional_chain.tsx:1:11]
4370
1 │ (foo?.bar)!.baz

0 commit comments

Comments
 (0)