From 7e44075fcde2a44abdf646ec98a0d72f21a062e1 Mon Sep 17 00:00:00 2001 From: mgierok Date: Tue, 24 Mar 2026 18:10:34 +0100 Subject: [PATCH 1/3] fix(golangci-lint): restore run wrapper and align guidance Keep bare golangci-lint invocations as passthrough while preserving compact filtering for golangci-lint run. Update discover/rewrite rules, regression tests, and docs to advertise only the supported compact run path. --- CLAUDE.md | 2 +- README.md | 2 +- docs/FEATURES.md | 4 +- docs/TROUBLESHOOTING.md | 2 +- src/discover/registry.rs | 34 ++++- src/discover/rules.rs | 6 +- src/golangci_cmd.rs | 269 +++++++++++++++++++++++++++++++++++---- src/main.rs | 4 +- 8 files changed, 285 insertions(+), 38 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 35ff19ed..cac8f202 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -391,7 +391,7 @@ pub fn execute_with_filter(cmd: &str, args: &[&str]) -> Result<()> { - `rtk go test`: NDJSON line-by-line parser for interleaved events (90%+ reduction) - `rtk go build`: Text filter showing errors only (80% reduction) - `rtk go vet`: Text filter for issues (75% reduction) - - `rtk golangci-lint`: JSON parsing grouped by rule (85% reduction) + - `rtk golangci-lint run`: JSON parsing grouped by rule (85% reduction) - **Architecture**: Standalone Python commands (mirror lint/prettier), Go sub-enum (mirror git/cargo) - **Patterns**: JSON for structured output (ruff check, golangci-lint, pip), NDJSON streaming (go test), text state machine (pytest), text filters (go build/vet, ruff format) diff --git a/README.md b/README.md index 6073d5eb..a6922ef6 100644 --- a/README.md +++ b/README.md @@ -404,7 +404,7 @@ Plugin in `openclaw/` directory. Uses `before_tool_call` hook, delegates to `rtk | `pytest` | `rtk pytest` | | `pip list/install` | `rtk pip ...` | | `go test/build/vet` | `rtk go ...` | -| `golangci-lint` | `rtk golangci-lint` | +| `golangci-lint run` | `rtk golangci-lint run` | | `rake test` / `rails test` | `rtk rake test` | | `rspec` / `bundle exec rspec` | `rtk rspec` | | `rubocop` / `bundle exec rubocop` | `rtk rubocop` | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 061a604a..8eece7cf 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -758,7 +758,7 @@ Regroupe les erreurs de type par fichier. --- -### `rtk golangci-lint` -- Linter Go +### `rtk golangci-lint run` -- Linter Go **Economies :** ~85% @@ -1269,7 +1269,7 @@ rtk verify | `mypy` | `rtk mypy` | | `pip list/install` | `rtk pip ...` | | `go test/build/vet` | `rtk go ...` | -| `golangci-lint` | `rtk golangci-lint` | +| `golangci-lint run` | `rtk golangci-lint run` | | `docker ps/images/logs` | `rtk docker ...` | | `kubectl get/logs` | `rtk kubectl ...` | | `curl` | `rtk curl` | diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index cf52f026..fba0ca96 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -224,7 +224,7 @@ rtk --version # Should be 0.23.1+ ``` ### Affected Commands -All commands that spawn external tools: `rtk vitest`, `rtk lint`, `rtk tsc`, `rtk pnpm`, `rtk playwright`, `rtk prisma`, `rtk next`, `rtk prettier`, `rtk ruff`, `rtk pytest`, `rtk pip`, `rtk mypy`, `rtk golangci-lint`, and others. +All commands that spawn external tools: `rtk vitest`, `rtk lint`, `rtk tsc`, `rtk pnpm`, `rtk playwright`, `rtk prisma`, `rtk next`, `rtk prettier`, `rtk ruff`, `rtk pytest`, `rtk pip`, `rtk mypy`, `rtk golangci-lint run`, and others. --- diff --git a/src/discover/registry.rs b/src/discover/registry.rs index fafdaa8b..497e6dc6 100644 --- a/src/discover/registry.rs +++ b/src/discover/registry.rs @@ -1855,7 +1855,29 @@ mod tests { assert!(matches!( classify_command("golangci-lint run"), Classification::Supported { - rtk_equivalent: "rtk golangci-lint", + rtk_equivalent: "rtk golangci-lint run", + .. + } + )); + } + + #[test] + fn test_classify_golangci_lint_bare_is_not_compact_wrapper() { + assert!(!matches!( + classify_command("golangci-lint"), + Classification::Supported { + rtk_equivalent: "rtk golangci-lint run", + .. + } + )); + } + + #[test] + fn test_classify_golangci_lint_other_subcommand_is_not_compact_wrapper() { + assert!(!matches!( + classify_command("golangci-lint version"), + Classification::Supported { + rtk_equivalent: "rtk golangci-lint run", .. } )); @@ -1893,6 +1915,16 @@ mod tests { ); } + #[test] + fn test_rewrite_bare_golangci_lint_skips_compact_wrapper() { + assert_eq!(rewrite_command("golangci-lint", &[]), None); + } + + #[test] + fn test_rewrite_other_golangci_lint_subcommand_skips_compact_wrapper() { + assert_eq!(rewrite_command("golangci-lint version", &[]), None); + } + // --- JS/TS tooling --- #[test] diff --git a/src/discover/rules.rs b/src/discover/rules.rs index 44f19d60..6a8ad3f0 100644 --- a/src/discover/rules.rs +++ b/src/discover/rules.rs @@ -43,7 +43,7 @@ pub const PATTERNS: &[&str] = &[ r"^(pip3?|uv\s+pip)\s+(list|outdated|install)", // Go tooling r"^go\s+(test|build|vet)", - r"^golangci-lint(\s|$)", + r"^(?:golangci-lint|golangci)\s+(run)(?:\s|$)", // Ruby tooling r"^bundle\s+(install|update)\b", r"^(?:bundle\s+exec\s+)?(?:bin/)?(?:rake|rails)\s+test", @@ -330,8 +330,8 @@ pub const RULES: &[RtkRule] = &[ subcmd_status: &[], }, RtkRule { - rtk_cmd: "rtk golangci-lint", - rewrite_prefixes: &["golangci-lint", "golangci"], + rtk_cmd: "rtk golangci-lint run", + rewrite_prefixes: &["golangci-lint run", "golangci run"], category: "Go", savings_pct: 85.0, subcmd_savings: &[], diff --git a/src/golangci_cmd.rs b/src/golangci_cmd.rs index b2fdcd28..076cb459 100644 --- a/src/golangci_cmd.rs +++ b/src/golangci_cmd.rs @@ -5,6 +5,41 @@ use anyhow::{Context, Result}; use serde::Deserialize; use std::collections::HashMap; +const GOLANGCI_SUBCOMMANDS: &[&str] = &[ + "cache", + "completion", + "config", + "custom", + "fmt", + "formatters", + "help", + "linters", + "migrate", + "run", + "version", +]; + +const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &[ + "-c", + "--color", + "--config", + "--cpu-profile-path", + "--mem-profile-path", + "--trace-path", +]; + +#[derive(Debug, PartialEq, Eq)] +struct RunInvocation { + global_args: Vec, + run_args: Vec, +} + +#[derive(Debug, PartialEq, Eq)] +enum Invocation { + FilteredRun(RunInvocation), + Passthrough, +} + #[derive(Debug, Deserialize)] struct Position { #[serde(rename = "Filename")] @@ -79,40 +114,27 @@ fn detect_major_version() -> u32 { } pub fn run(args: &[String], verbose: u8) -> Result<()> { + match classify_invocation(args) { + Invocation::FilteredRun(invocation) => run_filtered(args, &invocation, verbose), + Invocation::Passthrough => run_passthrough(args, verbose), + } +} + +fn run_filtered(original_args: &[String], invocation: &RunInvocation, verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); let version = detect_major_version(); let mut cmd = resolved_command("golangci-lint"); - - // Force JSON output (only if user hasn't specified it) - let has_format = args.iter().any(|a| { - a == "--out-format" - || a.starts_with("--out-format=") - || a == "--output.json.path" - || a.starts_with("--output.json.path=") - }); - - if !has_format { - if version >= 2 { - cmd.arg("run").arg("--output.json.path").arg("stdout"); - } else { - cmd.arg("run").arg("--out-format=json"); - } - } else { - cmd.arg("run"); - } - - for arg in args { + for arg in build_filtered_args(invocation, version) { cmd.arg(arg); } if verbose > 0 { - if version >= 2 { - eprintln!("Running: golangci-lint run --output.json.path stdout"); - } else { - eprintln!("Running: golangci-lint run --out-format=json"); - } + eprintln!( + "Running: {}", + format_command("golangci-lint", &build_filtered_args(invocation, version)) + ); } let output = cmd.output().context( @@ -140,8 +162,8 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> { } timer.track( - &format!("golangci-lint {}", args.join(" ")), - &format!("rtk golangci-lint {}", args.join(" ")), + &format_command("golangci-lint", original_args), + &format_command("rtk golangci-lint", original_args), &raw, &filtered, ); @@ -160,6 +182,127 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> { } } +fn run_passthrough(args: &[String], verbose: u8) -> Result<()> { + let timer = tracking::TimedExecution::start(); + + let mut cmd = resolved_command("golangci-lint"); + for arg in args { + cmd.arg(arg); + } + + if verbose > 0 { + eprintln!("Running: {}", format_command("golangci-lint", args)); + } + + let output = cmd + .output() + .with_context(|| format!("Failed to run {}", format_command("golangci-lint", args)))?; + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + let raw = format!("{}\n{}", stdout, stderr); + + print!("{}", stdout); + eprint!("{}", stderr); + + timer.track( + &format_command("golangci-lint", args), + &format_command("rtk golangci-lint", args), + &raw, + &raw, + ); + + if !output.status.success() { + std::process::exit(output.status.code().unwrap_or(1)); + } + + Ok(()) +} + +fn classify_invocation(args: &[String]) -> Invocation { + match find_subcommand_index(args) { + Some(idx) if args[idx] == "run" => Invocation::FilteredRun(RunInvocation { + global_args: args[..idx].to_vec(), + run_args: args[idx + 1..].to_vec(), + }), + _ => Invocation::Passthrough, + } +} + +fn find_subcommand_index(args: &[String]) -> Option { + let mut i = 0; + while i < args.len() { + let arg = args[i].as_str(); + + if arg == "--" { + return None; + } + + if !arg.starts_with('-') { + if GOLANGCI_SUBCOMMANDS.contains(&arg) { + return Some(i); + } + return None; + } + + if let Some(flag) = split_flag_name(arg) { + if GLOBAL_FLAGS_WITH_VALUE.contains(&flag) { + i += 1; + } + } + + i += 1; + } + + None +} + +fn split_flag_name(arg: &str) -> Option<&str> { + if arg.starts_with("--") { + return Some(arg.split_once('=').map(|(flag, _)| flag).unwrap_or(arg)); + } + + if arg.starts_with('-') { + return Some(arg); + } + + None +} + +fn build_filtered_args(invocation: &RunInvocation, version: u32) -> Vec { + let mut args = invocation.global_args.clone(); + args.push("run".to_string()); + + if !has_output_flag(&invocation.run_args) { + if version >= 2 { + args.push("--output.json.path".to_string()); + args.push("stdout".to_string()); + } else { + args.push("--out-format=json".to_string()); + } + } + + args.extend(invocation.run_args.clone()); + args +} + +fn has_output_flag(args: &[String]) -> bool { + args.iter().any(|a| { + a == "--out-format" + || a.starts_with("--out-format=") + || a == "--output.json.path" + || a.starts_with("--output.json.path=") + }) +} + +fn format_command(base: &str, args: &[String]) -> String { + if args.is_empty() { + base.to_string() + } else { + format!("{} {}", base, args.join(" ")) + } +} + /// Filter golangci-lint JSON output - group by linter and file fn filter_golangci_json(output: &str, version: u32) -> String { let result: Result = serde_json::from_str(output); @@ -367,6 +510,78 @@ mod tests { assert_eq!(parse_major_version("not a version string"), 1); } + #[test] + fn test_classify_invocation_run_uses_filtered_path() { + assert_eq!( + classify_invocation(&["run".into(), "./...".into()]), + Invocation::FilteredRun(RunInvocation { + global_args: vec![], + run_args: vec!["./...".into()], + }) + ); + } + + #[test] + fn test_classify_invocation_with_global_flag_value_uses_filtered_path() { + assert_eq!( + classify_invocation(&[ + "--color".into(), + "never".into(), + "run".into(), + "./...".into(), + ]), + Invocation::FilteredRun(RunInvocation { + global_args: vec!["--color".into(), "never".into()], + run_args: vec!["./...".into()], + }) + ); + } + + #[test] + fn test_classify_invocation_with_short_global_flag_uses_filtered_path() { + assert_eq!( + classify_invocation(&["-v".into(), "run".into(), "./...".into()]), + Invocation::FilteredRun(RunInvocation { + global_args: vec!["-v".into()], + run_args: vec!["./...".into()], + }) + ); + } + + #[test] + fn test_classify_invocation_bare_command_is_passthrough() { + assert_eq!(classify_invocation(&[]), Invocation::Passthrough); + } + + #[test] + fn test_classify_invocation_version_flag_is_passthrough() { + assert_eq!( + classify_invocation(&["--version".into()]), + Invocation::Passthrough + ); + } + + #[test] + fn test_classify_invocation_version_subcommand_is_passthrough() { + assert_eq!( + classify_invocation(&["version".into()]), + Invocation::Passthrough + ); + } + + #[test] + fn test_build_filtered_args_does_not_duplicate_run() { + let invocation = RunInvocation { + global_args: vec![], + run_args: vec!["./...".into()], + }; + + assert_eq!( + build_filtered_args(&invocation, 2), + vec!["run", "--output.json.path", "stdout", "./..."] + ); + } + #[test] fn test_filter_golangci_v2_fields_parse_cleanly() { // v2 JSON includes Severity, SourceLines, Offset — must not panic diff --git a/src/main.rs b/src/main.rs index 654a2676..25539c94 100644 --- a/src/main.rs +++ b/src/main.rs @@ -688,10 +688,10 @@ enum Commands { command: GtCommands, }, - /// golangci-lint with compact output + /// golangci-lint wrapper with compact `run` support and passthrough for other invocations #[command(name = "golangci-lint")] GolangciLint { - /// golangci-lint arguments + /// Additional golangci-lint arguments #[arg(trailing_var_arg = true, allow_hyphen_values = true)] args: Vec, }, From 59be480fae1cea7bfa6954751df7ddb3a773edf0 Mon Sep 17 00:00:00 2001 From: mgierok Date: Tue, 24 Mar 2026 19:08:34 +0100 Subject: [PATCH 2/3] fix(discover): preserve golangci-lint flags in rewrite Normalize golangci-lint global flags before run during classification and keep them in rewritten commands. Add regression coverage for classify_command and rewrite_command with pre-run global flags. --- src/discover/registry.rs | 180 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) diff --git a/src/discover/registry.rs b/src/discover/registry.rs index 497e6dc6..990bf43f 100644 --- a/src/discover/registry.rs +++ b/src/discover/registry.rs @@ -54,6 +54,21 @@ lazy_static! { Regex::new(r"^(?:(?:-C\s+\S+|-c\s+\S+|--git-dir(?:=\S+|\s+\S+)|--work-tree(?:=\S+|\s+\S+)|--no-pager|--no-optional-locks|--bare|--literal-pathspecs)\s+)+").unwrap(); } +const GOLANGCI_GLOBAL_OPT_WITH_VALUE: &[&str] = &[ + "-c", + "--color", + "--config", + "--cpu-profile-path", + "--mem-profile-path", + "--trace-path", +]; + +#[derive(Debug, Clone, Copy)] +struct GolangciRunParts<'a> { + global_segment: &'a str, + run_segment: &'a str, +} + /// Classify a single (already-split) command. pub fn classify_command(cmd: &str) -> Classification { let trimmed = cmd.trim(); @@ -84,6 +99,9 @@ pub fn classify_command(cmd: &str) -> Classification { let cmd_normalized = strip_absolute_path(cmd_clean); // Strip git global options: git -C /tmp status → git status (#163) let cmd_normalized = strip_git_global_opts(&cmd_normalized); + // Strip golangci-lint global options before `run` so classify/rewrite stays + // aligned with the runtime wrapper behavior. + let cmd_normalized = strip_golangci_global_opts(&cmd_normalized); let cmd_clean = cmd_normalized.as_str(); // Exclude cat/head/tail with redirect operators — these are writes, not reads (#315) @@ -285,6 +303,93 @@ fn strip_git_global_opts(cmd: &str) -> String { format!("git {}", stripped.trim()) } +/// Strip golangci-lint global options before the `run` subcommand. +/// `golangci-lint --color never run ./...` → `golangci-lint run ./...` +/// Returns the original string unchanged if this is not a supported compact `run` invocation. +fn strip_golangci_global_opts(cmd: &str) -> String { + match parse_golangci_run_parts(cmd) { + Some(parts) => format!("golangci-lint {}", parts.run_segment), + None => cmd.to_string(), + } +} + +/// Parse supported golangci-lint invocations with optional global flags before `run`. +fn parse_golangci_run_parts(cmd: &str) -> Option> { + let tokens = split_token_spans(cmd); + let first = tokens.first()?; + if first.0 != "golangci-lint" && first.0 != "golangci" { + return None; + } + + let mut i = 1; + while i < tokens.len() { + let token = tokens[i].0; + + if token == "--" { + return None; + } + + if !token.starts_with('-') { + if token == "run" { + let global_segment = if i > 1 { + cmd[tokens[1].1..tokens[i].1].trim() + } else { + "" + }; + let run_segment = cmd[tokens[i].1..].trim(); + return Some(GolangciRunParts { + global_segment, + run_segment, + }); + } + return None; + } + + if let Some(flag) = split_golangci_flag_name(token) { + if GOLANGCI_GLOBAL_OPT_WITH_VALUE.contains(&flag) { + i += 1; + } + } + + i += 1; + } + + None +} + +fn split_golangci_flag_name(arg: &str) -> Option<&str> { + if arg.starts_with("--") { + return Some(arg.split_once('=').map(|(flag, _)| flag).unwrap_or(arg)); + } + + if arg.starts_with('-') { + return Some(arg); + } + + None +} + +fn split_token_spans(cmd: &str) -> Vec<(&str, usize, usize)> { + let mut tokens = Vec::new(); + let mut start = None; + + for (idx, ch) in cmd.char_indices() { + if ch.is_whitespace() { + if let Some(token_start) = start.take() { + tokens.push((&cmd[token_start..idx], token_start, idx)); + } + } else if start.is_none() { + start = Some(idx); + } + } + + if let Some(token_start) = start { + tokens.push((&cmd[token_start..], token_start, cmd.len())); + } + + tokens +} + /// Normalize absolute binary paths: `/usr/bin/grep -rn foo` → `grep -rn foo` (#485) /// Only strips if the first word contains a `/` (Unix path). fn strip_absolute_path(cmd: &str) -> String { @@ -640,6 +745,18 @@ fn rewrite_segment(seg: &str, excluded: &[String]) -> Option { return None; } + if let Some(parts) = parse_golangci_run_parts(cmd_clean) { + let rewritten = if parts.global_segment.is_empty() { + format!("{}rtk golangci-lint {}", env_prefix, parts.run_segment) + } else { + format!( + "{}rtk golangci-lint {} {}", + env_prefix, parts.global_segment, parts.run_segment + ) + }; + return Some(rewritten); + } + // #196: gh with --json/--jq/--template produces structured output that // rtk gh would corrupt — skip rewrite so the caller gets raw JSON. if rule.rtk_cmd == "rtk gh" { @@ -1861,6 +1978,28 @@ mod tests { )); } + #[test] + fn test_classify_golangci_lint_with_flag_before_run() { + assert!(matches!( + classify_command("golangci-lint -v run ./..."), + Classification::Supported { + rtk_equivalent: "rtk golangci-lint run", + .. + } + )); + } + + #[test] + fn test_classify_golangci_lint_with_value_flag_before_run() { + assert!(matches!( + classify_command("golangci-lint --color never run ./..."), + Classification::Supported { + rtk_equivalent: "rtk golangci-lint run", + .. + } + )); + } + #[test] fn test_classify_golangci_lint_bare_is_not_compact_wrapper() { assert!(!matches!( @@ -1915,6 +2054,30 @@ mod tests { ); } + #[test] + fn test_rewrite_golangci_lint_with_flag_before_run() { + assert_eq!( + rewrite_command("golangci-lint -v run ./...", &[]), + Some("rtk golangci-lint -v run ./...".into()) + ); + } + + #[test] + fn test_rewrite_golangci_lint_with_value_flag_before_run() { + assert_eq!( + rewrite_command("golangci-lint --color never run ./...", &[]), + Some("rtk golangci-lint --color never run ./...".into()) + ); + } + + #[test] + fn test_rewrite_env_prefixed_golangci_lint_with_value_flag_before_run() { + assert_eq!( + rewrite_command("FOO=1 golangci-lint --color never run ./...", &[]), + Some("FOO=1 rtk golangci-lint --color never run ./...".into()) + ); + } + #[test] fn test_rewrite_bare_golangci_lint_skips_compact_wrapper() { assert_eq!(rewrite_command("golangci-lint", &[]), None); @@ -2357,4 +2520,21 @@ mod tests { assert_eq!(strip_git_global_opts("git status"), "git status"); assert_eq!(strip_git_global_opts("cargo test"), "cargo test"); } + + #[test] + fn test_strip_golangci_global_opts_helper() { + assert_eq!( + strip_golangci_global_opts("golangci-lint -v run ./..."), + "golangci-lint run ./..." + ); + assert_eq!( + strip_golangci_global_opts("golangci-lint --color never run ./..."), + "golangci-lint run ./..." + ); + assert_eq!( + strip_golangci_global_opts("golangci-lint version"), + "golangci-lint version" + ); + assert_eq!(strip_golangci_global_opts("cargo test"), "cargo test"); + } } From c0e8ea83e7f90d74aad16ca60c53c23ef35791e6 Mon Sep 17 00:00:00 2001 From: mgierok Date: Tue, 24 Mar 2026 19:17:46 +0100 Subject: [PATCH 3/3] fix(golangci-lint): support inline global flags before run Handle --flag=value forms consistently in both the runtime parser and discover rewrite logic. Add regression coverage for classify and rewrite paths using inline global flag values before run. --- src/discover/registry.rs | 68 +++++++++++++++++++++++++++++++++++++++- src/golangci_cmd.rs | 36 ++++++++++++++++++++- 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/src/discover/registry.rs b/src/discover/registry.rs index 990bf43f..9e036571 100644 --- a/src/discover/registry.rs +++ b/src/discover/registry.rs @@ -346,7 +346,7 @@ fn parse_golangci_run_parts(cmd: &str) -> Option> { } if let Some(flag) = split_golangci_flag_name(token) { - if GOLANGCI_GLOBAL_OPT_WITH_VALUE.contains(&flag) { + if golangci_flag_takes_separate_value(token, flag) { i += 1; } } @@ -369,6 +369,18 @@ fn split_golangci_flag_name(arg: &str) -> Option<&str> { None } +fn golangci_flag_takes_separate_value(arg: &str, flag: &str) -> bool { + if !GOLANGCI_GLOBAL_OPT_WITH_VALUE.contains(&flag) { + return false; + } + + if arg.starts_with("--") && arg.contains('=') { + return false; + } + + true +} + fn split_token_spans(cmd: &str) -> Vec<(&str, usize, usize)> { let mut tokens = Vec::new(); let mut start = None; @@ -2000,6 +2012,28 @@ mod tests { )); } + #[test] + fn test_classify_golangci_lint_with_inline_value_flag_before_run() { + assert!(matches!( + classify_command("golangci-lint --color=never run ./..."), + Classification::Supported { + rtk_equivalent: "rtk golangci-lint run", + .. + } + )); + } + + #[test] + fn test_classify_golangci_lint_with_inline_config_flag_before_run() { + assert!(matches!( + classify_command("golangci-lint --config=foo.yml run ./..."), + Classification::Supported { + rtk_equivalent: "rtk golangci-lint run", + .. + } + )); + } + #[test] fn test_classify_golangci_lint_bare_is_not_compact_wrapper() { assert!(!matches!( @@ -2070,6 +2104,22 @@ mod tests { ); } + #[test] + fn test_rewrite_golangci_lint_with_inline_value_flag_before_run() { + assert_eq!( + rewrite_command("golangci-lint --color=never run ./...", &[]), + Some("rtk golangci-lint --color=never run ./...".into()) + ); + } + + #[test] + fn test_rewrite_golangci_lint_with_inline_config_flag_before_run() { + assert_eq!( + rewrite_command("golangci-lint --config=foo.yml run ./...", &[]), + Some("rtk golangci-lint --config=foo.yml run ./...".into()) + ); + } + #[test] fn test_rewrite_env_prefixed_golangci_lint_with_value_flag_before_run() { assert_eq!( @@ -2078,6 +2128,14 @@ mod tests { ); } + #[test] + fn test_rewrite_env_prefixed_golangci_lint_with_inline_value_flag_before_run() { + assert_eq!( + rewrite_command("FOO=1 golangci-lint --color=never run ./...", &[]), + Some("FOO=1 rtk golangci-lint --color=never run ./...".into()) + ); + } + #[test] fn test_rewrite_bare_golangci_lint_skips_compact_wrapper() { assert_eq!(rewrite_command("golangci-lint", &[]), None); @@ -2531,6 +2589,14 @@ mod tests { strip_golangci_global_opts("golangci-lint --color never run ./..."), "golangci-lint run ./..." ); + assert_eq!( + strip_golangci_global_opts("golangci-lint --color=never run ./..."), + "golangci-lint run ./..." + ); + assert_eq!( + strip_golangci_global_opts("golangci-lint --config=foo.yml run ./..."), + "golangci-lint run ./..." + ); assert_eq!( strip_golangci_global_opts("golangci-lint version"), "golangci-lint version" diff --git a/src/golangci_cmd.rs b/src/golangci_cmd.rs index 076cb459..cc098854 100644 --- a/src/golangci_cmd.rs +++ b/src/golangci_cmd.rs @@ -246,7 +246,7 @@ fn find_subcommand_index(args: &[String]) -> Option { } if let Some(flag) = split_flag_name(arg) { - if GLOBAL_FLAGS_WITH_VALUE.contains(&flag) { + if golangci_flag_takes_separate_value(arg, flag) { i += 1; } } @@ -269,6 +269,18 @@ fn split_flag_name(arg: &str) -> Option<&str> { None } +fn golangci_flag_takes_separate_value(arg: &str, flag: &str) -> bool { + if !GLOBAL_FLAGS_WITH_VALUE.contains(&flag) { + return false; + } + + if arg.starts_with("--") && arg.contains('=') { + return false; + } + + true +} + fn build_filtered_args(invocation: &RunInvocation, version: u32) -> Vec { let mut args = invocation.global_args.clone(); args.push("run".to_string()); @@ -548,6 +560,28 @@ mod tests { ); } + #[test] + fn test_classify_invocation_with_inline_value_flag_uses_filtered_path() { + assert_eq!( + classify_invocation(&["--color=never".into(), "run".into(), "./...".into()]), + Invocation::FilteredRun(RunInvocation { + global_args: vec!["--color=never".into()], + run_args: vec!["./...".into()], + }) + ); + } + + #[test] + fn test_classify_invocation_with_inline_config_flag_uses_filtered_path() { + assert_eq!( + classify_invocation(&["--config=foo.yml".into(), "run".into(), "./...".into()]), + Invocation::FilteredRun(RunInvocation { + global_args: vec!["--config=foo.yml".into()], + run_args: vec!["./...".into()], + }) + ); + } + #[test] fn test_classify_invocation_bare_command_is_passthrough() { assert_eq!(classify_invocation(&[]), Invocation::Passthrough);