Skip to content

fix(linter): SC2242 counts case/loop nesting instead of flagging it (closes #331) - #332

Open
noahgift wants to merge 1 commit into
mainfrom
fix/sc2242-nesting-depth
Open

fix(linter): SC2242 counts case/loop nesting instead of flagging it (closes #331)#332
noahgift wants to merge 1 commit into
mainfrom
fix/sc2242-nesting-depth

Conversation

@noahgift

Copy link
Copy Markdown
Contributor

Two defects in rash/src/linter/rules/sc2242.rs, one shape — a detector reporting a result it did not measure.

Reproducer (14 lines; bash -n accepts it, and it runs)

#!/usr/bin/env bash
set -euo pipefail
while IFS= read -r id; do
  case "$id" in '') continue ;; esac
  for a in 1 2; do
    for b in 3 4; do
      printf '%s%s\n' "$a" "$b"
    done
  done
  if [ -z "$id" ]; then
    continue                    # <- reported as "outside a loop"
  fi
  printf '%s\n' "$id"
done < <(printf 'x\n')
✗ 11:1-13 [error] SC2242: Can only break/continue from loops. Use 'exit' to exit case or function

Root cause

check() tracks context with in_case / in_loop / in_function as booleans, not depth counters:

  1. An inner done closes the outer loop. for/for set in_loop = true (already true), then each
    of the two done lines sets in_loop = false — so after the nested loops close, the still-open
    while is invisible.
  2. A one-line case … esac never closes. is_case_end is line.trim_start() == "esac", which is
    false for a case … ;; esac written on one line, so in_case stays true for the rest of the file.

Either alone is survivable; together they make in_case && !in_loop true inside a loop, and every later
break/continue is flagged. Found while linting a done_when script in paiml/infra (PMAT-534): the
rule reported 2 errors on a script bash -n accepts and that runs correctly.

Fix

fix/sc2242-nesting-depth: keyword-TOKEN depth counters (;, (, ), &, | and backtick are
separators, so a break ;; esac clause yields break and esac as their own tokens); openers count
before the check and closers after, so a one-line case whose clause breaks is still judged inside its own
case while a one-line for … done balances. Still a line/keyword heuristic, not a parser — a keyword
inside a quoted string or a heredoc is still miscounted, and the code says so — but it is no longer wrong
for code anyone writes.

Six tests added, including the reproducer above, donefile/casework (a word containing a keyword is
not the keyword) and a case-in-loop-in-case nesting. Discrimination proven: reverting check() to
the boolean behaviour turns exactly the two new nesting tests RED
(a_continue_after_nested_loops_close_is_not_in_the_case, a_one_line_case_closes_on_its_own_line) and
leaves the other 14 green. The rule still fires on the real defect: a break in a case outside any loop
is still one error, whether the case is written on one line or three.


Verification

  • cargo test -p bashrs --lib linter::6067 passed, 0 failed
  • cargo clippy -p bashrs --lib -- -D warnings — clean
  • cargo fmt --check — clean
  • The built binary (not the claim about it) gives 0 errors on the script that started this, and the 14-line reproducer lints clean.
  • Discrimination proven: reverting check() to the boolean behaviour turns exactly the two new nesting tests red — a_continue_after_nested_loops_close_is_not_in_the_case and a_one_line_case_closes_on_its_own_line — and leaves the other 14 green.

Found while landing paiml/infra PMAT-534, where these were 2 of the 6 bashrs lint errors on a done_when script. The other 4 were real findings and were fixed there.

🤖 Generated with Claude Code

…loses #331)

`bashrs lint` reported SC2242 on two `continue`s that are plainly inside a
`while` loop, in a script `bash -n` accepts and that runs. Two defects, one
shape — a detector reporting a result it did not measure:

1. `in_case`/`in_loop`/`in_function` were BOOLEANS, not depth counters, so an
   inner `done` cleared `in_loop` while the outer `while` was still open.
2. `is_case_end` demanded the line BE `esac`, so a one-line `case … esac`
   never cleared `in_case` — it stayed true for the rest of the file.

Either alone is survivable. Together they make `in_case && !in_loop` true
INSIDE a loop, and every later break/continue is flagged:

    while IFS= read -r id; do
      case "$id" in '') continue ;; esac
      for a in 1 2; do
        for b in 3 4; do printf '%s%s\n' "$a" "$b"; done
      done
      if [ -z "$id" ]; then
        continue                    # <- reported as "outside a loop"
      fi
    done < <(printf 'x\n')

Now: keyword-TOKEN depth counters (`;`, `(`, `)`, `&`, `|`, backtick are
separators, so a `break ;; esac` clause yields `break` and `esac` as tokens of
their own). Openers count BEFORE the check and closers AFTER, so a one-line
case whose clause breaks is still judged inside its own case while a one-line
`for … done` balances. Still a line/keyword heuristic and the code says so —
a keyword inside a quoted string or a heredoc is still miscounted — but it is
no longer wrong for code anyone writes.

Six tests, including the reproducer above, `donefile`/`casework` (a word
containing a keyword is not the keyword) and case-in-loop-in-case. PROVEN TO
DISCRIMINATE: reverting `check()` to the boolean behaviour turns exactly the
two new nesting tests red and leaves the other 14 green. The rule still fires
on the real defect — a `break` in a case outside any loop is one error,
whether the case is written on one line or three.

Found while landing paiml/infra PMAT-534, where it was 2 of the 6 errors on a
done_when script; the other 4 were real and were fixed there.

  cargo test -p bashrs --lib linter::  6067 passed, 0 failed
  cargo clippy -p bashrs --lib -- -D warnings  clean
  the fixed binary: 0 errors on the script that started this

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant