Skip to content

fix(getter/ruby): subshell backtick delimiters are counted as operators #1360

Description

@dekobon

Summary

Found reviewing the #1353 fix. Ruby's subshell delimiters are counted as
operators, the same fabrication #1312 removed for regex delimiters and
#1256 for Elixir — subshell was not in either issue's sweep.

tree-sitter-ruby aliases both ends of a `…` literal to the same
backtick token:

// grammar.js
subshell: $ => seq(
  alias($._subshell_start, '`'),
  optional($._literal_contents),
  alias($._string_end, '`'),
),

RubyCode::get_op_type (src/getter/ruby.rs) lists R::BQUOTE in the
operator arm for the def `(cmd) method-name marker, so every
subshell literal contributes two ` operator occurrences that are
nowhere in the source as operations.

Reproduction

u = `echo hi`
$ bca dump --no-config -p s.rb        # excerpt
╰─ {subshell:315} : `echo hi`
   ├─ {`:103} : `
   ├─ {string_content:138} : echo hi
   ╰─ {`:103} : `
$ bca ops --no-config -p s.rb
operators: =, `
operands:  `echo hi`, u

n1 2 / N1 3 for a line containing one operator. (Only the opening
delimiter shows in the deduplicated vocabulary, but N1 counts both —
get_operator_id_as_str folds the pair, exactly as it did for the regex
delimiters in #1312.)

Fix shape

The parent guard already sitting eleven lines above in the same match,
applied to the second delimiter family:

R::BQUOTE if ancestors.parent_has_kind(node, R::Subshell as u16) => HalsteadType::Unknown,

Parent, not ancestor — a backtick nested deeper (a def ` inside a
subshell's #{…} interpolation) must still count, which is the
distinction ruby_regex_guard_is_parent_scoped_not_ancestor_scoped
already pins for R::SLASH.

The subshell node itself is the operand (it reaches
string_operand_type in the string-like arm), so suppressing the
delimiters loses nothing.

Tests

Mirror the three regex tests directly:

  • the counts for u = `echo hi` — n1 1 / N1 1, n2 2 / N2 2;
  • def `(cmd) still bills a backtick operator, so the guard is
    parent-scoped and the method-name marker survives;
  • a backtick inside a subshell interpolation still counts.

Sweep

#1314 enumerated eight interpolating languages for the opener question
and did not look at delimiter aliasing outside regex. Any grammar that
aliases a paired delimiter to a token the operator arm also lists is in
scope — bca ops on a one-literal file is the cheap probe, and a
fabricated operator shows up immediately as an n1 the source cannot
justify.


Resolution

Status: Fixed (pending merge)
Branch: fix/issue-1360
Commit: 9108f0adfix(getter/ruby): stop billing subshell backticks
(the hash may be rewritten when the branch is re-signed at merge)

Root cause: tree-sitter-ruby aliases both delimiters of every subshell
spelling — `…` and all six %x forms — to the same BQUOTE token,
which RubyCode::get_op_type lists in its operator arm for the def `
method-name marker, so each literal fabricated two ` operators.

Fix: suppress a BQUOTE whose parent is a subshell
(grammar-dispatch §5). Gated rather than deleted (§6) because the
method-name marker wraps its BQUOTE in a named operator node, and
parent- rather than ancestor-scoped so a backtick method inside a
subshell's interpolation still counts. Five regression tests, each
verified against four production mutants. Sweep found no sibling language
with this defect.

Two premises in the body above were corrected in the resolution comment:
the def ` marker is an operator-wrapped BQUOTE rather than a bare
one, and the backtick heredoc (<<~`CMD`) is not a subshell sibling
— it emits a single heredoc_beginning leaf with no BQUOTE.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions