Summary
BashCode::get_op_type (src/getter/bash.rs) classifies both
Bash::TranslatedString and Bash::String as HalsteadType::Operand.
At the pinned tree-sitter-bash 0.25.1, translated_string (238) has
exactly one required child — a string (237) — so a $"…" literal is
counted twice: once as the wrapper, once as the string it wraps.
This is the same wrapper/leaf shape as #1351 (command_name), found while
fixing it, in the same match.
Reproduction
$ printf 'a=$"x"\n' > a.sh
$ bca dump --no-config -p a.sh
╰─ {variable_assignment:209} : a=$"x"
├─ {variable_name:160} : a
├─ {=:9} : =
╰─ {translated_string:238} : $"x"
├─ {$:101} : $
╰─ {string:237} : "x"
$ bca ops --no-config -p a.sh
operands: $"x", "x", a # honest: $"x", a
It also defeats the #180 interpolation guard
bash_string_has_expansion inspects the node's own children. A
translated_string has only $ and string as children, so it never sees
the expansion nested one level down and is classified as an operand
unconditionally — while the inner string is correctly skipped:
$ printf 'b=$"$y"\n' > b.sh
$ bca ops --no-config -p b.sh
operands: $"$y", $y, b # honest: $y, b
So the wrapper reintroduces exactly the double count #180 removed, for the
$"…" spelling only.
Where it is reachable
translated_string surfaces on an assignment RHS, as a case subject, and
in command-name position:
a=$"x" # variable_assignment → translated_string → string
case $"y" in *) ;; esac
$"ls" arg # command_name → translated_string → string
It is not emitted in ordinary argument position — echo $"hello" parses
as a bare $ anonymous token followed by a string, with no
translated_string node at all. That asymmetry is why the bug is easy to
miss: the common spelling is already correct.
Fix
Per .claude/rules/grammar-dispatch.md §5/§6 the keeper is the node that
exists for every spelling of the construct. Here the string is present
in all three positions and the translated_string only in some, so the
wrapper is the arm to drop — but check first whether dropping it loses the
$ prefix from the recorded operand text in a way that matters for bca ops, and re-derive against case/assignment/command-name positions rather
than assuming.
Whichever direction is chosen, the $"$y" interpolating form must end up
counting $y once and nothing else, matching what #180 already guarantees
for a plain "$y".
Tests
src/metrics/halstead.rs already pins the current (wrong) value: the
translated_string row of bash_command_name_wrapper_no_double_count
records $"ls" arg as [0, 0, 3, 3] and carries a FIXME pointing here.
That row is the one to flip, plus a new case for a=$"x" and one for
b=$"$y".
Resolution
Status: Fixed (pending merge) — branch fix/issue-1358, commits
69a7f787 (fix + tests) and 6853f8cc (test-doc accuracy).
BashCode::get_op_type classified the translated_string wrapper and
the single required string child the grammar gives it, so every $"…"
was counted twice in Halstead N2 — and, because
bash_string_has_expansion inspects only a node's own children, the
wrapper also hid a nested simple_expansion and reintroduced the double
count #180 removed. The wrapper arm is gone; the string carries the
count, which is what argument position already did.
Two corrections to this issue, both measured:
- The
translated_string row's _before pair becomes (3, 3), not the
(3, 4) written above — two stacked wrappers make it a counterfactual
against today's tree rather than a historical measurement.
translated_string is emitted in at least eight positions, not the
three listed; and $"${#}" now scores zero, matching what a plain
"${#}" already scored.
See the resolution comment for the sibling sweep: the same wrapper/leaf
shape exists in Perl, Elixir, PHP, Java and the JS family, and wants
separate issues.
Summary
BashCode::get_op_type(src/getter/bash.rs) classifies bothBash::TranslatedStringandBash::StringasHalsteadType::Operand.At the pinned tree-sitter-bash 0.25.1,
translated_string(238) hasexactly one required child — a
string(237) — so a$"…"literal iscounted twice: once as the wrapper, once as the string it wraps.
This is the same wrapper/leaf shape as #1351 (
command_name), found whilefixing it, in the same
match.Reproduction
It also defeats the #180 interpolation guard
bash_string_has_expansioninspects the node's own children. Atranslated_stringhas only$andstringas children, so it never seesthe expansion nested one level down and is classified as an operand
unconditionally — while the inner
stringis correctly skipped:So the wrapper reintroduces exactly the double count #180 removed, for the
$"…"spelling only.Where it is reachable
translated_stringsurfaces on an assignment RHS, as acasesubject, andin command-name position:
It is not emitted in ordinary argument position —
echo $"hello"parsesas a bare
$anonymous token followed by astring, with notranslated_stringnode at all. That asymmetry is why the bug is easy tomiss: the common spelling is already correct.
Fix
Per
.claude/rules/grammar-dispatch.md§5/§6 the keeper is the node thatexists for every spelling of the construct. Here the
stringis presentin all three positions and the
translated_stringonly in some, so thewrapper is the arm to drop — but check first whether dropping it loses the
$prefix from the recorded operand text in a way that matters forbca ops, and re-derive againstcase/assignment/command-name positions ratherthan assuming.
Whichever direction is chosen, the
$"$y"interpolating form must end upcounting
$yonce and nothing else, matching what #180 already guaranteesfor a plain
"$y".Tests
src/metrics/halstead.rsalready pins the current (wrong) value: thetranslated_stringrow ofbash_command_name_wrapper_no_double_countrecords
$"ls" argas[0, 0, 3, 3]and carries aFIXMEpointing here.That row is the one to flip, plus a new case for
a=$"x"and one forb=$"$y".Resolution
Status: Fixed (pending merge) — branch
fix/issue-1358, commits69a7f787(fix + tests) and6853f8cc(test-doc accuracy).BashCode::get_op_typeclassified thetranslated_stringwrapper andthe single required
stringchild the grammar gives it, so every$"…"was counted twice in Halstead
N2— and, becausebash_string_has_expansioninspects only a node's own children, thewrapper also hid a nested
simple_expansionand reintroduced the doublecount #180 removed. The wrapper arm is gone; the
stringcarries thecount, which is what argument position already did.
Two corrections to this issue, both measured:
translated_stringrow's_beforepair becomes(3, 3), not the(3, 4)written above — two stacked wrappers make it a counterfactualagainst today's tree rather than a historical measurement.
translated_stringis emitted in at least eight positions, not thethree listed; and
$"${#}"now scores zero, matching what a plain"${#}"already scored.See the resolution comment for the sibling sweep: the same wrapper/leaf
shape exists in Perl, Elixir, PHP, Java and the JS family, and wants
separate issues.