fix(lint): five false positives that made users disable bashrs lint (GH-217, GH-209) - #219
Merged
Conversation
…217) `bashrs lint` reported SC1007 (Severity::Error, so it BLOCKS commits) on Python embedded in a quoted heredoc: python3 - <<'PY' p = 1 q = 2 PY `bash -n` accepts that file. A quoted delimiter means the body is literal text — that is the entire point of quoting it — so no shell rule may analyse it. Confirmed against the published 6.66.2 binary: 2 errors on valid shell. Root cause is one level up from the symptom. The rules see a flat stream of physical lines with no notion of heredoc regions, so ANY line-oriented rule fires inside these bodies; SC1007 is just the one that happened to hit. #211 was the same shape (sc2188 iterating physical rather than logical lines). The machinery already existed and was not shared: sc2006 carried a private get_quoted_heredoc_lines (issue #96) and the other 384 rules did not. A per-rule fix cannot generalise, and the next line-oriented rule reintroduces the bug — so this is applied ONCE where diagnostics are aggregated, next to the existing inline-suppression filter. Every current rule and every future rule inherits it. sc2006's private copy is deleted in favour of the shared one (-34 lines, plus two now-dead regexes), so there is a single definition rather than two that can drift. Implemented as a state machine, not "regex-match every line" as sc2006 did: scanning all lines for openers also matches openers INSIDE a body (a heredoc documenting a heredoc), which silently extends the suppressed region past its end and swallows real diagnostics. Covered by a test. Unquoted heredocs are deliberately NOT skipped: their bodies undergo parameter expansion and command substitution, so they really are shell and SC2006 et al. should still fire there. 6 unit tests: the GH-217 repro, unquoted-is-not-a-region, `<<-"EOF"`, here-string (`<<<` has no body), opener-inside-a-body, and two heredocs on one line consuming bodies in order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…om CI (Refs #209) Reported against a ~70-line production Makefile whose recipes are correct. After applying every fix the linter suggested it still produced 5 unsatisfiable warnings, so the PR removed the `bashrs lint Makefile` step entirely. A rule that cannot be satisfied trains people to delete the linter. 1. --fail-on was ignored for the common invocation. Single-file lint routed to output_lint_results, which chose the exit code itself (has_warnings -> exit 1) and never saw opts.fail_on. --fail-on was honoured only under --ci, so `bashrs lint --fail-on error Makefile` still exited 1 on a warnings-only run. That function is REMOVED rather than taught about the flag, so no caller can reintroduce an exit decision that bypasses the threshold: printing and exiting are now separate and exit_for_fail_on is the single decider. Default stays Warning, so out-of-the-box exit codes are unchanged. 2. MAKE003 read `$$` as a variable. `$$` is Make's escape for a literal `$`. parse_variable_reference fell into its `$VAR` branch, matched zero alphanumerics (next char is `$`), and emitted a ONE-CHARACTER diagnostic whose autofix was `"$"` — which changes what the shell receives. Canonical trigger is the self-documenting-help idiom in most Makefiles, where `$$1`/`$$2` are awk FIELD references and quoting them breaks the awk program. 3. MAKE010 did not recognise compound error handlers. has_error_handling was a substring test for the literal "|| exit", so `curl … || { echo "✗ failed"; exit 1; }` was reported as unhandled — though it handles errors better than a bare `|| exit 1`, since the user learns what failed. The autofix appended a SECOND `|| exit 1` after a block that already exits. Now the `||` tail is inspected, so `exit`/`return` and die/fail/abort helpers all count. 4. MAKE016 suggested a fix that BREAKS THE BUILD. It flagged `$(VAR)` in prerequisites and offered to quote it. GNU Make does no quote removal in prerequisite lists: `sakila: "$(DIR)"/x.sql` asks for a file whose name literally starts with `"`, and the build then fails pointing at the prerequisite rather than at the linter that wrote it. The rule's own doc-comment carried the broken form as its GOOD example, so the premise was wrong from the first commit. There is no correct version of "quote this prerequisite" — recipes are shell, prerequisites are not — so it is retired to a total no-op. The ID stays registered so existing `--ignore MAKE016` configs keep parsing, and the reasoning lives where the next person looks. Tests: 45 passed, 0 failed across make003/make010/make016 plus the existing property tests. Each fix is paired with a test asserting the TRUE positive still fires, so none of these trades a false positive for a false negative. The MAKE003 test uses an UNQUOTED `$$1`: the awk-in-single-quotes form from the issue is already skipped by the pre-existing quote tracking, so a test using it would have passed with or without the fix. `|| { fail "x"; }` initially failed my own fix — `{` tokenises separately, so stripping the brace from the first word left an empty string. Caught by the test before it shipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 11, 2026
fix(oracle): point the extracted test module at the crate root, and test the workspace (GH-214)
#223
Merged
Merged
noahgift
added a commit
that referenced
this pull request
Aug 12, 2026
Ships the user-facing fixes accumulated since 6.66.2, none of which
reach anyone until this is published:
- five lint false positives that were driving users to disable
`bashrs lint` (#219, GH-217, GH-209)
- CLI stack overflow from an oversized clap frame (#216, #215)
- RUSTSEC-2026-0204, crossbeam-epoch 0.9.20 (#210)
plus internal repairs: kani harnesses compile under cfg(kani) again
(#221), bashrs-oracle's test module compiles and the workspace is
actually tested (#223), and a workflow template stopped being run as a
workflow (#222).
Cargo.lock regenerated in the same commit -- forjar's 1.12.4 release
tripped its lockfile-preflight by bumping Cargo.toml alone.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #217. Closes #209.
Both issues have the same ending: a user turned
bashrs lintoff. #217 blocked commits via forjar's pre-commit hook; #209's reporter dropped thebashrs lint Makefilestep from CI entirely after no combination of suggested fixes could make a correct Makefile exit 0.A rule that cannot be satisfied doesn't get fixed — it gets removed.
GH-217 — quoted heredoc bodies are not shell
bash -naccepts this. Published 6.66.2 reports 2 ×SC1007—Severity::Error, so it blocks commits.Root cause is one level above the symptom. The rules see a flat stream of physical lines with no notion of heredoc regions, so any line-oriented rule fires inside these bodies. SC1007 is just the one that got hit. (#211 was the same shape:
sc2188iterating physical instead of logical lines.)The machinery already existed and wasn't shared —
sc2006carried a privateget_quoted_heredoc_linesfrom issue #96; the other 384 rules had nothing. A per-rule fix can't generalise, so this is applied once, where diagnostics are aggregated. Every current and future rule inherits it. sc2006's copy is deleted, so there's one definition instead of two that can drift.Written as a state machine, not the regex-per-line approach sc2006 used: scanning every line for openers also matches openers inside a body (a heredoc documenting a heredoc), silently extending the suppressed region past its end. There's a test for exactly that.
Unquoted heredocs are deliberately still linted — their bodies undergo expansion, so they really are shell.
GH-209 — four Makefile defects
1.
--fail-onwas ignored for the common invocation. Single-file lint routed tooutput_lint_results, which picked the exit code itself (has_warnings → exit 1) and never sawopts.fail_on. The flag worked only under--ci. That function is removed rather than taught about the flag, so no caller can reintroduce an exit decision that bypasses the threshold. Default staysWarning; out-of-the-box exit codes are unchanged.2. MAKE003 read
$$as a variable.$$is Make's escape for a literal$. The parser fell into its$VARbranch, matched zero characters, and emitted a one-character diagnostic whose autofix was"$". The trigger is the self-documenting-help idiom in most Makefiles, where$$1/$$2are awk field references — quoting them breaks the awk program.3. MAKE010 missed compound error handlers.
has_error_handlingwas a substring test for the literal"|| exit", socurl … || { echo "✗ failed"; exit 1; }read as unhandled — though it's better than a bare|| exit 1. The autofix appended a second|| exit 1after a block that already exits.4. MAKE016's autofix broke the build. It flagged
$(VAR)in prerequisites and offered to quote it. GNU Make does no quote removal in prerequisite lists, sosakila: "$(DIR)"/x.sqlasks for a file whose name literally starts with"— and the build then fails pointing at the prerequisite, not at the linter that wrote it. The rule's own doc-comment carried the broken form as its ✅ GOOD example, so the premise was wrong from the first commit. There is no correct version of "quote this prerequisite" — recipes are shell, prerequisites are not — so it's retired to a total no-op, ID still registered so existing--ignore MAKE016configs keep parsing.Verification
The full suite is the one that mattered: a filter at the aggregation layer could have silently suppressed diagnostics hundreds of tests depend on. It didn't.
Every fix is paired with a test asserting the true positive still fires, so none of these trades a false positive for a false negative.
Two self-inflicted bugs caught before shipping, both worth noting:
|| { fail "x"; }failed my own MAKE010 fix —{tokenises separately, so stripping the brace from the first word left an empty string.$$1inside the awk single-quotes is already skipped by pre-existing quote tracking, so it would have passed with or without the fix. Moved outside quotes so it discriminates.🤖 Generated with Claude Code