Skip to content

fix(gradlew): parse args with arg_tokenizer so a flag's value is not the task - #4007

Open
KuSh wants to merge 6 commits into
rtk-ai:developfrom
KuSh:feat/gradlew-arg-tokenizer
Open

KuSh wants to merge 6 commits into
rtk-ai:developfrom
KuSh:feat/gradlew-arg-tokenizer

Conversation

@KuSh

@KuSh KuSh commented Sep 12, 2026 •

Copy link
Copy Markdown
Collaborator

The bug

detect_task picked the gradle task as "the last argument that does not start with -, ignoring clean". It has no notion of a flag consuming the next token, so a flag's value became the task:

rtk gradlew test --tests com.example.Foo   → task = "com.example.Foo" → GradlewTask::Other → unfiltered passthrough
rtk gradlew assembleDebug -p ../other      → task = "../other"        → GradlewTask::Other → unfiltered passthrough

Routing is substring-based (task.contains("test")), so some wrong values still land on the right branch by luck (--tests MyTest contains "test"); the reproducers above are where the luck runs out. Confirmed on the develop binary against a ./gradlew stub: both print the daemon banner and the PASSED line, i.e. no filter ran at all.

Flag facts, verified against real gradle

gradle --help on gradle:jdk21 (9.7.1) and gradle:8.14-jdk21, plus actual runs against a throwaway project, both under LC_ALL=C:

form result
test --tests com.example.Foo value consumed (no "Task not found"); without --tests, Selection failed: Task 'com.example.Foo' not found
-P myprop=v / -Pmyprop=v both set the property
-D sysprop=v / -Dsysprop=v both set the property
--console plain, -x compileJava, -M sha256, --update-locks org.foo:bar value consumed
dependencies --configuration compileClasspath, help --task showProps value consumed
tasks --group build, run --args x, dependencyInsight --dependency x, init --test-framework testng value consumed (gradle help --task <name> lists them; init alone has eight)
-qm clusters (quiet + dry-run)
-mx compileJava rejected: "No argument was provided for command-line option '-x'"
-xcompileJava accepted
-b/-c present (deprecated) in 8.14, removed in 9.7.1 — kept in the table for 8.x wrappers

Dialect

Gradle's parser is org.gradle.cli — gradle:jdk21 ships gradle-cli-9.7.1.jar and no commons-cli — and no other tool uses it, so under #4008's preset naming rule it gets no preset. It is a one-axis derivation of Posix, composed as a const at its call site in gradlew_cmd.rs:

/// `org.gradle.cli` is gradle's own parser, shared with no other tool, so it gets no preset.
const GRADLE: Dialect = Dialect {
    dash_dash: DashDashRole::EndsGlobalOptions,
    ..Dialect::Posix
};

Every other axis is Posix, and each is measured: gradle -qi tasks works, so short flags cluster; only = attaches; every short is single-char; there is no /flag spelling. Shorts use ValueSpec::solo_only() — gradle -qp /w tasks is rejected while gradle -q -p /w and gradle -q -p=/w work, which is exactly what solo_only models, and the same shape git's -n already uses.

DashDashRole::EndsGlobalOptions is the axis value gradle needs. gradle --help calls -- "Signals the end of built-in options. Parses subsequent parameters as tasks or task options only", and that is measurably what it does — gradle -- test --tests com.example.Foo still consumes the filter value, while gradle -- showProps --offline fails. That is neither EndsOptions (git, cargo, rg) nor Forwards (dotnet).

So task detection is one pass under GRADLE. A repeated -- needs one extra rule: where a task is expected it is a task name (gradle -- -- help → Task '--' not found), but after a task it separates that task's options (gradle -- help -- → BUILD SUCCESSFUL), so a -- is dropped from the task names. wants_verbose_logging still slices with before_dashdash, because gradle rejects its own global options past --. #[allow(dead_code)] comes off DashDashRole::EndsGlobalOptions, which gradlew is the in-tree caller of.

Also migrated

The verbose-logging bypass was an arg == "--flag" scan. It now runs on tokens, which gains two things:

  • gradle's short spellings -i/-d/-s/-S turn filtering off the same way --info etc. already did;
  • --info sitting in a value position (--exclude-task --info) no longer trips it.

restore_double_dash is now called: clap strips the user's -- when it is the first token of the trailing region (measured: rtk gradlew -- test … reached the stub as test …, while rtk gradlew test -- … kept its --). Gradle's -- is meaningful, so dropping a token the user typed is a silent argv mutation.

gradlew is the only command routing through this module — gradle, ./gradlew and gradlew.bat all rewrite to rtk gradlew (src/discover/rules.rs), so nothing is left behind.

When no task resolves

Consuming a flag's value can leave no task name at all, and gradle then runs its default help task. Bare rtk gradlew and flags-only invocations (-p ../other, --console plain, -g <home>, -I <script>) therefore stay on passthrough instead of the build filter, which would strip that output.

The value-taking task options cover the built-in tasks rtk routes on rather than claiming to be exhaustive: plugins define task options freely.

Tests

tests/gradlew_arg_grammar_test.rs stubs ./gradlew with a script that records its argv and prints gradle-shaped output, then asserts on the resolved routing (did a filter run) and the child argv, not on the predicate, which reports green for this whole bug family. It spawns rtk through common::rtk_command(), so it never writes to the developer's real ~/.local/share/rtk/. Unit tests cover detect_task's resolved task, including the repeated -- and the no-task cases.

Each behavioural test fails against the previous implementation and passes after. test_detect_last_task_still_wins and test_detect_attached_project_prop_is_not_the_task pass both ways by design: they pin the existing "last task wins" intent and a case the old starts_with('-') scan already got right.

@rtk-wshm-sync-bot

Copy link
Copy Markdown

wshm · Automated triage by AI

📊 Automated PR Analysis

🐛 Type bug-fix
🟡 Risk medium

Summary

Fixes gradlew task detection to use a proper argument tokenizer instead of a naive 'last non-dash argument' heuristic, so flag values (e.g. --tests com.example.Foo, -p ../other) are no longer mistaken for the gradle task name and silently bypass output filtering. Also migrates verbose-flag detection to the same tokenizer and adds handling for gradle's -- semantics, which differ from both existing Dialect models.

Review Checklist

  • Tests present
  • Breaking change
  • Docs updated

Analyzed automatically by wshm · This is an automated analysis, not a human review.

@KuSh
KuSh marked this pull request as draft September 12, 2026 22:22
@KuSh
KuSh force-pushed the feat/gradlew-arg-tokenizer branch 2 times, most recently from 6c552fd to 6f03235 Compare September 18, 2026 21:11
@KuSh
KuSh changed the base branch from develop to feat/tokenizer-dialect-axes September 18, 2026 21:15
@KuSh
KuSh marked this pull request as ready for review September 18, 2026 21:15
@KuSh
KuSh force-pushed the feat/tokenizer-dialect-axes branch from a41cc10 to cd9d7a6 Compare October 6, 2026 22:35
@KuSh
KuSh force-pushed the feat/gradlew-arg-tokenizer branch 2 times, most recently from a1a85eb to bf81426 Compare October 6, 2026 22:49
@KuSh
KuSh changed the base branch from feat/tokenizer-dialect-axes to develop October 6, 2026 22:49
@KuSh
KuSh force-pushed the feat/gradlew-arg-tokenizer branch from bf81426 to d6546df Compare October 6, 2026 22:54
KuSh and others added 6 commits October 7, 2026 02:35
…the task

`detect_task` picked the last argument that did not start with `-`, with no
notion of a flag consuming the next token. `gradlew test --tests com.example.Foo`
therefore resolved the task to `com.example.Foo`, which matches no branch, and
the run fell through to unfiltered passthrough; `gradlew assembleDebug -p ../other`
did the same. Verified against gradle 9.7.1 and 8.14 that `--tests`, `-p`,
`-x`, `-P`, `-D`, `--console`, `--configuration` and the rest of the global
option table take a separate value, and that a clustered value-taker does not
(`-mx compileJava` is rejected), hence `solo_only` for shorts.

Task detection and the verbose-logging bypass now go through
`tokenize_grammar` under `Dialect::Posix`. The bypass gains gradle's short
spellings (`-i`/`-d`/`-s`/`-S`) and stops firing when `--info` is another
flag's value. Gradle's `--` ends built-in option parsing but keeps parsing
tasks and their options, so the region past it is re-tokenized with the same
grammar; `restore_double_dash` hands back the `--` clap strips, which gradle
treats as meaningful.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`tasks --group build` still read `build` as the task and routed the listing
into the build filter, which swallowed it. `gradle help --task` on 9.7.1
confirms `--group`, `--groups`, `--args` and `--dependency` take values too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Consuming a flag's value can leave zero task names, and `gradlew -p ../other`
then fell to the `Build` fallback and had gradle's default-task output stripped
by the build filter. That fallback is now reserved for a genuinely task-less
invocation.

`gradle help --task init` also lists eight value-taking options; without them
`init --test-framework testng` read `testng` as the task and routed to the test
filter, which swallowed the scaffolding output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`gradlew -p ../other` already stayed unfiltered, but bare `gradlew` still hit
the `Build` fallback and had its default-task output stripped. Same reason,
so it takes the same branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`task_names` re-tokenized the region past `--` by hand, because no dialect
modelled a `--` that ends only the *global* option region while tasks and their
options keep parsing. `DashDashRole::EndsGlobalOptions` models exactly that, so
one pass over the whole arg vector replaces the split, and `free_positionals`
folds into its only remaining caller.

Gradle's parser is `org.gradle.cli`, shared with no other tool, so the dialect
is composed from `Posix` at this call site instead of becoming a preset.

The one behaviour the single pass would have changed is a repeated `--`, which
is an ordinary positional rather than a second boundary. Gradle 9.7.1 runs
`gradle -- test --` as `test`, so it is dropped from the task names and pinned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The gradlew grammar test named CARGO_BIN_EXE_rtk directly, so the child it
spawned wrote to the contributor's real ~/.local/share/rtk/ (tracking rows,
tee spool). Build the command with common::rtk_command(), which redirects
rtk's data to the test binary's scratch directory, as the other integration
tests do. core::test_isolation::tests_that_spawn_rtk_isolate_their_data
rejects a test file that names the binary itself.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@KuSh
KuSh force-pushed the feat/gradlew-arg-tokenizer branch from d6546df to 044dca1 Compare October 7, 2026 00:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant