Skip to content

JS-1491 Fix S2392 false positive for loop counter variables reused across loops#6646

Draft
sonar-nigel[bot] wants to merge 5 commits intomasterfrom
fix/JS-1491-fix-fp-on-s2392-loop-counter-variables-reused-across-separate-loops-sonnet
Draft

JS-1491 Fix S2392 false positive for loop counter variables reused across loops#6646
sonar-nigel[bot] wants to merge 5 commits intomasterfrom
fix/JS-1491-fix-fp-on-s2392-loop-counter-variables-reused-across-separate-loops-sonnet

Conversation

@sonar-nigel
Copy link
Contributor

@sonar-nigel sonar-nigel bot commented Mar 19, 2026

Fix a false positive in S2392 where var loop counter variables reused across multiple sequential for-loops were incorrectly flagged.

Problem

S2392 incorrectly reported the first for (var i ...) declaration when the same counter variable was reused across multiple sequential for-loops. Because var is hoisted to function scope, ESLint treats all declarations as the same variable, making references in subsequent loops appear to be "outside" the first loop's binding range.

Changes

  • Core fix: Collect AST ranges of other for-loops that declare the same variable. If every outside reference falls within one of those sibling loop ranges, the reuse is intentional and the issue is suppressed. References genuinely outside all loops (e.g., console.log(i) after the last loop) are still reported.
  • Refinement: Restrict the FP suppression to variables declared directly in a for-loop header (init/left position), preventing over-suppression for variables declared inside a loop body.
  • Tests: Added test cases covering sequential for-loop counter reuse (for-in then for, three consecutive loops sharing i), and confirming that genuine violations are still caught.
  • Rspec update: Updated the noncompliant example to show a real violation and added a compliant example for the sequential counter reuse pattern.
  • Ruling files: Synced expected ruling files after the refinement.

Fixes JS-1491

Proposed rspec changes
diff --git a/rules/S2392/javascript/rule.adoc b/rules/S2392/javascript/rule.adoc
index 03bfb88..a26ca58 100644
--- a/rules/S2392/javascript/rule.adoc
+++ b/rules/S2392/javascript/rule.adoc
@@ -15,11 +15,9 @@ function doSomething(a, b) {
     console.log(x);
   }
 
-  for (var i = 0; i < m; i++) { // Noncompliant: both loops use same variable
-  }
-
-  for (var i = 0; i < n; i++) {
+  for (var i = 0; i < m; i++) { // Noncompliant: 'i' is referenced after the loop
   }
+  console.log(i);
 
   return a + b;
 }
@@ -27,13 +25,36 @@ function doSomething(a, b) {
 
 When `var` declaration is used outside of a block, the declaration should be done at the uppermost level where it is used. When possible, use `let` or `const`, which allow for block-scoped variables.
 
+Reusing a `var` counter across sequential for-loops is also compliant. Since `var` is hoisted to function scope, each loop independently re-declares the same variable, and references within each loop's body are contained within that loop's own scope range.
+
+[source,javascript,diff-id=2,diff-type=noncompliant]
+----
+function processItems(items, extras) {
+  for (var n in items)
+    process(n);
+  for (var n = extras.length - 1; n >= 0; n--)
+    extras[n].remove();
+  console.log(n); // Noncompliant: 'n' is referenced after all loops
+}
+----
+
+[source,javascript,diff-id=2,diff-type=compliant]
+----
+function processItems(items, extras) {
+  for (var n in items) // Compliant: 'n' is reused across sequential for-loops
+    process(n);
+  for (var n = extras.length - 1; n >= 0; n--)
+    extras[n].remove();
+}
+----
+
 [source,javascript,diff-id=1,diff-type=compliant]
 ----
 function doSomething(a, b) {
   var x;
 
   if (a > b) {
-    x = a - b; 
+    x = a - b;
   }
 
   if (a > 4) {
@@ -46,7 +67,6 @@ function doSomething(a, b) {
   for (let i = 0; i < n; i++) {
   }
 
-
   return a + b;
 }
 ----

Vibe Bot added 4 commits March 19, 2026 15:16
Tests cover the scenario where the same `var` loop counter variable
is reused across multiple sequential for-loops (e.g., for-in then
for, or three consecutive for-loops sharing `i`). Because `var` is
hoisted to function scope, each loop independently re-declares the
variable; the rule incorrectly flags the first declaration as having
outside-scope references that are actually covered by sibling loops.

The tests verify that sequential for-loop counter reuse is treated as
compliant, while references to a loop counter genuinely used after all
loops are still flagged.

Relates to JS-1491
S2392 incorrectly flagged the first `for (var i ...)` declaration when the same
counter variable was reused across multiple sequential for-loops. Because `var` is
hoisted to function scope, ESLint treats all declarations as the same variable, making
the second loop's references appear "outside" the first loop's binding range.

The fix collects AST ranges of other for-loops that also declare the same variable.
If every outside reference falls within one of those other-loop ranges, the reuse is
intentional and the issue is suppressed. If any reference falls outside all other
loops (e.g., `console.log(i)` after the last loop), the issue is still reported.

The rspec was also updated: the noncompliant example now shows a genuine violation
(single loop with reference after it), and a new compliant example demonstrates the
sequential for-loop counter reuse pattern that is now accepted.

Relates to JS-1491
Only apply the sequential-loop FP suppression when the current `var`
declaration is directly in a for-loop header (init/left position). This
prevents over-suppression when a variable is declared as a regular
statement inside a for-loop body or other block. Ruling analysis
confirmed that nested redeclarations (e.g., `var rule` re-declared
inside a nested for-loop body when `rule` is already the outer for-in
iterator) are legitimate issues and should continue to be reported.

No implementation changes were required in this pass; the ruling entry
for `ace:src/mode/text.js:323` was correctly identified as a true
positive (shouldRaise=true), resolving the only mismatch from the
previous attempt.
@github-actions
Copy link
Contributor

github-actions bot commented Mar 19, 2026

Ruling Report

Code no longer flagged (110 issues)

S2392

TypeScript/lib/tsc.js:933

   931 |         if (!left || !right)
   932 |             return false;
>  933 |         for (var key in left)
   934 |             if (hasOwnProperty.call(left, key)) {
   935 |                 if (!hasOwnProperty.call(right, key) === undefined)

TypeScript/lib/tsc.js:975

   973 |     function extend(first, second) {
   974 |         var result = {};
>  975 |         for (var id in second)
   976 |             if (hasOwnProperty.call(second, id)) {
   977 |                 result[id] = second[id];

TypeScript/lib/tsc.js:24906

  24904 |                     return undefined;
  24905 |                 }
> 24906 |                 for (var i = 1; i < signatureLists.length; i++) {
  24907 |                     if (!findMatchingSignature(signatureLists[i], signature, false, false, false)) {
  24908 |                         return undefined;

TypeScript/lib/tsc.js:25577

  25575 |                         typeArguments = [];
  25576 |                     }
> 25577 |                     for (var i = numTypeArguments; i < numTypeParameters; i++) {
  25578 |                         typeArguments[i] = isJavaScript ? anyType : emptyObjectType;
  25579 |                     }

TypeScript/lib/tsc.js:28845

  28843 |                     var sourceTypes = source.aliasTypeArguments;
  28844 |                     var targetTypes = target.aliasTypeArguments;
> 28845 |                     for (var i = 0; i < sourceTypes.length; i++) {
  28846 |                         inferFromTypes(sourceTypes[i], targetTypes[i]);
  28847 |                     }

TypeScript/lib/tsc.js:32139

  32137 |             var typeParameters = signature.typeParameters;
  32138 |             var inferenceMapper = getInferenceMapper(context);
> 32139 |             for (var i = 0; i < typeParameters.length; i++) {
  32140 |                 if (!context.inferences[i].isFixed) {
  32141 |                     context.inferredTypes[i] = undefined;

TypeScript/lib/tsc.js:32948

  32946 |             var len = signature.parameters.length - (signature.hasRestParameter ? 1 : 0);
  32947 |             if (checkMode === 2) {
> 32948 |                 for (var i = 0; i < len; i++) {
  32949 |                     var declaration = signature.parameters[i].valueDeclaration;
  32950 |                     if (declaration.type) {

TypeScript/lib/tsc.js:46133

  46131 |                 var clauseLabels = [];
  46132 |                 var defaultClauseIndex = -1;
> 46133 |                 for (var i = 0; i < numClauses; i++) {
  46134 |                     var clause = caseBlock.clauses[i];
  46135 |                     clauseLabels.push(defineLabel());

TypeScript/lib/tsc.js:46530

  46528 |             ts.Debug.assert(blocks !== undefined);
  46529 |             if (labelText) {
> 46530 |                 for (var i = blockStack.length - 1; i >= 0; i--) {
  46531 |                     var block = blockStack[i];
  46532 |                     if (supportsLabeledBreakOrContinue(block) && block.labelText === labelText) {

TypeScript/lib/tsc.js:46553

  46551 |             ts.Debug.assert(blocks !== undefined);
  46552 |             if (labelText) {
> 46553 |                 for (var i = blockStack.length - 1; i >= 0; i--) {
  46554 |                     var block = blockStack[i];
  46555 |                     if (supportsUnlabeledContinue(block) && hasImmediateContainingLabeledBlock(labelText, i - 1)) {

...and 100 more

📋 View full report

Code no longer flagged (110)

S2392

Refactor rule.ts to address code quality issues reported by SonarQube:

- S3776 (Cognitive Complexity): Extracted isForLoopNode, getOtherLoopRanges,
  and isCoveredByOtherLoops as module-level helper functions, reducing the
  VariableDeclaration handler's cognitive complexity from 26 to within the
  allowed limit of 15.

- S134 (Deep nesting): The deeply nested for/if loops inside the FP suppression
  logic (previously at nesting depth 4+) are now in getOtherLoopRanges at depth
  1-2, satisfying the max-3-level nesting requirement.

- S2966 (Forbidden non-null assertion): Replaced loopNode.range! with a
  loopRange const using optional chaining plus a truthy guard, and replaced
  ref.range![0]/[1] with a range const plus an explicit undefined check.
@sonarqube-next
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants