Skip to content

Commit 163dbea

Browse files
committed
fix(linter/prefer-for-of): false positive with diff member expression (#13201)
fixes <#13200>
1 parent 76a9865 commit 163dbea

File tree

2 files changed

+53
-16
lines changed

2 files changed

+53
-16
lines changed

crates/oxc_linter/src/rules/typescript/prefer_for_of.rs

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use oxc_macros::declare_oxc_lint;
1010
use oxc_span::{GetSpan, Span};
1111
use oxc_syntax::operator::{AssignmentOperator, BinaryOperator, UnaryOperator, UpdateOperator};
1212

13-
use crate::{AstNode, context::LintContext, rule::Rule};
13+
use crate::{AstNode, context::LintContext, rule::Rule, utils::is_same_expression};
1414

1515
fn prefer_for_of_diagnostic(span: Span) -> OxcDiagnostic {
1616
OxcDiagnostic::warn(
@@ -145,7 +145,7 @@ impl Rule for PreferForOf {
145145
return;
146146
}
147147

148-
let array_name = {
148+
let (array_name, array_expr) = {
149149
let Some(mem_expr) = test_expr.right.as_member_expression() else {
150150
return;
151151
};
@@ -154,7 +154,8 @@ impl Rule for PreferForOf {
154154
return;
155155
}
156156

157-
match mem_expr.object() {
157+
let array_expr = mem_expr.object();
158+
let array_name = match mem_expr.object() {
158159
Expression::Identifier(id) => id.name.as_str(),
159160
expr @ match_member_expression!(Expression) => {
160161
match expr.to_member_expression().static_property_name() {
@@ -163,7 +164,9 @@ impl Rule for PreferForOf {
163164
}
164165
}
165166
_ => return,
166-
}
167+
};
168+
169+
(array_name, array_expr)
167170
};
168171

169172
let Some(update_expr) = &for_stmt.update else {
@@ -198,7 +201,7 @@ impl Rule for PreferForOf {
198201
}
199202

200203
// Check if this is a non-array access that prevents conversion
201-
prevents_for_of_non_array_access(parent, array_name)
204+
prevents_for_of_non_array_access(parent, array_expr, ctx)
202205
}) {
203206
return;
204207
}
@@ -264,20 +267,15 @@ fn prevents_for_of_array_access(
264267
}
265268

266269
/// Check if this is a non-array access that prevents conversion
267-
fn prevents_for_of_non_array_access(parent: &AstNode, array_name: &str) -> bool {
270+
fn prevents_for_of_non_array_access(
271+
parent: &AstNode,
272+
array_expr: &Expression,
273+
ctx: &LintContext,
274+
) -> bool {
268275
let parent_kind = parent.kind();
269276

270277
if let Some(mem_expr) = parent_kind.as_member_expression_kind() {
271-
match &mem_expr.object() {
272-
Expression::Identifier(id) => id.name.as_str() != array_name,
273-
expr if expr.is_member_expression() => {
274-
match expr.to_member_expression().static_property_name() {
275-
Some(prop_name) => prop_name != array_name,
276-
None => true,
277-
}
278-
}
279-
_ => true,
280-
}
278+
!is_same_expression(mem_expr.object(), array_expr, ctx)
281279
} else {
282280
true
283281
}
@@ -370,6 +368,18 @@ fn test() {
370368
"for (var j = 0; j < 10; j++) {}",
371369
"const arr = [];
372370
for (i = 0; i < arr.length; i++) { el = arr[i]; console.log(i, el); }",
371+
"for (let x = 0; x < series.data.length; x++) { let newValue = series.data[x].y; for (const otherSeries of subseries) { newValue -= otherSeries.data[x].y; } series.data[x].y = newValue; }",
372+
// Deep nesting test cases
373+
"const a = { b: { c: { d: { e: [1, 2, 3] } } } };
374+
const x = { b: { c: { d: { e: [4, 5, 6] } } } };
375+
for (let i = 0; i < a.b.c.d.e.length; i++) {
376+
console.log(x.b.c.d.e[i]); // Different object with same path
377+
}",
378+
"const obj1 = { a: { b: [1, 2, 3] } };
379+
const obj2 = { a: { b: [4, 5, 6] } };
380+
for (let i = 0; i < obj1.a.b.length; i++) {
381+
console.log(obj2.a.b[i]); // Different object
382+
}",
373383
];
374384

375385
let fail = vec![
@@ -401,6 +411,15 @@ fn test() {
401411
"const array = []; for (let i = 0; i < array.length; i++) { let foo = array[i]; }",
402412
"const array = []; for (let i = 0; i < array.length; i++) { const foo = array[i]; }",
403413
"const array = []; for (let i = 0; i < array.length; i++) { var foo = array[i], bar = 1; }",
414+
// Deep nesting test cases that should trigger warning
415+
"const a = { b: { c: { d: { e: [1, 2, 3] } } } };
416+
for (let i = 0; i < a.b.c.d.e.length; i++) {
417+
console.log(a.b.c.d.e[i]); // Same deeply nested array
418+
}",
419+
"const obj = { a: { b: [1, 2, 3] } };
420+
for (let i = 0; i < obj.a.b.length; i++) {
421+
console.log(obj.a.b[i]); // Same nested array
422+
}",
404423
];
405424

406425
Tester::new(PreferForOf::NAME, PreferForOf::PLUGIN, pass, fail).test_and_snapshot();

crates/oxc_linter/src/snapshots/typescript_prefer_for_of.snap

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,21 @@ source: crates/oxc_linter/src/tester.rs
185185
· ────────────────────────────────
186186
╰────
187187
help: Consider using a for-of loop for this simple iteration.
188+
189+
typescript-eslint(prefer-for-of): Expected a `for-of` loop instead of a `for` loop with this simple iteration.
190+
╭─[prefer_for_of.tsx:2:15]
191+
1const a = { b: { c: { d: { e: [1, 2, 3] } } } };
192+
2for (let i = 0; i < a.b.c.d.e.length; i++) {
193+
· ────────────────────────────────────
194+
3console.log(a.b.c.d.e[i]); // Same deeply nested array
195+
╰────
196+
help: Consider using a for-of loop for this simple iteration.
197+
198+
typescript-eslint(prefer-for-of): Expected a `for-of` loop instead of a `for` loop with this simple iteration.
199+
╭─[prefer_for_of.tsx:2:15]
200+
1const obj = { a: { b: [1, 2, 3] } };
201+
2for (let i = 0; i < obj.a.b.length; i++) {
202+
· ──────────────────────────────────
203+
3console.log(obj.a.b[i]); // Same nested array
204+
╰────
205+
help: Consider using a for-of loop for this simple iteration.

0 commit comments

Comments
 (0)