Skip to content

fix(coverage): cover every line of a multi-line statement - #1339

Merged
Chemaclass merged 3 commits into
mainfrom
fix/1338-multi-line-array-assignments-reported-uncovered
Sep 6, 2026
Merged

fix(coverage): cover every line of a multi-line statement#1339
Chemaclass merged 3 commits into
mainfrom
fix/1338-multi-line-array-assignments-reported-uncovered

Conversation

@Chemaclass

@Chemaclass Chemaclass commented Sep 5, 2026

Copy link
Copy Markdown
Member

🤔 Background

Related #1338

Bash reports one executed statement to the DEBUG trap on a single line, even when it is written over several. #722 handled the backslash chain; every other way a statement spans lines was left out, so an array literal written one element per line cost one uncovered line per element. On Bash 3.2 the assignment is reported on its closing ), which is non-executable, so the hit was discarded and the whole array read as uncovered.

💡 Changes

  • Group source lines into statement spans and give every line of a span the highest count recorded in it, so a statement that ran is covered wherever the trap attributed it. Array literals, multi-line strings and heredoc bodies are now covered; the #722 backslash chain is unchanged.
  • A multi-line $( ) is deliberately left alone: its interior lines are commands tracked in their own right, and crediting them would report lines that never ran.
  • One scanner, a Bash reference mirrored in awk, replacing five copies of the old propagation loop. The differential now compares the scanner too over every repo shell file, and asserts each one lexes to a clean end state.
  • Coverage rises where a multi-line statement really did run, and the report phase is 20-30% slower (3-5% of a whole --coverage run) — the cost of a per-line lexer. Verified on Bash 3.0, 3.2 and 5.3.

Bash reports one executed statement to the DEBUG trap on a single line,
even when the statement is written over several. #722 handled the
backslash chain; every other way a statement spans lines was left out.

An array literal written one element per line therefore cost one
uncovered line per element -- and on Bash 3.2, which attributes the
assignment to its closing `)`, that line is non-executable, so the hit
was discarded and the whole array read as uncovered. Multi-line strings
and heredoc bodies had the same gap.

Adds a small shell lexer, a Bash reference in coverage/lines.sh mirrored
in awk, that groups lines into statement spans: a stack of open contexts
(single quote, double quote, array literal, command substitution, plain
paren) plus a pending heredoc delimiter. Propagation becomes span-max
rather than a forward carry, so it works whichever end of the span the
trap attributed the statement to. For a backslash chain it produces
exactly what the forward carry did.

A multi-line `$( )` is deliberately not a span: its interior lines are
commands tracked in their own right, and crediting them from the line
that opened the substitution would report lines that never ran.

`bu_propagate` replaces five copies of the same loop across the LCOV,
stats and HTML passes. The differential now compares the scanner as well
as the classifier over every `git ls-files '*.sh'`, and asserts each file
lexes to a clean end state -- real shell files balance their quotes, so a
leftover context is the lexer misreading real code.

Related #1338

Claude-Session: https://claude.ai/code/session_01MeysZ63ewZiFTgCDs172XJ
@Chemaclass Chemaclass added the bug Something isn't working label Sep 5, 2026
@Chemaclass Chemaclass self-assigned this Sep 5, 2026
No behaviour change; the differential and the span tests pin that.

The escape rule was written twice, once in the double-quoted branch and
once in the unquoted one. A backslash escapes the next character in both,
and it can never reach either from inside `'..'` -- that context reports
nothing but its closing quote, and its branch returns first. So the rule
is hoisted between the two, stated once.

The stack now lives in a local for the length of the walk and is written
back once at the end. Eight statements were longer than the work they
did because the global's name is 30 characters. The comment case leaves
by `break` rather than `return` so there is a single write-back point.

Claude-Session: https://claude.ai/code/session_01MeysZ63ewZiFTgCDs172XJ
editorconfig-checker 4.0.0 reads the ANSI ESC byte in four recorded
snapshots as Latin-1 and fails them against the global `charset = utf-8`.
All four are pure ASCII -- `file` says so, and no byte in them is >= 0x80.
The same job passed on main in August, so the checker upgraded under it;
`make lint` has been red on main since, for every branch.

Snapshots are byte-exact recordings of terminal output, not hand-written
source, so there is no encoding to pin in the first place.

Claude-Session: https://claude.ai/code/session_01MeysZ63ewZiFTgCDs172XJ
@Chemaclass
Chemaclass merged commit 3ba02b6 into main Sep 6, 2026
37 checks passed
@Chemaclass
Chemaclass deleted the fix/1338-multi-line-array-assignments-reported-uncovered branch September 6, 2026 10:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants