fix: batch 2026-08-30 — Halstead, ABC, LOC, NPM/NPA, nargs, preproc - #1403
Merged
Merged
Conversation
`bca preproc` serialized `PreprocResults.files` and the three `HashSet<String>` fields of `PreprocFile` straight off hash order, so the document differed byte for byte between runs over an unchanged tree: eight runs over a five-file fixture gave eight distinct hashes. This was the last file destination in the #1244 / #1303 family still nondeterministic, and unlike those two it moves identically at `--jobs 1` — the variation is at the serialization seam, not in worker arrival order. Sort there rather than storing ordered containers. The four fields are public on published structs and `get_macros` publishes `HashSet<String>` as its return type, so a `BTreeMap` / `BTreeSet` swap would be a source-level break reserved for the next major; four `serialize_with` hooks leave the in-memory types, the public field types, and the wire shape untouched and make only the byte order deterministic. Both destinations benefit — one `to_string` feeds `--output` and stdout alike. The document uses two comparators, which is now stated rather than incidental: `files` sorts its `PathBuf` keys component-wise, matching what `metrics --output` and `ops --output` sort their aggregates on, while the `String` name sets sort by bytes. The two disagree for paths such as `a/x.h` and `a-b/x.h`, so the unit test carries that trio in both positions and fails if either hook reaches for the other comparator. Fixes #1304
`translated_string: $ => seq('$', $.string)` gives the wrapper one
required `string` child and no text of its own beyond the `$`, but
`BashCode::get_op_type` classified both as operands. Every `$"…"`
therefore scored twice in N2 and planted a second n2 entry beside its
child: `a=$"x"` reported N2 3 for two operands.
The wrapper also defeated the #180 expansion guard.
`bash_string_has_expansion` inspects a node's own children, and a
`translated_string` has only `$` and `string`, so it never saw the
`simple_expansion` one level down and `b=$"$y"` counted both `$"$y"`
and `$y` — exactly the double count #180 removed, for the `$"…"`
spelling alone.
Drop the wrapper arm, as #1351 did for `command_name` in the same
match. The `string` is the keeper under grammar-dispatch §6 because it
is the node present for every spelling: in ordinary argument position
the grammar emits no wrapper at all, just a `$` token and a `string`.
Dropping it therefore makes each wrapper-bearing position agree with
argument position rather than newly disagree. The wrapper is never
childless, so nothing regresses to zero that a plain string did not
already — `$"${#}"` and `"${#}"` both score zero, and the new parity
test pins that.
The duplicated row-driver in the two Bash wrapper tables is extracted
into `assert_bash_wrapper_sheds_one_operand`, which re-derives both
`_before` columns from the current parse instead of trusting them.
Fixes #1358
An audit of the #1358 tests measured that the two `_before`-column identities in `assert_bash_wrapper_sheds_one_operand` hold whatever `get_op_type` answers: neutralising the final `assert_halstead_counts` under the pre-fix arm leaves both wrapper tables green. They catch a mistyped column, not a production regression, so the doc no longer claims they are "re-derived from the current parse" as if that were coverage. Also record that only the command-name half of the parity test moves under the pre-fix arm — dropping the argument-position assertions still fails, dropping the command-name ones passes — so the reference half is not mistaken for redundancy later.
tree-sitter-ruby parses a suffixed numeric literal as a wrapper over the
numeral it suffixes -- `rational: seq($._int_or_float, immediate('r'))`,
and `complex` over an `_int_or_float` or over an aliased `rational` for
the `ri` form. `RubyCode::get_op_type` listed `Integer`, `Float`,
`Complex` and `Rational` side by side in one plain operand arm, so every
level of the nest was billed: `a = 1r; b = 2i; c = 3ri; d = 4` reported
n2 11 / N2 12 for its eight operands, with `3ri` counted three times.
Suppress a numeral whose parent is a `complex` or a `rational`, the
compound-leaf guard of grammar-dispatch section 5.
The wrapper is the keeper rather than the leaf, which is the opposite of
the resolution #1351 and #1358 took for their Bash wrappers. Section 6's
keeper rule -- take the node present for every spelling -- exists to
stop a childless variant scoring zero, and neither choice can do that
here: both wrappers' numeral child is required, and an unsuffixed `4`
carries no wrapper at all. With that hazard absent the tiebreak is
operand identity. `1`, `1r`, `1i` and `1ri` are four distinct Ruby
constants and only the wrapper's span carries the suffix that says
which, so billing the leaf would file all four under the operand text
`1` -- n2 5 / N2 8 for eight operands. Every other language here spells
the suffix inside a single token, so keeping the wrapper is also what
makes Ruby agree with its siblings.
The regression test walks the nesting depths separately, because the
half-fix that guards the two leaf kinds and not the intermediate
`rational` still passes `1r` and `2i` and fails only the `ri` rows.
Fixes #1359
tree-sitter-ruby aliases both ends of a subshell literal to one backtick
token: `bca dump` at the pinned =0.23.1 shows all seven spellings -- the
backtick pair plus %x with {}, (), [], <>, || and !! -- emitting BQUOTE
for the opener and the closer alike. `RubyCode::get_op_type` lists
BQUOTE in its operator arm, so `u = `echo hi`` reported n1 2 / N1 3 for
a line whose only operation is the assignment. This is the delimiter
fabrication #1312 removed for regexes and #1256 for Elixir; `subshell`
was in neither sweep.
Suppress a BQUOTE whose parent is a `subshell`, the compound-leaf guard
of grammar-dispatch section 5. The `subshell` node itself remains the
operand, so the literal is not lost -- including the childless `` ``
spelling, which still bills its wrapper.
Gated rather than deleted, per section 6: a backtick is also a legal
Ruby method name, and at this grammar `def `(cmd)` and `s.`("ls")` wrap
their BQUOTE in a named `operator` node. Deleting the kind would score
that marker zero, which the positive-control test pins.
Parent, not ancestor, and here the difference is observable: a backtick
method inside a subshell's interpolation keeps `operator` as its parent
but gains the `subshell` as an ancestor, so an ancestor scan would
swallow it. The three mutants -- no guard, ancestor scan, and deletion
from the operator arm -- each fail a different subset of the five new
tests.
Ruby Halstead operator counts drop for code containing subshell
literals. The sweep the issue asked for found no sibling: no other
language fabricates a paired-delimiter operator through this shape.
Both baselines are refreshed in this commit. The new arm raises
`get_op_type`'s halstead.effort past its recorded value, and it is the
fourth stuck arm in a match that has been outside `cargo fmt` since
before this change.
Fixes #1360
`Cpp::This` / `Mozcpp::This` were in neither arm of
`CppCode::get_op_type` or `MozcppCode::get_op_type`, so a C++ `this`
contributed nothing at all: not an operator (defensible) and not an
operand (wrong). `struct S { int f() { return this->x; } }` billed `S`,
`f` and `x` and dropped the receiver. Same shape as #1316's character
literals, and the inverse of the #1351-#1355 wrapper/leaf over-counts,
so the fix adds an arm rather than deleting one.
Operand, not operator, on three grounds. Structure: `field_expression`
is `<receiver> -> <field>`, and `p->x` already bills operands `p` / `x`
around the `->` operator, so calling `this` an operator makes `this->x`
a binary operator with one operand and scores the identical AST
differently for the two spellings of the same receiver. Role: `this` is
a pointer rvalue standing exactly where a variable stands — `return
this;`, `*this`, `f(this)` — while `->` / `.` / `*` are the operators
already counted acting on it. Precedent: ten of the thirteen languages
here bill their self-reference as an operand, and the JS family's is the
only reasoned one among them, its `MetaProperty` note calling `this`
"one atomic operand". Keeping C++ an operand also keeps it agreeing with
ObjC, whose `self` is an operand and whose `.mm` files route to this
same impl.
Java, C# and Kotlin disagree, each having swept `This` in as one entry
in a run of keywords grouped by lexical class. That three-way split is
filed as #1380 rather than settled here.
Listing the leaf is safe against a grammar-dispatch section 5 double
count: `this` is childless in every position the pinned grammar admits
it — `this->x`, `(*this)`, `return this`, `f(this)`, `[this]`, `[=,
this]`, `decltype(this->x)`, and a constructor body after a
member-initialiser list — and none of `field_expression`,
`pointer_expression`, `lambda_capture_specifier` or `argument_list` is
classified. Neither generated enum aliases the rule, so there is no
`This2` for a single-variant arm to miss. C++23's explicit object
parameter does not reach the arm at all: the pinned grammar cannot parse
`this S&& self` and emits `type_identifier` plus an ERROR, a spelling
already operand-classed through `TypeIdentifier`.
Halstead `n2` / `N2` rise by one per `this`, moving the derived volume /
difficulty / effort / time / bugs and the three maintainability-index
variants. 157 DeepSpeech corpus snapshots refresh; all 1000 changed
operand values increased, `unique_operands` by exactly one per space,
and no operator count or space moved.
Fixes #1361
`cpp_this_is_an_operand_in_every_position` promised a completeness property its fixture does not have: `delete this;`, `throw this;`, `[&, this]`, a default member initialiser and `this->template f<int>()` all parse cleanly and are absent from `CPP_THIS_POSITIONS`. None of them is a distinct path — every one is the same childless `this:215` leaf under an unclassified wrapper, which is exactly why the arm needs no guard — so the fix is to name the property the walk actually establishes, not to enumerate six more rows. The wrapper check also now covers every ancestor rather than only `chain.last()`, matching the claim both it and the `cpp.rs` arm comment make. `(*this)` is the case that separates them: its `parenthesized_expression` grandparent sits two levels above the leaf, and classifying it (as the aliased `ParenthesizedExpression2`, id 363 — not the `ParenthesizedExpression` 248 a reader would reach for) fails the widened loop where the parent-only form passed.
The Tcl and iRules grammars spell a script body and a plain literal with one kind, `braced_word`. #1314 guarded the `{` of the literal form the grammars special-case (`braced_word_simple`), which left every other value position fabricating a block: `lappend x {a b}` and `puts {c d}` each reported a `{}` operator for a block the line does not contain, where the synonym `set x {a b}` reported none. No kind-scoped arm can separate the two roles, so recognise them out-of-band by the enclosing command's leading word (grammar-dispatch section 9). A braced word filling a slot of a construct the grammar models is a script; one passed to a command in `SCRIPT_TAKING_COMMANDS` (`after`, `eval`, `for`, `on`, `switch`, `time`, `trap`, `uplevel`) is a script; a defaulted `proc` parameter's value and a braced word in the command-name position are data. Everything else is a value and its `{` is a quote, not an operator. An unrecognised command therefore defaults to *value*. The script-taking set is closed and small — Tcl has no user-extensible control structures — while the value-taking set is open, holding every user proc and package command. Defaulting the other way fabricates; this way can only omit one `{}` occurrence whose vocabulary entry any real block in the file already carries. The rule revises the operator and nothing else. Suppressing a value word's contents as well would make `lappend x {a b}` score like `set x {a b}`, which is right for that line and wrong in general: a braced argument to an unrecognised command is as often real code as a list. Built and measured on a file holding an `oo::class` body, a `tcltest -body` and an `apply` lambda, it took n1 5 -> 2, N1 8 -> 2, n2 25 -> 15 and N2 32 -> 15, each body collapsing into a single operand. So the classifier withdraws a claim it cannot support and never discards code the walk has read. The residual asymmetry is filed as #1382; keeping to the brace also keeps the test O(1), where an ancestor scan measured quadratic on a deeply nested `expr`. Tcl's `switch` needs one extra step: the grammar models it with no node of its own, so the arm list parses as a command named after the first pattern and its bodies would read as that command's literals. The rescue is scoped to the arm's arguments, so a braced *pattern* stays a literal. iRules models `switch` with `switch_arm` children and never reaches it. The recognition needs the source bytes, so it lands in a `get_op_type_with_code` override in both getters -- the spelling the walk calls. `get_op_type` keeps answering the byte-less question unchanged. Fixes #1318
The comparison operator and at least one non-comparison construct are the same bare anonymous token in every grammar named here, so six ABC dispatch arms scored markup, type syntax and declarations as decisions. Re-measured at the pinned grammars, `abc.conditions` before -> after: TSX JSX tag delimiters 6 -> 0 JavaScript JSX tag delimiters 6 -> 0 Mozjs JSX tag delimiters 6 -> 0 Lua `local x <const> = 1` attributes 4 -> 0 C# `operator <` / `operator >` overloads 4 -> 2 Kotlin `super<A>.g()` qualified super call 2 -> 0 Perl `<FH>` / `<$fh>` readlines 2 -> 0 Take the allowlist polarity #1274 chose for Java and Groovy, and that C, C++, Objective-C, mozcpp, Rust and Go already used: count the token only when its parent is the construct that applies it. A denylist is a coverage claim a grammar bump can silently invalidate (`.claude/rules/grammar-dispatch.md` §1), and each of these four was already incomplete. TypeScript and TSX denied `type_arguments` and `type_parameters` and left three JSX productions counting; Kotlin denied the same two and left `super_expression`; C# denied `type_argument_list`, `type_parameter_list` and `function_pointer_type` and left `operator_declaration`; JavaScript, Mozjs, Lua and Perl had no gate at all, with `LT` / `GT` sitting in the unconditional condition arm. C# takes a two-sided gate rather than the one-entry allowlist, because its `relational_pattern` (`x is > 0`, and a `> 5 =>` switch-expression arm) is a genuine comparison living outside `binary_expression`; a one-entry allowlist would have under-counted it, the inverse error and the harder one to notice. Preserving that count also preserves a pre-existing double charge against the arm that owns the pattern, filed as #1383 rather than settled here as an unmeasured behaviour change riding along. Each gate rests on a `grammar.json` sweep of its pinned crate, enumerating every production that emits a bare `<` or `>` with hidden `_`-rules resolved to their visible parents: six productions in tree-sitter-typescript 0.23.2, four in tree-sitter-javascript 0.25.0 and in the vendored mozjs fork, two in tree-sitter-lua 0.5.0 (the second being `_attrib`, aliased to `attribute`), six in tree-sitter-c-sharp 0.23.5 and four in tree-sitter-kotlin-ng 1.1.0. Two rows of the issue no longer reproduce and are left alone: #1280 (f9b475b, 01c5953) already gated Bash `file_redirect` and Ruby `superclass`, both with regression tests, and both fixtures measure 0 on this branch before any change of mine. #1275 (8246f50) already settled the issue's `?` section for C# and TS/TSX. Perl is the row the issue got wrong in the other direction. It was cleared on the grounds that `<STDIN>` lexes as one token, which is true — but `<FH>` and `<$fh>` are `standard_input_to_identifier` and `standard_input_to_variable`, plain three-token sequences whose brackets scored two phantom conditions each. A sweep of tree-sitter-perl 1.1.2 finds bare angle brackets in exactly those two productions plus `binary_expression`, so the same gate applies. TypeScript gets a positive-direction parity test rather than a JSX fixture: the `.ts` dialect does not parse JSX, so the construct is unreachable there. On that malformed input the flip does move the number (3 -> 2), because a `<` / `>` under an `ERROR` node no longer counts — the same trade #1274 recorded, in the direction that under-counts unparsable text rather than inflating it. One regression test per affected language, each pairing the excluded construct with a genuine condition and asserting a non-zero total, so an unparsable fixture (which also scores 0) stays distinguishable from a working gate. `csharp_relational_pattern_still_counts_as_a_condition` exists because the `operator <` fixture cannot prove the `RelationalPattern` entry alone (grammar-dispatch §11): its `is > 0` sits beside a `binary_expression` comparison, so a one-entry allowlist would still leave a plausible non-zero total. Verified by eight perturbations of the six gates against the whole 499-test `metrics::abc` suite; every new test fails under at least one, and the JS, Lua, Perl and C# gates fail nothing else. Real comparison shapes were measured rather than assumed: `for` headers, `while` predicates, parenthesised chains, a comparison inside a JSX expression container, C# LINQ `where` beside a generic method call, and Kotlin lambda comparisons all still count. Also corrects prose the change falsified or that contradicts it: the lessons-learned entry whose conclusion was "C#, Kotlin and the JS family still carry the denylist form"; a C# test comment naming an "LT/GT exclusion list" that no longer exists; the note claiming `BinaryExpression2` is reachable here, when C#'s preprocessor admits no relational operator; and the Objective-C arm's claim that its gate is kept only "for parity", when that grammar emits bare angle brackets from four non-comparison productions. Kotlin keeps a residual over-count the gate cannot reach: tree-sitter-kotlin-ng resolves `id<Int>(a)` into nested `binary_expression` nodes, so both brackets satisfy any comparison gate. Recorded on the arm and filed as #1394, alongside #1383 (C# relational-pattern double count) and #1395 (Halstead still bills every excluded bracket as an operator). `PerlCode::compute` reaches cyclomatic 15 with the new arm. It joins the `halstead` marker it already carried rather than being split: it is the same exhaustive one-arm-per-grammar-kind dispatch table that `CppCode`, `CCode`, `ObjcCode` and `MozcppCode` suppress on identical grounds, and removing `cyclomatic` from the marker re-trips the gate, so the marker is load-bearing rather than decoration. No integration-snapshot churn: the C# corpus carries no operator overload, pdf.js no JSX, and no Lua, Kotlin or Perl file is snapshotted. Fixes #1297
Every #1297 test asserted a condition total that its own fixture could still produce after the excluded construct was deleted from it. The production revert test proves the construct reaches the arm today, but nothing failed if a later edit dropped it, so each test could quietly decay into one that asserts the surviving comparison and nothing else. Anchor the three JSX fixtures on `assignments_sum()`: the `className="x"` attribute `=` is each fixture's only assignment, so removing the JSX now fails. Anchor the Perl fixture the same way — the three readlines are three of the sub's four assignments. Assert the C# claim per space instead of through the file total, which is 3 both with the two operator overloads and without them. The overloads open function spaces, so the test can now say what it means: each overload scores no condition and `m` scores three. Lua and TypeScript get a comment rather than an anchor. Once excluded, a Lua attribute and a TypeScript type argument contribute to no ABC axis and open no space — `local x <const> = 1` and `local x = 1` score identically — so there is nothing to assert, and the revert test is the only coverage available. Saying so stops the next reader from taking the missing anchor for an oversight. Verified by perturbing the fixtures rather than the production code: deleting the JSX from each of the three tests, the readlines from the Perl test, and the overloads from the C# test each fails exactly its own test and nothing else.
#778 established that the interior rows of a multi-line string are code, not blank, mirroring Python's #415 decision, and routed thirteen languages through `add_multiline_string_ploc`. Bash, Elixir, Tcl and iRules never got an arm, so in each of them a literal spanning rows reached neither PLOC nor CLOC and `blank = sloc - ploc - cloc` claimed its interior. The issue's four reproductions measured `blank` 2, 2, 1 and 1 on files with no blank rows at all. Route each language's multi-row-capable literals: - Bash: `string`, `raw_string`, `ansi_c_string` and both `heredoc_body` ids. `string` needed an arm despite emitting a `string_content` child per row, because it emits none for a row that is empty *inside* the literal — the shape every other language already counts as code. - Elixir: `quoted_content`, the literal text every string form wraps (`"…"`, `"""` heredoc, `'''` charlist, `~s`/`~S` sigil). Matched by name: the grammar aliases it to twenty ids, and a name comparison survives a twenty-first at a measured +1.3% of walk time. - Tcl and iRules: `quoted_word`. `braced_word` stays out — both grammars parse a braced literal as a script, so routing it would turn every blank line inside every procedure body into code. Elixir's `@doc` / `@moduledoc` heredoc rows are PLOC, not CLOC. Python's carve-out to CLOC is a *bare* string expression statement whose value is discarded — a docstring by position. A module attribute is not that shape: `@doc "…"` is an assignment whose value the compiler stores and `Code.fetch_docs/1` reads back, so its Python analogue is `x = """…"""`, which Python already counts as PLOC. Two fixes to the shared helper fall out of the four new callers: - It no longer takes `end`, deriving the literal's last row from `Node::end_line` instead. A node whose end column is 0 finished at the start of the row below its last content row, so the raw end row is one the literal does not span. No current grammar makes the difference observable — every such row carries a delimiter token that credits it anyway — so this is correct-by-construction, not a behaviour change. - Its parent gate, which skips the opening row on the premise that the enclosing statement owns it, is only safe where the caller's `_` arm credits every node's start row. Bash's is leaf-gated, so a container parent contributes nothing and a childless `raw_string` is the sole node covering its own row: `'ls'` alone in a file reported `ploc 0, blank 1` until Bash's arm inserted the row itself. `bash_heredoc_loc` and `tcl_no_string_lloc` had pinned the defect (`blank 1` on all-code fixtures); both now assert the corrected values, and their snapshots move with them. No integration snapshot moves — the output submodule holds no `.sh`, `.tcl`, `.ex` or `.irule` files. Fixes #1260
Ruby's `private` / `public` / `protected` were modelled as a single
body-wide flag flipped by a bare identifier. That covers one of the
four shapes the language actually has, and two of the other three
corrupt `class_nm` / `class_na` rather than only the public split.
`private def x` and `private attr_accessor :b` nest the declaration in
the keyword's argument list, so a walk over the body's direct children
never sees it: `class A; def pub1; private def hidden; def pub2; end`
reported nm 2 against `nom.functions` 3, and `attr_accessor :a` plus
`private attr_accessor :b` reported na 1. Both walkers now read what a
visibility call's arguments declare, taking visibility from the keyword
rather than the flag.
`private :foo` demotes a method already defined, which a running flag
cannot express at all. `Npm` records each declaration during the pass —
name, method family, visibility — and tallies once the body is read, so
a symbol argument can re-file an earlier entry. The name comes from the
grammar's `name` field, which is what makes `def val=` (a `setter`) and
`def ==` (an `operator`) resolvable; `%i[a b]` names several.
A bare `private` governs instance methods only, so `def self.factory`
stayed public where the flag had demoted it. Only `private_class_method`
reaches a singleton, in both its symbol and its wrapping form, and
`private def self.x` correspondingly leaves one public. Inside
`class << self` the declarations are plain `method` nodes, so the flag
applies there — the exemption keys on the node kind, not the container.
The rules live once, in `npa/shared.rs`, and both metrics consult them,
so the two cannot drift on the same Ruby question. Two guards fall out
of putting them there: a receiver other than `self` means the call
names another object (`Other.private :a` declared and demoted nothing
before this change only because the receiver happened to be a
constant), and a partially interpolated `:"get_#{s}"` resolves to no
name rather than to `get_`.
Deferred with issues rather than folded in: retroactive demotion of an
attribute and the runtime-resolved argument forms (#1399), `initialize`
being automatically private (#1400), and a `def` wrapped in a call that
is not a visibility keyword (#1401).
No integration snapshots move; the output submodule holds no Ruby.
Fixes #1255
The fixture put its `hash_key_symbol` in an unrelated hash literal, so the test asserted the arm was unreached without ever offering it the chance to fire. `attr_accessor :x, foo: 1` puts one inside the very argument list `ruby_symbol_argument_count` walks -- wrapped in a `pair`, which is the claim -- so the na = 1 result now fails if the defensive arm is ever reachable. Perturbing its `_ => 0` to `_ => 1` fails this test; against the old fixture it did not.
`bca.to_sarif(thresholds={"nargs": N})` compared `metrics.nargs.total`,
the subtree sum, while `bca check --threshold nargs=N` has gated on the
callable's own parameter list since #1196. The two front-ends therefore
disagreed on which spaces breach, against `to_sarif`'s documented parity
with `bca check --report-format sarif`: a two-argument function holding
a three- and a two-argument closure scored 7 in the binding and 2 in the
CLI, so closure-heavy code drew SARIF findings no CLI run produces.
The binding walks JSON and no serialized key carried the gate's number —
#1196 moved the extractor without one. Follow the #958 precedent and
serialize it as `nargs.value`, additive and `#[serde(default)]`,
alongside the unchanged `nargs.total`. The sum now has one definition,
`nargs::Stats::own_args()`, which the CLI extractor, the report hotspot
table and the wire projection call instead of spelling
`function_args() + closure_args()` three times.
`metric_catalog`'s documentary `skip_at_unit` flag was stale for the
same reason and is corrected to `true`; its pinned set had asserted a
JSON-vs-accessor parity that stopped holding at #1196. STABILITY.md now
states what #958 and this change both practise — a `wire` struct field
addition is additive within 2.x — and `_flatten.py`'s CSV-parity
docstring names the five `*.value` columns CSV_HEADER does not carry.
The baseline refresh also records two decreases from earlier commits on
this branch, and `extract_summaries_inner`'s abc drops from 40.1 to 39.2
because `own_args()` is one call where the open-coded sum was two.
Fixes #1236
Record the ten issue fixes merged onto this branch: one additive wire field (nargs.value) and nine corrections spanning preproc output ordering, ABC condition counting, LOC blank-line classification, Ruby visibility modelling, and four Halstead classifier defects.
Tcl/iRules braced_word_op_type withdrew every operator child of a value-role braced word rather than only its opening brace, so a `;` command separator was silently dropped and halstead.effort collapsed to 0.0 for an unmodelled command. The Bash lloc arm listed only the unsuffixed VariableAssignment id, which the parser never emits, so every Bash assignment scored zero logical lines. Added the alias, gated so a declaration_command or a command environment prefix does not double-count. The #1297 sweep skipped Elixir, whose denylist named only sigil delimiters, so an operator named rather than applied (`&</2`, `Kernel.<`) scored a phantom ABC condition. Tcl `::eval` fell to the value default and lost its block; a leading global qualifier is now stripped, while `ns::eval` still defaults.
A `switch` arm list is `pattern body pattern body …`, and the Tcl grammar breaks it into commands at newlines. The #1318 rescue kept the *first* braced pattern a literal — it is the arm command's name — but every later one on the same line is an argument, and the rescue read all of an arm command's arguments as bodies. So a one-line `switch -regexp $v { {^a} {…} {^b} {…} }` billed a `{}` around `{^b}` (N1 4) that the same arms written one per line did not (N1 3): the score moved with layout, which is the thing the rule exists to stop. Gate the rescue on argument position. Tcl pairs the list up by index with no marker on either half, so even-indexed arguments are bodies and odd-indexed ones patterns; a `-` fall-through body and a `default` pattern are `simple_word`s that take one slot each, so the parity survives them. Two rows pin it, and perturbing the gate to "always a body" fails exactly the new one-line row across the halstead suite. Also corrects the cost bound #1318 recorded. Its doc-comment, the book and the changelog claimed the value default "can only omit one `N1` occurrence of a `{}` whose vocabulary entry any real block in the file already carries". Measured against `main`: a mixed file omitted seven occurrences, and a top-level `dict for {k v} $d { puts $k }` with nothing around it loses the vocabulary entry too, so `n1` is 0 and `halstead.effort` reads `0.0` (13.5 on `main`, with the `{k v}` list fabricated as a block). Inside a `proc` the keyword and body brace keep the space non-zero (48.4 -> 41.5), which is the scope the metric is gated at, and `set x {a b}` already produced the same `0.0` before #1318 — Halstead's shape on an operator-free space, not a new failure mode. The prose now says so instead of understating it. Lesson 89's #1297 paragraph is pared back to what that fix showed. The rewrite in 80c0b2b closed on "the allowlist needs to know only what a comparison is, and that set does not grow", which the same commit contradicts: C# needed a second allowlist entry for `relational_pattern`, a comparison outside `binary_expression`, and a one-entry allowlist would have under-counted it silently. It also listed JavaScript and Mozjs among the denylist languages when they had no gate at all, and dropped Perl, the row the issue had declared immune. The paragraph now records both directions.
Lesson 93 records the #1196/#1236 shape: a threshold gate moved to a typed per-space accessor while the wire kept serializing only subtree sums, so the JSON-walking SARIF binding diverged from `bca check` with no failing test. The extractor table had noted the disagreement and named no consumer, which is the part of the mechanism worth keeping. The other batch findings had better homes than a lesson: - lesson 66 gains the #1318 layout sub-example (Tcl splits `switch` arm lists at newlines, so the same source reaches the recogniser as one command or several); - grammar-dispatch §6 gets the identity tiebreak for a wrapper/leaf pair with no childless-zero hazard (#1359 vs #1358); - testing.md gets "perturb the fixture as well as the production line" from the #1297 anchors; - batch-fix gains Step 6c, a whole-branch review in a fresh context, after ten per-issue pipelines left four bugs that one pass found.
#778 wired `add_multiline_string_ploc` into eighteen languages, not thirteen; the #1260 comments and changelog entry all copied the wrong number. The `this` regression test's doc said "ten of the thirteen" where the getter comment it mirrors says "eleven of the fourteen". Also record in `.rustfmt-bail-baseline.txt` why `src/getter/ruby.rs` went 3 -> 4 in #1360 (a new arm in an already-bailing match, nothing new to hoist) and lock in the silent `abc/elixir.rs` decrease.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1403 +/- ##
==========================================
+ Coverage 98.46% 98.49% +0.02%
==========================================
Files 278 278
Lines 75772 77200 +1428
Branches 75342 76769 +1427
==========================================
+ Hits 74610 76037 +1427
+ Misses 756 752 -4
- Partials 406 411 +5
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Codecov on PR #1403 reported two production arms no test executed: the C `string_literal` and mozjs `template_string` arms that credit a multi-row string to PLOC. Only `.jsm` routes to the mozjs copy, and C has no raw string, so neither was reached by the sibling fixtures. Add both to the cross-language parity test; each arm perturbed alone now fails it. Two Ruby `?` fall-throughs were reachable but unexercised: the empty delimited symbol `:""` (no `string_content` child) and `self.(...)` (a `call` with a `self` receiver and no `method` field). Extend the #1399 unresolvable-arguments fixture and the another-object npa fixture with one each; perturbing either line fails exactly its own test. The remaining lines are non-UTF-8 or grammar-unreachable `?` branches and assertion failure messages inside tests, none of which a passing test can execute.
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.
Batch fix of ten issues on one integration branch, followed by a
whole-branch review pass and its corrections.
Fixes
$"…"billed once: thestringchild is the keeper, thetranslated_stringwrapper is not classified1r,1i,1ribilled once as therational/complexwrapper, whose text is the constant's identitythisis an operand;cpp/mozcppparity test pins the cloneSCRIPT_TAKING_COMMANDS); one-lineswitchpatterns stay literal</>score a condition only as abinary_expressionoperator, in six grammars (JSX tags, generics, C#operator <, Perl readlines, Lua attributes no longer count)private/protected/public/module_function/private_class_methodmodelled as calls with arguments and retroactive scope, not a bare flagnargs.valuewire field (additive,#[serde(default)]); CLI extractor, report hotspot and wire projection shareStats::own_args(); the Pythonto_sarifbinding reads it, restoring parity withbca checkFixes #1236
Fixes #1255
Fixes #1260
Fixes #1297
Fixes #1304
Fixes #1318
Fixes #1358
Fixes #1359
Fixes #1360
Fixes #1361
Review passes
Two whole-branch reviews ran after the per-issue pipelines:
audit-testshad missed (0c987134), plus a Tclswitchlayoutcase where a one-line arm list reached the recogniser as a single
command (
de043cdd).findings are in
54a32232.That experience is recorded as
batch-fixStep 6c, lesson 93 (the#1196 → #1236 gate/wire divergence), a new
testing.mdsection onperturbing the fixture, and a grammar-dispatch §6 tiebreak.
Validation
make pre-commit:BCA_GATE: passon the branch tip before the twodocs-only commits; those touched comments, Markdown and
.rustfmt-bail-baseline.txtonly (cargo fmt --check,rustfmt-bailandrumdlre-run clean).big-code-analysis-outputat3cd242f2,already on its
main..bca-baseline.tomland.rustfmt-bail-baseline.txtrefreshed inthe commits that moved them; the
src/getter/ruby.rs3 → 4 bailincrease is explained in the baseline's notes.