Repository: oxc-project/oxc
Pull request #11154: feat(linter): unicorn/no-for-loop
Author: login: camc314
Labels: name: C-enhancement
description: Category - New feature or request
name: A-linter
description: Area - Linter
Created: 2025-05-19T10:59:54Z | Merged: 2025-05-19T11:10:23Z



Diff:
diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs
index bd8661376bad5..2644d5165916e 100644
--- a/crates/oxc_linter/src/rules.rs
+++ b/crates/oxc_linter/src/rules.rs
@@ -350,6 +350,7 @@ mod unicorn {
     pub mod no_console_spaces;
     pub mod no_document_cookie;
     pub mod no_empty_file;
+    pub mod no_for_loop;
     pub mod no_hex_escape;
     pub mod no_instanceof_array;
     pub mod no_invalid_fetch_options;
@@ -984,6 +985,7 @@ oxc_macros::declare_all_lint_rules! {
     unicorn::explicit_length_check,
     unicorn::filename_case,
     unicorn::new_for_builtins,
+    unicorn::no_for_loop,
     unicorn::no_unnecessary_slice_end,
     unicorn::no_accessor_recursion,
     unicorn::no_invalid_fetch_options,
diff --git a/crates/oxc_linter/src/rules/unicorn/no_for_loop.rs b/crates/oxc_linter/src/rules/unicorn/no_for_loop.rs
new file mode 100644
index 0000000000000..df4f54372a2f8
--- /dev/null
+++ b/crates/oxc_linter/src/rules/unicorn/no_for_loop.rs
@@ -0,0 +1,425 @@
+use oxc_ast::{AstKind, ast::*};
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_macros::declare_oxc_lint;
+use oxc_semantic::Reference;
+use oxc_span::{GetSpan, Span};
+
+use crate::{AstNode, ast_util::get_declaration_of_variable, context::LintContext, rule::Rule};
+
+fn no_for_loop_diagnostic(span: Span) -> OxcDiagnostic {
+    OxcDiagnostic::warn("Use a `for-of` loop instead of this classical `for` loop.")
+        .with_help("Rewrite the loop as `for (const el of array) { … }`")
+        .with_label(span)
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct NoForLoop;
+
+declare_oxc_lint!(
+    /// ### What it does
+    ///
+    /// Enforces the use of `for-of` loops over classical counter based `for` loops.
+    ///
+    /// ### Why is this bad?
+    ///
+    /// * A `for-of` loop makes intent explicit – _iterate over all elements_.
+    /// * It removes boiler-plate (`i`, `arr.length`, manual `++`).
+    ///
+    /// ### Examples
+    ///
+    /// Examples of **incorrect** code for this rule:
+    /// ```js
+    /// for (let i = 0; i < items.length; i++) {
+    ///   const item = items[i];
+    ///   console.log(item);
+    /// }
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```js
+    /// for (const item of items) {
+    ///   console.log(item);
+    /// }
+    /// ```
+    NoForLoop,
+    unicorn,
+    style,
+    pending
+);
+
+impl Rule for NoForLoop {
+    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+        let AstKind::ForStatement(for_stmt) = node.kind() else { return };
+
+        let Some(init) = &for_stmt.init else { return };
+        let ForStatementInit::VariableDeclaration(init) = init else { return };
+        if init.declarations.len() != 1
+            || init.declarations[0]
+                .init
+                .as_ref()
+                .is_some_and(|e| !e.get_inner_expression().is_number_0())
+        {
+            return;
+        }
+        let BindingPatternKind::BindingIdentifier(ident) = &init.declarations[0].id.kind else {
+            return;
+        };
+
+        let Some(test) = &for_stmt.test else { return };
+        let Expression::BinaryExpression(bin_expr) = test.get_inner_expression() else { return };
+        let array = {
+            let (lesser, greater) = match bin_expr.operator {
+                BinaryOperator::LessThan => (&bin_expr.left, &bin_expr.right),
+                BinaryOperator::GreaterThan => (&bin_expr.right, &bin_expr.left),
+                _ => return,
+            };
+
+            if lesser.get_identifier_reference().is_none_or(|lesser_ident| {
+                ctx.scoping().get_reference(lesser_ident.reference_id()).symbol_id()
+                    != Some(ident.symbol_id())
+            }) {
+                return;
+            }
+
+            let Some(greater) = greater.get_inner_expression().as_member_expression() else {
+                return;
+            };
+            if greater.static_property_name().is_none_or(|name| name != "length") {
+                return;
+            }
+            let Some(ident) = greater.object().get_identifier_reference() else {
+                return;
+            };
+            ident
+        };
+
+        let Some(update) = &for_stmt.update else { return };
+        match update {
+            Expression::UpdateExpression(u) => {
+                if u.operator != UpdateOperator::Increment {
+                    return;
+                }
+                let SimpleAssignmentTarget::AssignmentTargetIdentifier(update_ident) = &u.argument
+                else {
+                    return;
+                };
+
+                if ctx.scoping().get_reference(update_ident.reference_id()).symbol_id()
+                    != Some(ident.symbol_id())
+                {
+                    return;
+                }
+            }
+            Expression::AssignmentExpression(u) => {
+                let AssignmentTarget::AssignmentTargetIdentifier(update_ident) = &u.left else {
+                    return;
+                };
+                if ctx.scoping().get_reference(update_ident.reference_id()).symbol_id()
+                    != Some(ident.symbol_id())
+                {
+                    return;
+                }
+
+                match u.operator {
+                    AssignmentOperator::Addition
+                        if u.right.get_inner_expression().is_number_value(1.0) => {}
+                    AssignmentOperator::Assign => {
+                        let Expression::BinaryExpression(right) = &u.right else { return };
+                        if right.operator != BinaryOperator::Addition {
+                            return;
+                        }
+                        if (!(right.left.get_inner_expression().is_number_value(1.0))
+                            && (right
+                                .right
+                                .get_inner_expression()
+                                .get_identifier_reference()
+                                .is_some_and(|i| {
+                                    ctx.scoping().get_reference(i.reference_id()).symbol_id()
+                                        == Some(ident.symbol_id())
+                                })))
+                            && !((right.right.get_inner_expression().is_number_value(1.0))
+                                && (right
+                                    .left
+                                    .get_inner_expression()
+                                    .get_identifier_reference()
+                                    .is_some_and(|i| {
+                                        ctx.scoping().get_reference(i.reference_id()).symbol_id()
+                                            == Some(ident.symbol_id())
+                                    })))
+                        {
+                            return;
+                        }
+                    }
+                    _ => return,
+                }
+            }
+            _ => return,
+        }
+
+        let Some(array_symbol_id) = ctx.scoping().get_reference(array.reference_id()).symbol_id()
+        else {
+            return;
+        };
+
+        if let Some(decl) = get_declaration_of_variable(array, ctx.semantic()) {
+            if let AstKind::VariableDeclarator(decl) = decl.kind() {
+                if let BindingPatternKind::BindingIdentifier(_) = decl.id.kind {
+                    if let Some(
+                        Expression::StringLiteral(_)
+                        | Expression::NumericLiteral(_)
+                        | Expression::BooleanLiteral(_),
+                    ) = decl.init
+                    {
+                        return;
+                    }
+                }
+            }
+        }
+
+        let Statement::BlockStatement(body_block) = &for_stmt.body else { return };
+
+        let mut is_reference_used_as_index = false;
+        for reference in ctx.symbol_references(ident.symbol_id()) {
+            if !body_block.span.contains_inclusive(reference_span(reference, ctx)) {
+                continue;
+            }
+            if reference.is_write() {
+                return;
+            }
+            let node = ctx.nodes().get_node(reference.node_id());
+            let parent = ctx.nodes().parent_node(node.id());
+            if parent.is_some_and(|node| {
+                if let AstKind::MemberExpression(member_expr) = node.kind() {
+                    member_expr.is_computed()
+                        && member_expr.object().get_identifier_reference().is_some_and(
+                            |reference| {
+                                ctx.scoping().get_reference(reference.reference_id()).symbol_id()
+                                    == Some(array_symbol_id)
+                            },
+                        )
+                } else {
+                    false
+                }
+            }) {
+                is_reference_used_as_index = true;
+            }
+        }
+
+        if !is_reference_used_as_index {
+            return;
+        }
+
+        for reference in ctx.symbol_references(array_symbol_id) {
+            if !body_block.span.contains_inclusive(reference_span(reference, ctx)) {
+                continue;
+            }
+            if reference.is_write() {
+                return;
+            }
+            let Some(parent) = ctx.nodes().parent_node(reference.node_id()) else { continue };
+            let AstKind::MemberExpression(member_expr) = parent.kind() else { return };
+            let MemberExpression::ComputedMemberExpression(computed_member_expr) = member_expr
+            else {
+                return;
+            };
+            if computed_member_expr
+                .expression
+                .get_inner_expression()
+                .get_identifier_reference()
+                .is_none_or(|r| {
+                    ctx.scoping().get_reference(r.reference_id()).symbol_id()
+                        != Some(ident.symbol_id())
+                })
+            {
+                return;
+            }
+            if let Some(AstKind::SimpleAssignmentTarget(_)) = ctx.nodes().parent_kind(parent.id()) {
+                return;
+            }
+        }
+
+        ctx.diagnostic(no_for_loop_diagnostic(Span::sized(for_stmt.span.start, 3)));
+    }
+}
+
+fn reference_span(reference: &Reference, ctx: &LintContext<'_>) -> Span {
+    ctx.nodes().get_node(reference.node_id()).span()
+}
+
+#[test]
+fn test() {
+    use crate::tester::Tester;
+
+    let pass = vec![
+        "for (;;);",
+        "for (;;) {}",
+        "for (a;; c) { d }",
+        "for (a; b;) { d }",
+        "for (the; love; of) { god }",
+        "for ([a] = b; f(c); d--) { arr[d] }",
+        "for (var a = b; c < arr.length; d++) { arr[e] }",
+        "for (const x of xs) {}",
+        "for (var j = 0; j < 10; j++) {}",
+        "for (i = 0; i < arr.length; i++) {
+				el = arr[i];
+				console.log(i, el);
+			}",
+        "var foo = function () {
+				for (var i = 0; i < bar.length; i++) {
+				}
+			};",
+        "for (let i = 0, j = 0; i < arr.length; i++) {
+				const el = arr[i];
+				console.log(i, el);
+			}",
+        "for (let {i} = 0; i < arr.length; i++) {
+				const el = arr[i];
+				console.log(i, el);
+			}",
+        "for (let i = 0; f(i, arr.length); i++) {
+				const el = arr[i];
+				console.log(i, el);
+			}",
+        "for (let i = 0; i < arr.size; i++) {
+				const el = arr[i];
+				console.log(i, el);
+			}",
+        "for (let i = 0; j < arr.length; i++) {
+				const el = arr[i];
+				console.log(i, el);
+			}",
+        "for (let i = 0; i <= arr.length; i++) {
+				const el = arr[i];
+				console.log(i, el);
+			}",
+        "for (let i = 0; arr.length > i;) {
+				let el = arr[i];
+				console.log(i, el);
+			}",
+        "for (let i = 0; arr.length > i; i--) {
+				let el = arr[i];
+				console.log(i, el);
+			}",
+        "for (let i = 0; arr.length > i; f(i)) {
+				let el = arr[i];
+				console.log(i, el);
+			}",
+        "for (let i = 0; arr.length > i; i = f(i)) {
+				let el = arr[i];
+				console.log(i, el);
+			}",
+        "const arr = []; for (let i = 0; arr.length > i; i ++);",
+        "const arr = []; for (let i = 0; arr.length > i; i ++) console.log(NaN)",
+        "const arr = []; for (let i = 0; i < arr.length; ++i) {
+				const el = f(i);
+				console.log(i, el);
+			}",
+        "const xs = [];
+        for (var j = 0; j < xs.length; j++) {
+				var x;
+			}",
+        "const xs = [];
+        for (var j = 0; j < xs.length; j++) {
+				var {x} = y;
+			}",
+        "const arr = [];
+        for (let i = 0; i < arr.length; i++) {
+				console.log(i);
+			}",
+        "const input = [];
+        for (let i = 0; i < input.length; i++) {
+				const el = input[i];
+				i++;
+				console.log(i, el);
+			}",
+        "const input = [];
+        for (let i = 0; i < input.length; i++) {
+				const el = input[i];
+				i = 4;
+				console.log(i, el);
+			}",
+        "const arr = [];
+        for (let i = 0; i < arr.length;i++) {
+				console.log(arr[i]);
+				arr.reverse();
+			}",
+        "const arr = [];
+        for (let i = 0; i < arr.length; i++) {
+				arr[i] = i + 2;
+			}",
+        "const cities = [];
+        for (let i = 0; i < cities.length; i++) {
+				const foo = function () {
+					console.log(cities)
+				}
+			}",
+        r#"const notArray = "abc"; for (let i = 0; i < notArray.length; i++) { console.log(notArray[i]); }"#,
+        "const notArray = 123; for (let i = 0; i < notArray.length; i++) { console.log(notArray[i]); }",
+        "const notArray = true; for (let i = 0; i < notArray.length; i++) { console.log(notArray[i]); }",
+        "for (;;);",
+        "for (;;) {}",
+        "for (var j = 0; j < 10; j++) {}",
+        "const arr = [];
+        for (i = 0; i < arr.length; i++) {
+				el = arr[i];
+				console.log(i, el);
+			}",
+        "const bar = [];
+        var foo = function () {
+				for (var i = 0; i < bar.length; i++) {
+				}
+			};",
+    ];
+
+    let fail = vec![
+        "const positions = []; 
+        for (let i = 0; i < positions.length; i++) {
+        		let last: vscode.Position | vscode.Range = positions[i];
+        		let selectionRange = allProviderRanges[i];
+        	}",
+        "const positions = [];
+        for (let i = 0; i < positions.length; i++) {
+				const    last   /* comment */    : /* comment */ Position = positions[i];
+				console.log(i);
+			}",
+        "const positions = [];
+        for (let i = 0; i < positions.length; i++) {
+				let last: vscode.Position | vscode.Range = positions[i];
+			}",
+        "const arr = [];
+        for (let i = 0; i < arr.length; i += 1) {
+				console.log(arr[i])
+			}",
+        "const plugins = [];
+        for (let i = 0; i < plugins.length; i++) {
+				let plugin = plugins[i];
+				plugin = calculateSomeNewValue();
+				// ...
+			}",
+        "const array = [];
+        for (
+				let i = 0;
+				i < array.length;
+				i++
+			)
+			// comment (foo)
+				{
+					var foo = array[i];
+					foo = bar();
+				}",
+        "const array = [];
+        for (let i = 0; i < array.length; i++) {
+				let foo = array[i];
+			}",
+        "const array = [];
+        for (let i = 0; i < array.length; i++) {
+				const foo = array[i];
+			}",
+        "const array = [];
+        for (let i = 0; i < array.length; i++) {
+				var foo = array[i], bar = 1;
+			}",
+    ];
+
+    Tester::new(NoForLoop::NAME, NoForLoop::PLUGIN, pass, fail).test_and_snapshot();
+}
diff --git a/crates/oxc_linter/src/snapshots/unicorn_no_for_loop.snap b/crates/oxc_linter/src/snapshots/unicorn_no_for_loop.snap
new file mode 100644
index 0000000000000..f808d7ad97d00
--- /dev/null
+++ b/crates/oxc_linter/src/snapshots/unicorn_no_for_loop.snap
@@ -0,0 +1,83 @@
+---
+source: crates/oxc_linter/src/tester.rs
+---
+  ⚠ eslint-plugin-unicorn(no-for-loop): Use a `for-of` loop instead of this classical `for` loop.
+   ╭─[no_for_loop.tsx:2:9]
+ 1 │ const positions = []; 
+ 2 │         for (let i = 0; i < positions.length; i++) {
+   ·         ───
+ 3 │                 let last: vscode.Position | vscode.Range = positions[i];
+   ╰────
+  help: Rewrite the loop as `for (const el of array) { … }`
+
+  ⚠ eslint-plugin-unicorn(no-for-loop): Use a `for-of` loop instead of this classical `for` loop.
+   ╭─[no_for_loop.tsx:2:9]
+ 1 │ const positions = [];
+ 2 │         for (let i = 0; i < positions.length; i++) {
+   ·         ───
+ 3 │                 const    last   /* comment */    : /* comment */ Position = positions[i];
+   ╰────
+  help: Rewrite the loop as `for (const el of array) { … }`
+
+  ⚠ eslint-plugin-unicorn(no-for-loop): Use a `for-of` loop instead of this classical `for` loop.
+   ╭─[no_for_loop.tsx:2:9]
+ 1 │ const positions = [];
+ 2 │         for (let i = 0; i < positions.length; i++) {
+   ·         ───
+ 3 │                 let last: vscode.Position | vscode.Range = positions[i];
+   ╰────
+  help: Rewrite the loop as `for (const el of array) { … }`
+
+  ⚠ eslint-plugin-unicorn(no-for-loop): Use a `for-of` loop instead of this classical `for` loop.
+   ╭─[no_for_loop.tsx:2:9]
+ 1 │ const arr = [];
+ 2 │         for (let i = 0; i < arr.length; i += 1) {
+   ·         ───
+ 3 │                 console.log(arr[i])
+   ╰────
+  help: Rewrite the loop as `for (const el of array) { … }`
+
+  ⚠ eslint-plugin-unicorn(no-for-loop): Use a `for-of` loop instead of this classical `for` loop.
+   ╭─[no_for_loop.tsx:2:9]
+ 1 │ const plugins = [];
+ 2 │         for (let i = 0; i < plugins.length; i++) {
+   ·         ───
+ 3 │                 let plugin = plugins[i];
+   ╰────
+  help: Rewrite the loop as `for (const el of array) { … }`
+
+  ⚠ eslint-plugin-unicorn(no-for-loop): Use a `for-of` loop instead of this classical `for` loop.
+   ╭─[no_for_loop.tsx:2:9]
+ 1 │ const array = [];
+ 2 │         for (
+   ·         ───
+ 3 │                 let i = 0;
+   ╰────
+  help: Rewrite the loop as `for (const el of array) { … }`
+
+  ⚠ eslint-plugin-unicorn(no-for-loop): Use a `for-of` loop instead of this classical `for` loop.
+   ╭─[no_for_loop.tsx:2:9]
+ 1 │ const array = [];
+ 2 │         for (let i = 0; i < array.length; i++) {
+   ·         ───
+ 3 │                 let foo = array[i];
+   ╰────
+  help: Rewrite the loop as `for (const el of array) { … }`
+
+  ⚠ eslint-plugin-unicorn(no-for-loop): Use a `for-of` loop instead of this classical `for` loop.
+   ╭─[no_for_loop.tsx:2:9]
+ 1 │ const array = [];
+ 2 │         for (let i = 0; i < array.length; i++) {
+   ·         ───
+ 3 │                 const foo = array[i];
+   ╰────
+  help: Rewrite the loop as `for (const el of array) { … }`
+
+  ⚠ eslint-plugin-unicorn(no-for-loop): Use a `for-of` loop instead of this classical `for` loop.
+   ╭─[no_for_loop.tsx:2:9]
+ 1 │ const array = [];
+ 2 │         for (let i = 0; i < array.length; i++) {
+   ·         ───
+ 3 │                 var foo = array[i], bar = 1;
+   ╰────
+  help: Rewrite the loop as `for (const el of array) { … }`


Reviews:
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T11:02:24Z
user: login: camc314
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T11:03:37Z
user: login: camc314
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T11:03:48Z

Review comments:
diff_hunk: @@ -0,0 +1,425 @@
+use oxc_ast::{AstKind, ast::*};
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_macros::declare_oxc_lint;
+use oxc_semantic::Reference;
+use oxc_span::{GetSpan, Span};
+
+use crate::{AstNode, ast_util::get_declaration_of_variable, context::LintContext, rule::Rule};
+
+fn no_for_loop_diagnostic(span: Span) -> OxcDiagnostic {
+    OxcDiagnostic::warn("Use a `for-of` loop instead of this classical `for` loop.")
+        .with_help("Rewrite the loop as `for (const el of array) { … }`")
+        .with_label(span)
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct NoForLoop;
+
+declare_oxc_lint!(
+    /// ### What it does
+    ///
+    /// Enforces the use of `for-of` loops over classical counter based `for` loops.
+    ///
+    /// ### Why is this bad?
+    ///
path: crates/oxc_linter/src/rules/unicorn/no_for_loop.rs
user: login: graphite-app[bot]
body: The diagnostic message still contains placeholder text. Please update it with a meaningful warning like `OxcDiagnostic::warn("For loops should be replaced with for-of loops")` and a helpful message such as `.with_help("Use a for-of loop for better readability and to avoid off-by-one errors")`. This will provide clear guidance to users encountering this lint.
```suggestion

fn no_for_loop_diagnostic(span: Span) -> OxcDiagnostic {
    // See <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> for details
    OxcDiagnostic::warn("For loops should be replaced with for-of loops")
        .with_help("Use a for-of loop for better readability and to avoid off-by-one errors")
        .with_label(span)
}

```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=oxc-project&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-19T11:02:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,425 @@
+use oxc_ast::{AstKind, ast::*};
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_macros::declare_oxc_lint;
+use oxc_semantic::Reference;
+use oxc_span::{GetSpan, Span};
+
+use crate::{AstNode, ast_util::get_declaration_of_variable, context::LintContext, rule::Rule};
+
+fn no_for_loop_diagnostic(span: Span) -> OxcDiagnostic {
+    OxcDiagnostic::warn("Use a `for-of` loop instead of this classical `for` loop.")
+        .with_help("Rewrite the loop as `for (const el of array) { … }`")
+        .with_label(span)
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct NoForLoop;
+
+declare_oxc_lint!(
+    /// ### What it does
+    ///
+    /// Enforces the use of `for-of` loops over classical counter based `for` loops.
+    ///
+    /// ### Why is this bad?
+    ///
+    /// * A `for-of` loop makes intent explicit – _iterate over all elements_.
+    /// * It removes boiler-plate (`i`, `arr.length`, manual `++`).
+    ///
+    /// ### Examples
+    ///
+    /// Examples of **incorrect** code for this rule:
+    /// ```js
+    /// for (let i = 0; i < items.length; i++) {
+    ///   const item = items[i];
+    ///   console.log(item);
+    /// }
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```js
+    /// for (const item of items) {
+    ///   console.log(item);
+    /// }
+    /// ```
+    NoForLoop,
+    unicorn,
+    style,
+    pending
+);
+
+impl Rule for NoForLoop {
+    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+        let AstKind::ForStatement(for_stmt) = node.kind() else { return };
+
+        let Some(init) = &for_stmt.init else { return };
+        let ForStatementInit::VariableDeclaration(init) = init else { return };
+        if init.declarations.len() != 1
+            || init.declarations[0]
+                .init
+                .as_ref()
+                .is_some_and(|e| !e.get_inner_expression().is_number_0())
+        {
+            return;
+        }
+        let BindingPatternKind::BindingIdentifier(ident) = &init.declarations[0].id.kind else {
+            return;
+        };
+
+        let Some(test) = &for_stmt.test else { return };
+        let Expression::BinaryExpression(bin_expr) = test.get_inner_expression() else { return };
+        let array = {
+            let (lesser, greater) = match bin_expr.operator {
+                BinaryOperator::LessThan => (&bin_expr.left, &bin_expr.right),
+                BinaryOperator::GreaterThan => (&bin_expr.right, &bin_expr.left),
+                _ => return,
+            };
+
+            if lesser.get_identifier_reference().is_none_or(|lesser_ident| {
+                ctx.scoping().get_reference(lesser_ident.reference_id()).symbol_id()
+                    != Some(ident.symbol_id())
+            }) {
+                return;
+            }
+
+            let Some(greater) = greater.get_inner_expression().as_member_expression() else {
+                return;
+            };
+            if greater.static_property_name().is_none_or(|name| name != "length") {
+                return;
+            }
+            let Some(ident) = greater.object().get_identifier_reference() else {
+                return;
+            };
+            ident
+        };
+
+        let Some(update) = &for_stmt.update else { return };
+        match update {
+            Expression::UpdateExpression(u) => {
+                if u.operator != UpdateOperator::Increment {
+                    return;
+                }
+                let SimpleAssignmentTarget::AssignmentTargetIdentifier(update_ident) = &u.argument
+                else {
+                    return;
+                };
+
+                if ctx.scoping().get_reference(update_ident.reference_id()).symbol_id()
+                    != Some(ident.symbol_id())
+                {
+                    return;
+                }
+            }
+            Expression::AssignmentExpression(u) => {
+                let AssignmentTarget::AssignmentTargetIdentifier(update_ident) = &u.left else {
+                    return;
+                };
+                if ctx.scoping().get_reference(update_ident.reference_id()).symbol_id()
+                    != Some(ident.symbol_id())
+                {
+                    return;
+                }
+
+                match u.operator {
+                    AssignmentOperator::Addition
+                        if u.right.get_inner_expression().is_number_value(1.0) => {}
+                    AssignmentOperator::Assign => {
+                        let Expression::BinaryExpression(right) = &u.right else { return };
+                        if right.operator != BinaryOperator::Addition {
+                            return;
+                        }
+                        if (!(right.left.get_inner_expression().is_number_value(1.0))
+                            && (right
+                                .right
+                                .get_inner_expression()
+                                .get_identifier_reference()
+                                .is_some_and(|i| {
+                                    ctx.scoping().get_reference(i.reference_id()).symbol_id()
+                                        == Some(ident.symbol_id())
+                                })))
+                            && !((right.right.get_inner_expression().is_number_value(1.0))
+                                && (right
+                                    .left
+                                    .get_inner_expression()
+                                    .get_identifier_reference()
+                                    .is_some_and(|i| {
+                                        ctx.scoping().get_reference(i.reference_id()).symbol_id()
+                                            == Some(ident.symbol_id())
+                                    })))
+                        {
path: crates/oxc_linter/src/rules/unicorn/no_for_loop.rs
user: login: graphite-app[bot]
body: The `dbg!(node)` statement appears to be a debugging artifact that should be removed before this PR is merged. Debug statements can impact performance and may expose internal implementation details in logs.

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=oxc-project&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-19T11:02:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,425 @@
+use oxc_ast::{AstKind, ast::*};
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_macros::declare_oxc_lint;
+use oxc_semantic::Reference;
+use oxc_span::{GetSpan, Span};
+
+use crate::{AstNode, ast_util::get_declaration_of_variable, context::LintContext, rule::Rule};
+
+fn no_for_loop_diagnostic(span: Span) -> OxcDiagnostic {
+    OxcDiagnostic::warn("Use a `for-of` loop instead of this classical `for` loop.")
+        .with_help("Rewrite the loop as `for (const el of array) { … }`")
+        .with_label(span)
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct NoForLoop;
+
+declare_oxc_lint!(
+    /// ### What it does
+    ///
+    /// Enforces the use of `for-of` loops over classical counter based `for` loops.
+    ///
+    /// ### Why is this bad?
+    ///
+    /// * A `for-of` loop makes intent explicit – _iterate over all elements_.
+    /// * It removes boiler-plate (`i`, `arr.length`, manual `++`).
+    ///
+    /// ### Examples
+    ///
+    /// Examples of **incorrect** code for this rule:
+    /// ```js
+    /// for (let i = 0; i < items.length; i++) {
+    ///   const item = items[i];
+    ///   console.log(item);
+    /// }
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```js
+    /// for (const item of items) {
+    ///   console.log(item);
+    /// }
+    /// ```
+    NoForLoop,
+    unicorn,
+    style,
+    pending
+);
+
+impl Rule for NoForLoop {
+    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+        let AstKind::ForStatement(for_stmt) = node.kind() else { return };
+
+        let Some(init) = &for_stmt.init else { return };
+        let ForStatementInit::VariableDeclaration(init) = init else { return };
+        if init.declarations.len() != 1
+            || init.declarations[0]
+                .init
+                .as_ref()
+                .is_some_and(|e| !e.get_inner_expression().is_number_0())
+        {
+            return;
+        }
+        let BindingPatternKind::BindingIdentifier(ident) = &init.declarations[0].id.kind else {
+            return;
+        };
+
+        let Some(test) = &for_stmt.test else { return };
+        let Expression::BinaryExpression(bin_expr) = test.get_inner_expression() else { return };
+        let array = {
+            let (lesser, greater) = match bin_expr.operator {
+                BinaryOperator::LessThan => (&bin_expr.left, &bin_expr.right),
+                BinaryOperator::GreaterThan => (&bin_expr.right, &bin_expr.left),
+                _ => return,
+            };
+
+            if lesser.get_identifier_reference().is_none_or(|lesser_ident| {
+                ctx.scoping().get_reference(lesser_ident.reference_id()).symbol_id()
+                    != Some(ident.symbol_id())
+            }) {
+                return;
+            }
+
+            let Some(greater) = greater.get_inner_expression().as_member_expression() else {
+                return;
+            };
+            if greater.static_property_name().is_none_or(|name| name != "length") {
+                return;
+            }
+            let Some(ident) = greater.object().get_identifier_reference() else {
+                return;
+            };
+            ident
+        };
+
+        let Some(update) = &for_stmt.update else { return };
+        match update {
+            Expression::UpdateExpression(u) => {
+                if u.operator != UpdateOperator::Increment {
+                    return;
+                }
+                let SimpleAssignmentTarget::AssignmentTargetIdentifier(update_ident) = &u.argument
+                else {
+                    return;
+                };
+
+                if ctx.scoping().get_reference(update_ident.reference_id()).symbol_id()
+                    != Some(ident.symbol_id())
+                {
+                    return;
+                }
+            }
+            Expression::AssignmentExpression(u) => {
+                let AssignmentTarget::AssignmentTargetIdentifier(update_ident) = &u.left else {
+                    return;
+                };
+                if ctx.scoping().get_reference(update_ident.reference_id()).symbol_id()
+                    != Some(ident.symbol_id())
+                {
+                    return;
path: crates/oxc_linter/src/rules/unicorn/no_for_loop.rs
user: login: graphite-app[bot]
body: The `Expression::AssignmentExpression(u)` branch contains an empty implementation block that doesn't perform any validation. This creates an inconsistency with the `UpdateExpression` branch which has proper validation logic. Either implement the necessary validation for assignment expressions (checking the target and operator) or return early from this case if assignment expressions aren't intended to be supported in this context.
```suggestion
            Expression::AssignmentExpression(assign_expr) => {
                // Check if the assignment target is the same as the loop variable
                if let AssignmentTarget::SimpleAssignmentTarget(
                    SimpleAssignmentTarget::AssignmentTargetIdentifier(ident),
                ) = &assign_expr.left
                {
                    if ident.name.as_str() == variable_name
                        && assign_expr.operator == AssignmentOperator::Assignment
                    {
                        return false;
                    }
                }
            }
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=oxc-project&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-05-19T11:02:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,425 @@
+use oxc_ast::{AstKind, ast::*};
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_macros::declare_oxc_lint;
+use oxc_semantic::Reference;
+use oxc_span::{GetSpan, Span};
+
+use crate::{AstNode, ast_util::get_declaration_of_variable, context::LintContext, rule::Rule};
+
+fn no_for_loop_diagnostic(span: Span) -> OxcDiagnostic {
+    OxcDiagnostic::warn("Use a `for-of` loop instead of this classical `for` loop.")
+        .with_help("Rewrite the loop as `for (const el of array) { … }`")
+        .with_label(span)
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct NoForLoop;
+
+declare_oxc_lint!(
+    /// ### What it does
+    ///
+    /// Enforces the use of `for-of` loops over classical counter based `for` loops.
+    ///
+    /// ### Why is this bad?
+    ///
path: crates/oxc_linter/src/rules/unicorn/no_for_loop.rs
user: login: camc314
body: outdated > https://github.com/oxc-project/oxc/blob/60f0f0f241532df7d6d2b6255951d93b2eac0e08/crates/oxc_linter/src/rules/unicorn/no_for_loop.rs#L19-L28
created_at: 2025-05-19T11:03:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,425 @@
+use oxc_ast::{AstKind, ast::*};
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_macros::declare_oxc_lint;
+use oxc_semantic::Reference;
+use oxc_span::{GetSpan, Span};
+
+use crate::{AstNode, ast_util::get_declaration_of_variable, context::LintContext, rule::Rule};
+
+fn no_for_loop_diagnostic(span: Span) -> OxcDiagnostic {
+    OxcDiagnostic::warn("Use a `for-of` loop instead of this classical `for` loop.")
+        .with_help("Rewrite the loop as `for (const el of array) { … }`")
+        .with_label(span)
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct NoForLoop;
+
+declare_oxc_lint!(
+    /// ### What it does
+    ///
+    /// Enforces the use of `for-of` loops over classical counter based `for` loops.
+    ///
+    /// ### Why is this bad?
+    ///
+    /// * A `for-of` loop makes intent explicit – _iterate over all elements_.
+    /// * It removes boiler-plate (`i`, `arr.length`, manual `++`).
+    ///
+    /// ### Examples
+    ///
+    /// Examples of **incorrect** code for this rule:
+    /// ```js
+    /// for (let i = 0; i < items.length; i++) {
+    ///   const item = items[i];
+    ///   console.log(item);
+    /// }
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```js
+    /// for (const item of items) {
+    ///   console.log(item);
+    /// }
+    /// ```
+    NoForLoop,
+    unicorn,
+    style,
+    pending
+);
+
+impl Rule for NoForLoop {
+    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+        let AstKind::ForStatement(for_stmt) = node.kind() else { return };
+
+        let Some(init) = &for_stmt.init else { return };
+        let ForStatementInit::VariableDeclaration(init) = init else { return };
+        if init.declarations.len() != 1
+            || init.declarations[0]
+                .init
+                .as_ref()
+                .is_some_and(|e| !e.get_inner_expression().is_number_0())
+        {
+            return;
+        }
+        let BindingPatternKind::BindingIdentifier(ident) = &init.declarations[0].id.kind else {
+            return;
+        };
+
+        let Some(test) = &for_stmt.test else { return };
+        let Expression::BinaryExpression(bin_expr) = test.get_inner_expression() else { return };
+        let array = {
+            let (lesser, greater) = match bin_expr.operator {
+                BinaryOperator::LessThan => (&bin_expr.left, &bin_expr.right),
+                BinaryOperator::GreaterThan => (&bin_expr.right, &bin_expr.left),
+                _ => return,
+            };
+
+            if lesser.get_identifier_reference().is_none_or(|lesser_ident| {
+                ctx.scoping().get_reference(lesser_ident.reference_id()).symbol_id()
+                    != Some(ident.symbol_id())
+            }) {
+                return;
+            }
+
+            let Some(greater) = greater.get_inner_expression().as_member_expression() else {
+                return;
+            };
+            if greater.static_property_name().is_none_or(|name| name != "length") {
+                return;
+            }
+            let Some(ident) = greater.object().get_identifier_reference() else {
+                return;
+            };
+            ident
+        };
+
+        let Some(update) = &for_stmt.update else { return };
+        match update {
+            Expression::UpdateExpression(u) => {
+                if u.operator != UpdateOperator::Increment {
+                    return;
+                }
+                let SimpleAssignmentTarget::AssignmentTargetIdentifier(update_ident) = &u.argument
+                else {
+                    return;
+                };
+
+                if ctx.scoping().get_reference(update_ident.reference_id()).symbol_id()
+                    != Some(ident.symbol_id())
+                {
+                    return;
+                }
+            }
+            Expression::AssignmentExpression(u) => {
+                let AssignmentTarget::AssignmentTargetIdentifier(update_ident) = &u.left else {
+                    return;
+                };
+                if ctx.scoping().get_reference(update_ident.reference_id()).symbol_id()
+                    != Some(ident.symbol_id())
+                {
+                    return;
+                }
+
+                match u.operator {
+                    AssignmentOperator::Addition
+                        if u.right.get_inner_expression().is_number_value(1.0) => {}
+                    AssignmentOperator::Assign => {
+                        let Expression::BinaryExpression(right) = &u.right else { return };
+                        if right.operator != BinaryOperator::Addition {
+                            return;
+                        }
+                        if (!(right.left.get_inner_expression().is_number_value(1.0))
+                            && (right
+                                .right
+                                .get_inner_expression()
+                                .get_identifier_reference()
+                                .is_some_and(|i| {
+                                    ctx.scoping().get_reference(i.reference_id()).symbol_id()
+                                        == Some(ident.symbol_id())
+                                })))
+                            && !((right.right.get_inner_expression().is_number_value(1.0))
+                                && (right
+                                    .left
+                                    .get_inner_expression()
+                                    .get_identifier_reference()
+                                    .is_some_and(|i| {
+                                        ctx.scoping().get_reference(i.reference_id()).symbol_id()
+                                            == Some(ident.symbol_id())
+                                    })))
+                        {
path: crates/oxc_linter/src/rules/unicorn/no_for_loop.rs
user: login: camc314
body: outdated
created_at: 2025-05-19T11:03:48Z
author_association: CONTRIBUTOR

PR comments:
user: login: camc314
created_at: 2025-05-19T11:00:08Z
body: * **#11154** <a href="https://app.graphite.dev/github/pr/oxc-project/oxc/11154?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈 <a href="https://app.graphite.dev/github/pr/oxc-project/oxc/11154?utm_source=stack-comment-view-in-graphite" target="_blank">(View in Graphite)</a>
* `main`
<hr />

## How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:
- <kbd>0-merge</kbd> - adds this PR to the back of the merge queue
- <kbd>hotfix</kbd> - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using [this link](https://app.graphite.dev/invite/github/oxc-project?ref=merge-queue-instructions-in-stack-comment&prId=11154).

<h2></h2>
<sub>

An organization admin has enabled the [Graphite Merge Queue](https://graphite.dev/docs/graphite-merge-queue) in this repository.

</sub>

<sub>

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

</sub>

<h2></h2>

This stack of pull requests is managed by <a href="https://graphite.dev?utm-source=stack-comment"><b>Graphite</b></a>. Learn more about <a href="https://stacking.dev/?utm_source=stack-comment">stacking</a>.
<!-- Current dependencies on/for this PR: -->
author_association: CONTRIBUTOR
user: login: camc314
created_at: 2025-05-19T11:01:54Z
body: ### Merge activity

* **May 19, 7:01 AM EDT**: The merge label '0-merge' was detected. This PR will be added to the [Graphite merge queue](https://app.graphite.dev/merges?org=oxc-project&repo=oxc) once it meets the requirements.
* **May 19, 7:06 AM EDT**: `camc314` added this pull request to the [Graphite merge queue](https://app.graphite.dev/merges?org=oxc-project&repo=oxc).
* **May 19, 7:10 AM EDT**: Merged by the [Graphite merge queue](https://app.graphite.dev/merges?org=oxc-project&repo=oxc).

author_association: CONTRIBUTOR
user: login: codspeed-hq[bot]
created_at: 2025-05-19T11:06:28Z
body: <!-- __CODSPEED_PERFORMANCE_REPORT_COMMENT__ -->
<!-- __CODSPEED_INSTRUMENTATION_PERFORMANCE_REPORT_COMMENT__ -->

## [CodSpeed Instrumentation Performance Report](https://codspeed.io/oxc-project/oxc/branches/c%2F05-16-feat_linter_unicorn_no-for-loop)

### Merging #11154 will **improve performances by 14.02%**

<sub>Comparing <code>c/05-16-feat_linter_unicorn_no-for-loop</code> (385b84d) with <code>main</code> (83e4f9b)</sub>



### Summary

`⚡ 1` improvements  
`✅ 37` untouched benchmarks  



### Benchmarks breakdown

|     | Benchmark | `BASE` | `HEAD` | Change |
| --- | --------- | ----------------------- | ------------------- | ------ |
| ⚡ | `` mangler[cal.com.tsx] `` | 3.5 ms | 3 ms | +14.02% |

author_association: NONE