Skip to content

fix(rules): evaluate subshell-wrapped arguments through the wrapper path#297

Merged
fohte merged 5 commits intomainfrom
fohte/fix-time-subshell-wrapper
Apr 11, 2026
Merged

fix(rules): evaluate subshell-wrapped arguments through the wrapper path#297
fohte merged 5 commits intomainfrom
fohte/fix-time-subshell-wrapper

Conversation

@fohte
Copy link
Copy Markdown
Owner

@fohte fohte commented Apr 11, 2026

Why

  • Commands like time (ls | tail -40), where a wrapper's argument is a bare subshell, are not parsed as a wrapped form

What

  • Match subshell arguments as a wrapper's <cmd> placeholder

fohte added 2 commits April 11, 2026 16:56
intent(rules/command_parser): fix `time (lefthook run pre-commit 2>&1 | tail -40)`
  and similar wrapper-over-subshell forms falling through to the default
  action because neither the wrapper pattern nor the compound-command
  path could see the subshell argument as a single unit
learned(rules/command_parser): tree-sitter-bash parses `time (...)` as a
  single `command` node with the subshell as a named child, so
  `extract_commands_with_metadata` emits exactly one `time (...)` string
  and the compound guard never splits it — the fix has to live at the
  tokenize layer that `parse_command` uses for wrapper placeholder
  extraction
decision(rules/command_parser): teach `tokenize` to keep `(...)`, `$(...)`,
  backtick substitutions, and `${...}` as single atoms (with nesting and
  quoted-content awareness) instead of special-casing `time` — the fix is
  then uniform across every wrapper (`sudo (...)`, `env (...)`, etc.) and
  matches how shell word-splitting actually treats these groupings
rejected(rules/command_parser): handling the subshell inside
  `collect_commands` by synthesising separate `time` + inner sub-commands
  — it forces a wrapper-vs-compound coupling into the AST walker and
  still leaves `parse_command`'s whitespace tokenizer broken for any
  other caller
intent(docs/releases): replace the #000 placeholder now that the PR exists
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 11, 2026

Codecov Report

❌ Patch coverage is 89.55224% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.57%. Comparing base (1a3d94f) to head (b4a84d2).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/rules/command_parser.rs 89.55% 7 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #297      +/-   ##
==========================================
+ Coverage   89.55%   89.57%   +0.01%     
==========================================
  Files          52       52              
  Lines       11015    11080      +65     
==========================================
+ Hits         9865     9925      +60     
- Misses       1150     1155       +5     
Flag Coverage Δ
Linux 89.37% <89.55%> (+0.01%) ⬆️
macOS 90.88% <89.55%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

gemini-code-assist[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

…ading subshells

intent(rules/command_parser): stop `read_balanced_group` from being
  tripped by a `)` embedded inside a backtick substitution, a nested
  `$(...)`, or a `${...}` parameter expansion — before this change,
  inputs like `time (echo ${FOO:-)} done)` silently lost the trailing
  content of the outer subshell
learned(rules/command_parser): the outer-group reader only looked at
  plain paren nesting and quoted strings, so any inner construct that
  can legitimately contain an unmatched `)` acted as a false terminator;
  the `` `...` `` / `$(...)` / `${...}` cases all need the same
  skip-over treatment used by the top-level tokenizer
fohte added 2 commits April 11, 2026 17:46
…-sitter AST

intent(rules/command_parser): let wrapper placeholder extraction see a
  whole subshell as a single `<cmd>` argument, so `time (lefthook run
  pre-commit 2>&1 | tail -40)` routes through the `time <cmd>` wrapper
  path and gets its body split into sub-commands instead of falling
  through to `defaults.action`
decision(rules/command_parser): walk the tree-sitter-bash AST when the
  input parses as a single `command` node and emit one token per named
  child, preserving `subshell` / `command_substitution` /
  `process_substitution` verbatim; run everything else through the
  existing whitespace tokenizer as a fallback. This keeps `(...)` and
  `$(...)` fully symmetric at the wrapper layer and avoids re-inventing
  shell quoting in a character-level state machine
rejected(rules/command_parser): adding a `(...)` / `$(...)` / `${...}`
  special case to the whitespace tokenizer — it duplicated tree-sitter's
  balancing and quoting logic and drifted further every time a new
  construct came up; the AST walk is the same rule tree-sitter already
  applies to every other form of compound extraction
learned(rules/command_parser): the real asymmetry that made `time (...)`
  fail was that `collect_commands`'s `command` branch recursed into
  `command_substitution` children but not `subshell` children. Adding
  `subshell` to the recursion list makes the compound extractor treat
  both the same, and letting `parse_command` consult the AST instead of
  whitespace tokens is what actually unblocks the wrapper path
@fohte fohte changed the title fix(rules): parse subshell-wrapped arguments as a single token fix(rules): evaluate subshell-wrapped arguments through the wrapper path Apr 11, 2026
@fohte fohte merged commit 5afc9c8 into main Apr 11, 2026
12 checks passed
@fohte fohte deleted the fohte/fix-time-subshell-wrapper branch April 11, 2026 09:27
@fohte-bot fohte-bot Bot mentioned this pull request Apr 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant