Skip to content

fix(ruff): route flag-first implicit check invocations to check (#3927) - #3968

Open
anujsingh-cse wants to merge 5 commits into
rtk-ai:developfrom
anujsingh-cse:fix/ruff-flag-first-check-routing
Open

anujsingh-cse wants to merge 5 commits into
rtk-ai:developfrom
anujsingh-cse:fix/ruff-flag-first-check-routing

Conversation

@anujsingh-cse

Copy link
Copy Markdown

Closes #3927

Problem

rtk ruff --select F401 src (a flag-first implicit check) exits 2 with no output because is_check_invocation bails on any leading -. The flag is forwarded verbatim to ruff, which rejects it because --select belongs to ruff check, not ruff. The error lands on stderr (dropped by the ruff filter), so the user sees nothing.

Repro (ruff 0.16.6, LC_ALL=C):

rtk ruff --select F401 src
# <empty>, exit 2

Same for --fix src, --statistics src, -q src.

Fix

Change is_check_invocation to find the first non-flag argument (skipping leading global flags) and route on that:

  • first positional is check, or is not a known subcommand → implicit check
  • otherwise → that subcommand

When there is no positional argument at all, only the genuinely top-level flags (--version/-V/--help/-h) stay top-level; every other flag-only invocation (rtk ruff --fix) is treated as an implicit check.

Tests

  • extended top_level_flags_are_not_misclassified_as_paths with -V/-h
  • flag_first_implicit_check_routes_to_check — --select F401 src, --fix src, -q src
  • flag_only_implicit_check_routes_to_check — --fix, --statistics

Existing routing tests (known_ruff_subcommands_do_not_route_through_check, paths_and_explicit_check_route_through_check) are unchanged and still pass. cargo fmt --check and cargo clippy --all-targets clean; 19 ruff tests pass.

Verified end-to-end: rtk ruff --select F401 <dir> now emits the ruled/filed summary (was empty exit 2), and rtk ruff --version still prints the version top-level.

@rtk-wshm-sync-bot rtk-wshm-sync-bot Bot added bug Something isn't working ruff cli-parsing labels Sep 9, 2026
@rtk-wshm-sync-bot

Copy link
Copy Markdown

wshm · Automated triage by AI

📊 Automated PR Analysis

🐛 Type bug-fix
🟢 Risk low

Summary

Fixes rtk's argument routing so flag-first implicit check invocations (e.g. rtk ruff --select F401 src) are correctly routed to the ruff check subcommand instead of being forwarded verbatim to ruff and failing silently. The change updates is_check_invocation to find the first non-flag argument, skipping leading global flags, to determine routing.

Review Checklist

  • Tests present
  • Breaking change
  • Docs updated

Linked issues: #3927


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

@anujsingh-cse
anujsingh-cse force-pushed the fix/ruff-flag-first-check-routing branch from f263b9f to ae272dc Compare September 13, 2026 04:15
@anujsingh-cse

Copy link
Copy Markdown
Author

Rebased onto latest \develop\ so it's up to date and mergeable. Happy to adjust anything reviewers would like.

@anujsingh-cse
anujsingh-cse force-pushed the fix/ruff-flag-first-check-routing branch from ae272dc to bfd38ff Compare October 10, 2026 08:47
@anujsingh-cse

Copy link
Copy Markdown
Author

Rebased onto current develop and fixed the CI failure.

The branch was 300+ commits behind a develop that has since repaired broken rustdoc intra-doc links (the rustdoc gate failed on the stale base); rustfmt also rewrapped one test literal to current style.

All local gates pass against current develop: cargo fmt --all --check, cargo clippy --all-targets, cargo doc --no-deps, the test-isolation guard, and all 12 ruff_cmd tests. The fresh workflow run will need approval to start.

@Protocol-zero-0 Protocol-zero-0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified bfd38ff with real Ruff 0.17.0 on Linux: nine scenarios, 27 raw/parent/head executions. The intended implicit --select F401 bad.py and -q clean.py routes are fixed, and ordinary check, help format, and --version still work. However, supported global-option-before-subcommand commands regress (see inline reproduction), so I recommend preserving those routes before merging.

Validation: 12 focused Ruff unit tests, cargo fmt --all -- --check, and cargo clippy --all-targets -- -D warnings passed. This is focused local verification, not full-suite or cross-platform validation. Related #3933 also covers global-option routing; please coordinate the shared routing changes rather than integrate competing parsers independently.

Comment thread src/cmds/python/ruff_cmd.rs Outdated
@anujsingh-cse

Copy link
Copy Markdown
Author

Fixed in 14c891c. The routing now tokenizes the pre-subcommand arguments instead of finding the first token without a -:

The boolean-predicate tests are replaced by complete CLI cases asserting the full forwarded argument vector — the gap you pointed out is exactly what let the forwarding regressions through.

Verified with real Ruff 0.17.0 through the built rtk binary (your three reproductions first):

17 ruff_cmd unit tests (six of them the new pinned CLI plans), cargo fmt --all --check, cargo clippy --all-targets, cargo doc --no-deps, and the test-isolation guard all pass.

@Protocol-zero-0 Protocol-zero-0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-tested exact head 14c891c with Ruff 0.17.0 on Linux: 12 scenarios / 36 raw-previous-head executions. The earlier --isolated check, --config ... format --check, and --config ... --version forwarding regressions are fixed. Implicit checks, split --color, equals-form config and the -- path separator also keep the expected exit codes.

One new output regression remains: rtk ruff help format is no longer complete. Raw Ruff and the previous reviewed head bfd38ff both return the same 4,884 bytes; this head returns 2,001 bytes ending in ..., losing --range, file selection and the remaining global options. The new effective-subcommand decision sends help through the existing generic truncation branch. Please preserve the complete non-check/help output and pin the stdout bytes, not only the forwarded argument vector. This is the output half of the #3933 coordination point.

The new routing parser should also use the shared arg_tokenizer API, as required by .claude/rules/rust-patterns.md rule 6, rather than its own flag/value scanner. The narrow fix should retain the now-correct forwarding cases above.

17 focused Ruff unit tests, fmt and clippy with warnings denied pass. No full-suite or cross-platform run in this review.

Comment thread src/cmds/python/ruff_cmd.rs Outdated
@anujsingh-cse

Copy link
Copy Markdown
Author

Both points addressed in d39d176.

Shared tokenizer. route_ruff_args now goes through tokenize_grammar with Dialect::Posix and a ruff_takes_value predicate (--config/--color are ruff 0.17's only value-taking global options). The first free positional before the -- boundary decides the route; before_dashdash/dashdash_index handle the separator, so a path after -- keeps implicit check while a check after it stays a path. No own flag/value scan remains.

Help output regression. The complete passthrough for help format was an accident of the old is_format matching any format token; keying the filter on the effective subcommand exposed it to the generic truncation. The output choice is now an explicit filter_ruff_output that keeps help, version, and top-level requests byte-complete, while other subcommands keep the bounded passthrough. Tests pin the stdout bytes — a help-sized fixture longer than passthrough_max_chars comes back exactly, and a rule-sized one still truncates — alongside the forwarded-argument plans.

Verified against real Ruff 0.17.0 through the built binary:

  • rtk ruff help format → 5,014 bytes, identical to raw ruff help format (compared both on this host)
  • the three earlier reproductions stay fixed: --isolated check clean.py exit 0, --config ruff.toml format --check exit 0 verbatim, --config ruff.toml --version prints the version
  • implicit --select F401 dirty.py still exits 1 with the check summary; -- clean.py routes through the separator as implicit check

20 ruff_cmd unit tests, cargo fmt --all --check, cargo clippy --all-targets (warnings denied), cargo doc --no-deps, and the test-isolation guard pass.

On #3933: with the output half now here and the routing half subsuming the first-argument rule (including its help format case and the global-option forms), this PR carries the full coordination point; whichever you prefer to land first, the other can rebase on it.

@Protocol-zero-0 Protocol-zero-0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed d39d176 with real Ruff 0.17.0 on Linux. The prior global-option forwarding and full-help blockers are cleared, and routing now uses the shared tokenizer.

Sixteen CLI scenarios / 48 raw-previous-head comparisons cover implicit checks, explicit check after global flags, config/color value forms, format checks, complete help/version output, separators and a 55-diagnostic run. Full help format stdout now matches raw Ruff, including the previously missing --range section; the previous head still truncates it. Twenty Ruff unit tests, fmt and clippy (all targets, warnings denied) pass.

One pre-existing CLI boundary gap remains on both previous and current heads: with a clean file named check, rtk ruff -- check checks the entire directory and exits 1 where ruff check -- check exits 0. The leading separator is lost before the routing helper receives the arguments. This is outside the changed router and does not hold this fix; the direct-router separator tests do not establish that end-to-end case.

The inline encoding note is a small cleanup, not a remaining blocker for this routing/help fix. I did not run the full repository suite, rustdoc or other platforms. #3933 overlaps this effective-subcommand routing; please coordinate integration rather than landing the two independently.

Comment thread src/cmds/python/ruff_cmd.rs Outdated
@anujsingh-cse

Copy link
Copy Markdown
Author

Restored in 6345475 — the ellipsis was mangled by a Windows-1252 round-trip in my brace-cleanup edit, which also corrupted the test expectation to match. Both now carry the proper U+2026; verified no mis-encoded bytes remain in the file and all 20 ruff_cmd tests, fmt, and clippy still pass. Thanks for the thorough re-review across the sixteen scenarios.

@KuSh KuSh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 1 — CHANGES REQUESTED

Claim: rtk ruff finds the subcommand through the shared tokenizer, so flag-first implicit checks (rtk ruff --select F401 src, #3927) and global options before an explicit subcommand route to the right ruff subcommand, and help/version output stays complete.
Scope: accept. This lands before #3933 (its help format case is covered here, and #3928 closes with this PR) and before #3912 (rename only, rebases on top).
Ran: real ruff 0.16.6, LC_ALL=C, a 64-module package plus a 301-module package for analyze graph. 39 invocations × native ruff / develop 5ab6291 / this head merged onto develop, comparing exit codes and stdout bytes. 586 generated invocations (global options × subcommands; global + check options + paths) checked against what native ruff accepts, with the forwarded argv captured: 0 mismatches (develop: 367). Invariants: over-cap + accounting (50 shown + 192 counted = 242 issues; 50 + 252 = 302), every decision, under the hook (the rewrite rule matches ruff check|format only and is unchanged). Savings on check runs: 95–96% (65177 → 3339 B, 87599 → 3229 B) vs the 20% floor (CONTRIBUTING.md).
Pins: 16 decisions, 25 mutations, 8 survived: 4 router decisions (item 2 below), 2 equivalent, and 2 that only move the 2000-char passthrough cap (#4655), which I am not holding this PR on.
Matrix: executed this round; pin check: executed this round
CI @6345475: no run. The branch conflicts with develop in src/cmds/python/ruff_cmd.rs (against #3942), so there is no merge ref to build, and the runs on the last four heads ended action_required. CI runs after the rebase.

Blocks merge (2)

  1. src/cmds/python/ruff_cmd.rs:143 — the router reads check-only options as if they were global (3 sites: removing the explicit check, first-positional subcommand detection, the help/version scan in top_level_request; the first is the one that must change). The grammar knows --config/--color as value options. In the flag-first form this PR adds, a check option's value therefore becomes a free positional. When that value is check, it is taken for the explicit subcommand and removed, and the option takes the next argument instead:

    • rtk ruff --output-file check keep.py runs ruff check --output-format=json --output-file keep.py: keep.py is overwritten with the JSON report for the whole directory. develop: exit 2, file intact. Native ruff check --output-file check keep.py writes the report to check.
    • rtk ruff --extend-exclude check src/pkg/bad.py reports 601 issues in 361 files, with bad.py excluded. Native: bad.py's 2 issues. develop: exit 2.
    • rtk ruff -qh prints check's help behind Ruff check (JSON parse failed: …). develop and native print the top-level help (1977 B). top_level_request compares raw strings, so it misses a cluster (.claude/rules/rust-patterns.md rule 6).

    A correct fix: when an option outside ruff's global grammar (--config, --color, --isolated, -v/--verbose, -q/--quiet, -s/--silent, -h/--help, -V/--version) appears before the first free positional, the call is the implicit-check form. No later word is the subcommand and nothing is removed. Help and version are read from the tokens. Rows it must pass: the three invocations above give keep.py intact with the report in check, bad.py's 2 issues, and the 1977 B top-level help. --extend-exclude config src/pkg/bad.py (exit 2 on both sides today) then works too. The 39 rows and 586 cases above stay unchanged. Tests: plan assertions for these inputs.

  2. src/cmds/python/ruff_cmd.rs:164 — unpinned behaviour (router decisions). Each of these edits passes all 28 ruff tests:

    • drop "color" from RUFF_GLOBAL_VALUE_OPTIONS: no test uses --color. Test must assert: --color never check a.py → check --output-format=json --color never a.py; --color never format --check a.py forwarded verbatim.
    • replace the return RuffRoute::Check { explicit_at: None } after a non-subcommand positional with continue (a later subcommand-named word wins). Test must assert: src format → check --output-format=json src format.
    • iterate tokens.iter() instead of before_dashdash(&tokens). Test must assert: --isolated -- format → check --output-format=json --isolated -- format. A -- in the middle reaches the router; a leading one does not (follow-ups).
    • false && in front of the post-loop dashdash_index(..) check. It only differs from the flags-only fallback when -h/--help/-V/--version is also present: pin it with such an input, or drop it.

    Each mutation must turn a test red.

Optional

  • run(), plan_ruff_command and filter_ruff_output each re-route and re-scan for --output-format. Computing the route once in run() and moving it into the closure keeps the three in step.
  • -v prints Running: ruff <user args>, not the argv rtk actually runs (unchanged from develop). With the plan in hand, printing the plan would show the injected check.

Filed as follow-ups

  • #4701 rtk ruff check --statistics prints statistics JSON where ruff prints a table, larger than native output. Pre-existing for check --statistics; this PR adds the --statistics spelling.
  • #4702 rtk ruff -- <path> drops the leading --: rtk ruff -- check lints the whole directory, where ruff check -- check lints the file check. Pre-existing.
  • #4655, comment added: with this PR, rtk ruff --output-format concise src reaches the same 2000-char cap as rtk ruff check --output-format concise src (byte-identical output); generate-shell-completion bash is cut from 311361 B to 2001 B.

Checked and correct — no need to re-verify

  • #3927's forms: --select F401 src (summary, exit 1 = native), --fix src, --statistics src, -q src, -s src. --fix or --statistics alone append ..
  • Global option before the subcommand: --isolated check src and --config ruff.toml check src give the JSON summary. --config ruff.toml format --check src and --config ruff.toml --version are forwarded verbatim, with exit codes equal to native.
  • help format (4889 B), help check (6297 B), --help, -h, -V, version: byte-for-byte equal to native. … +192 more renders as UTF-8.
  • Over-cap check output: lines shown + the count in the notice = total.

Your questions

None open since the last push; the 13 Oct comment closed the ellipsis note.

Next

Rebase onto develop. The conflict with #3942 resolves by keeping its exemption: in ruff_output_is_complete, the Subcommand arm also returns true for is_analyze_graph_invocation(args), and #3942's two tests call filter_ruff_output. With that, analyze graph big returns all 14816 B, as native does, and the full suite passes on the merge. Then item 1 and the tests in items 1 and 2. After that, this merges. Global options before analyze graph are #4654, outside this claim.
Rounds: 1. Threads: 2 open (items 1–2, waiting on the author), 3 resolved this round (routing, help output and ellipsis, fixed in 14c891c, d39d176 and 6345475).

Comment thread src/cmds/python/ruff_cmd.rs Outdated
if RUFF_SUBCOMMANDS.contains(&token.text) {
if token.text == "check" {
return RuffRoute::Check {
explicit_at: Some(token.source_index),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocks merge (1): a check option's value is taken for the explicit check. --output-file, --extend-exclude, --select and so on are unknown to the grammar, so their value is a free positional. When that value is check, it is taken for the subcommand and removed:

  • rtk ruff --output-file check keep.py runs ruff check --output-format=json --output-file keep.py and overwrites keep.py with the report. develop: exit 2, file intact.
  • rtk ruff --extend-exclude check src/pkg/bad.py lints 361 files with bad.py excluded. Native: bad.py's 2 issues.

An option outside ruff's global grammar before the first free positional should make this the implicit-check form: no later word is the subcommand, nothing is removed. The top_level_request raw scan belongs to the same fix, read from tokens: rtk ruff -qh today prints check's help behind a JSON-parse banner, where develop and native print the top-level help. The rows are in the review body.

/// (`ruff --help`, 0.17: `--config` and `--color`). Every other global token
/// is a flag; rule-selection options belong to the subcommand and only
/// appear once the routing decision has been made.
const RUFF_GLOBAL_VALUE_OPTIONS: &[&str] = &["config", "color"];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocks merge (2): router decisions no test pins. Each of these passes all 28 ruff tests:

  • drop "color" here → needs --color never check a.py → check --output-format=json --color never a.py, and --color never format --check a.py forwarded verbatim
  • continue instead of returning Check after a non-subcommand positional → needs src format → check --output-format=json src format
  • tokens.iter() instead of before_dashdash(&tokens) → needs --isolated -- format → check --output-format=json --isolated -- format
  • false && before the post-loop dashdash_index check → pin with -h/--version present, or drop it

Each mutation must turn a test red.

Finding the first token without a `-` misrouted invocations that put a
global option before the subcommand: `--config ruff.toml` fed its value
to the routing decision, so `--config ruff.toml format --check` was
treated as an implicit check and rebuilt as one, and an explicit
`check` after `--isolated` was forwarded as a path.

Route on a real pre-subcommand scan instead. Only `--config` and
`--color` consume a value (ruff 0.17 `--help`); every other global
token is a flag, and the first bare token decides the route — a known
subcommand routes, anything else is a path and the run is an implicit
check. An explicit `check` is consumed at its actual position, and
flag-only runs keep the top-level requests (`--version`, `--help`)
while others stay implicit checks. `is_format` now keys on the
effective subcommand, so `help format` no longer reaches the format
filter.

The boolean-predicate tests are replaced by complete CLI cases that
pin the full forwarded argument vector, which is what let the
forwarding regressions slip through before.
Route through the shared `arg_tokenizer` (rust-patterns rule 6) instead
of the hand-rolled scan: `tokenize_grammar` with a `Dialect::Posix`
grammar and a `ruff_takes_value` predicate for the two value-taking
global options, so an option value is consumed rather than read as the
subcommand or a path. The first free positional before the `--`
boundary decides the route, and a path after the boundary keeps the
run an implicit check.

Help and version output regressed to the bounded passthrough once the
format filter keyed on the effective subcommand. Extract the output
choice into `filter_ruff_output` and keep `help`/`version` requests and
top-level flags complete — raw Ruff returns every byte, and so does
rtk — while other subcommands keep the bounded passthrough. Pin the
stdout bytes for the complete routes instead of only the forwarded
argument vector.
A Windows-1252 round-trip mangled the ellipsis in the '+N more' cap
line and in its test expectation to the mojibake byte sequence.
Restore the proper U+2026 in both.
A check-only option's value looked like a free positional to the router,
so `--output-file check keep.py` removed that value as the explicit
subcommand and `--output-file` consumed `keep.py`.

Route the whole invocation as an implicit check whenever an option
outside ruff's global grammar appears before the first free positional:
nothing is removed and no later word is treated as the subcommand. Read
help and version from tokenized flags so a cluster such as `-qh` stays
top-level, and drop the redundant post-loop `--` fallback.

Add plan pins for every router decision named in the review.
@anujsingh-cse
anujsingh-cse force-pushed the fix/ruff-flag-first-check-routing branch from 6345475 to 211f7cf Compare October 26, 2026 09:09
@anujsingh-cse

Copy link
Copy Markdown
Author

Rebased onto develop (e05c7bd) and force-pushed 211f7cf; the fresh CI run will need approval.

#3942 conflict. Resolved as you specified: the Subcommand arm of ruff_output_is_complete also returns true for is_analyze_graph_invocation(args), and #3942's two tests now call filter_ruff_output. analyze graph keeps its full output.

Item 1 — check-only options. The router now scans the tokens before the first free positional against ruff's global grammar (--config/--color take values; --isolated, -v/--verbose, -q/--quiet, -s/--silent, -h/--help, -V/--version are flags). An option outside that grammar makes the invocation the implicit-check form immediately: nothing is removed and no later word is the subcommand. top_level_request reads tokens now, so clusters work. The post-loop dashdash_index block is gone, and --help -- path pins that a top-level request stays top-level.

Item 2 — pins. Added plan assertions for every row. Mutation check, each applied locally and each observed red:

Mutation Test that went red
drop "color" ruff_routes_color_and_later_words_per_review — --color never check a.py left the explicit check in the args
continue instead of return Check same test — src format planned ["src", "format"]
iterate tokens.iter() ruff_dashdash_and_clusters_route_per_review — --isolated -- format planned verbatim
re-add the post-loop dashdash_index block same test — --help -- path planned as a check

Verification (Windows, ruff 0.16.2):

  • cargo test --lib cmds::python::ruff_cmd: 31 passed.
  • cargo fmt --all -- --check clean; cargo clippy --all-targets -- -D warnings clean.
  • Full cargo test --lib: 4463 passed / 22 failed. The same 22 tests fail on the unmodified tree (4460 passed / 22 failed, all Windows sh-missing); the delta is exactly the 3 new tests.
  • Real-binary rows: rtk ruff --output-file check keep.py leaves keep.py intact and writes the F401 JSON report to check; rtk ruff --extend-exclude check src/pkg/bad.py reports bad.py's 2 F401s; rtk ruff -qh is byte-identical to ruff -qh (2021 B); --color never check a.py gives the JSON summary and --color never format --check a.py forwards verbatim.

@Protocol-zero-0

Protocol-zero-0 commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

The completion-only supplement below is now available as a reviewable PR against this PR's source branch.

Rechecked revised head 211f7cf3266dcfd662a31006ad8cdf32b2327977 on Linux with Ruff 0.17.0. The shared-router changes retain the tested help/cluster/global-option and check-only-option behavior.

The completion-output follow-up from #4655 still needs an adaptation to this new head: all five shell generators return only 2,001 bytes. The attached additive patch applies directly to 211f7cf, reuses its effective route, and sends complete documents through the existing runner passthrough. Generated Bash, Zsh, Fish, PowerShell and Elvish output then matches native Ruff byte for byte (Bash: 311,984 bytes; Zsh: 687,461 bytes). No runner or argument-scanning implementation is added.

Validation:

  • 21 real Ruff scenarios / 63 native, unmodified-head and patched executions; exit codes 0/1/2 preserved. Completion help/error controls, global flags/config, full help/version/graph and summarized checks are included.
  • All five Bash variants pass bash -n for native and patched output; unmodified-head scripts fail all five. Scripts were syntax checked, never evaluated.
  • 32 router unit tests, three passthrough process tests, fmt and warnings-denied all-target clippy pass.

This is the completion-only adaptation, not the full #4654/#4655 selected-format integration. It neither opens that dependent PR before this one lands nor changes the outstanding maintainer review. Full cargo test --locked stopped at two ruff_double_dash_test failures (4,871 passed, two failed, eight ignored before stopping). Both failing separator tests also fail on the unmodified 211f7cf head; no full-suite success is claimed. Linux/Ruff 0.17.0 only.

Completion-only supplement for revised head 211f7cf
diff --git a/src/cmds/python/ruff_cmd.rs b/src/cmds/python/ruff_cmd.rs
index 6d63126b..753e230d 100644
--- a/src/cmds/python/ruff_cmd.rs
+++ b/src/cmds/python/ruff_cmd.rs
@@ -78,6 +78,18 @@ pub fn run(args: &[String], verbose: u8) -> Result<i32> {
         eprintln!("Running: ruff {}", args.join(" "));
     }
 
+    if ruff_output_is_complete(route, args) {
+        // Complete documentation can exceed the capture limit and may contain
+        // shell syntax whose bytes must be preserved exactly.
+        return runner::run(
+            cmd,
+            "ruff",
+            &display_args(args),
+            runner::RunMode::Passthrough,
+            runner::RunOptions::default(),
+        );
+    }
+
     let owned_args = args.to_vec();
     runner::run_filtered(
         cmd,
@@ -123,12 +135,15 @@ fn filter_ruff_output(args: &[String], stdout: &str) -> String {
     }
 }
 
-/// Help and version requests must keep their full stdout.
+/// Help, version, graph JSON and generated shell completions must keep full stdout.
 fn ruff_output_is_complete(route: RuffRoute, args: &[String]) -> bool {
     match route {
         RuffRoute::TopLevel => true,
         RuffRoute::Subcommand { at } => {
-            matches!(args[at].as_str(), "help" | "version") || is_analyze_graph_invocation(args)
+            matches!(
+                args[at].as_str(),
+                "help" | "version" | "generate-shell-completion"
+            ) || is_analyze_graph_invocation(args)
         }
         RuffRoute::Check { .. } => false,
     }
@@ -533,6 +548,31 @@ fn compact_path(path: &str) -> String {
 mod tests {
     use super::*;
 
+    #[test]
+    fn completion_requests_are_complete_documents_only_when_routed() {
+        let args = |values: &[&str]| {
+            values
+                .iter()
+                .map(|value| value.to_string())
+                .collect::<Vec<_>>()
+        };
+        for args in [
+            args(&["generate-shell-completion", "bash"]),
+            args(&["--isolated", "generate-shell-completion", "bash"]),
+            args(&["--config", "ruff.toml", "generate-shell-completion", "bash"]),
+        ] {
+            let route = route_ruff_args(&args);
+            assert!(ruff_output_is_complete(route, &args));
+        }
+        for args in [
+            args(&["--config", "generate-shell-completion", "check", "src"]),
+            args(&["check", "--", "generate-shell-completion"]),
+        ] {
+            let route = route_ruff_args(&args);
+            assert!(!ruff_output_is_complete(route, &args));
+        }
+    }
+
     #[test]
     fn known_ruff_subcommands_do_not_route_through_check() {
         for subcommand in RUFF_SUBCOMMANDS {
diff --git a/tests/ruff_passthrough_test.rs b/tests/ruff_passthrough_test.rs
index 7938faeb..14e5a368 100644
--- a/tests/ruff_passthrough_test.rs
+++ b/tests/ruff_passthrough_test.rs
@@ -88,3 +88,18 @@ fn other_passthrough_subcommands_keep_the_cap() {
         context(&out)
     );
 }
+
+#[test]
+fn shell_completion_output_is_untruncated_and_byte_exact() {
+    let dir = common::tempdir();
+    let document = "complete -F _ruff ruff\n".repeat(512);
+    fake_ruff(
+        dir.path(),
+        &["generate-shell-completion", "bash"],
+        &document,
+    );
+
+    let out = rtk_ruff(dir.path(), &["generate-shell-completion", "bash"]);
+    assert_eq!(out.status.code(), Some(0), "{}", context(&out));
+    assert_eq!(out.stdout, document.as_bytes(), "{}", context(&out));
+}

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

Labels

bug Something isn't working cli-parsing ruff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rtk ruff never routes to check when a global flag comes first: rtk ruff --select F401 src exits 2 with no output

3 participants