Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .claude/hooks/rtk-rewrite.sh
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ elif echo "$MATCH_CMD" | grep -qE '^pip[[:space:]]+(list|outdated|install|show)(
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^pip /rtk pip /')"
elif echo "$MATCH_CMD" | grep -qE '^uv[[:space:]]+pip[[:space:]]+(list|outdated|install|show)([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^uv pip /rtk pip /')"
elif echo "$MATCH_CMD" | grep -qE '^mypy([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^mypy/rtk mypy/')"
elif echo "$MATCH_CMD" | grep -qE '^python[[:space:]]+-m[[:space:]]+mypy([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^python -m mypy/rtk mypy/')"

# --- Go tooling ---
elif echo "$MATCH_CMD" | grep -qE '^go[[:space:]]+test([[:space:]]|$)'; then
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ rtk gain --history | grep proxy
| pnpm_cmd.rs | pnpm package manager | Compact dependency trees (70-90% reduction) |
| ruff_cmd.rs | Ruff linter/formatter | JSON for check, text for format (80%+ reduction) |
| pytest_cmd.rs | Pytest test runner | State machine text parser (90%+ reduction) |
| mypy_cmd.rs | Mypy type checker | Group by file/error code (80% reduction) |
| pip_cmd.rs | pip/uv package manager | JSON parsing, auto-detect uv (70-85% reduction) |
| go_cmd.rs | Go commands | NDJSON for test, text for build/vet (80-90% reduction) |
| golangci_cmd.rs | golangci-lint | JSON parsing, group by rule (85% reduction) |
Expand Down
4 changes: 4 additions & 0 deletions hooks/rtk-rewrite.sh
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ elif echo "$MATCH_CMD" | grep -qE '^pip[[:space:]]+(list|outdated|install|show)(
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^pip /rtk pip /')"
elif echo "$MATCH_CMD" | grep -qE '^uv[[:space:]]+pip[[:space:]]+(list|outdated|install|show)([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^uv pip /rtk pip /')"
elif echo "$MATCH_CMD" | grep -qE '^mypy([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^mypy/rtk mypy/')"
elif echo "$MATCH_CMD" | grep -qE '^python[[:space:]]+-m[[:space:]]+mypy([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^python -m mypy/rtk mypy/')"

# --- Go tooling ---
elif echo "$MATCH_CMD" | grep -qE '^go[[:space:]]+test([[:space:]]|$)'; then
Expand Down
34 changes: 34 additions & 0 deletions src/discover/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const PATTERNS: &[&str] = &[
r"^kubectl\s+(get|logs)",
r"^curl\s+",
r"^wget\s+",
r"^(python3?\s+-m\s+)?mypy(\s|$)",
];

const RULES: &[RtkRule] = &[
Expand Down Expand Up @@ -225,6 +226,13 @@ const RULES: &[RtkRule] = &[
subcmd_savings: &[],
subcmd_status: &[],
},
RtkRule {
rtk_cmd: "rtk mypy",
category: "Build",
savings_pct: 80.0,
subcmd_savings: &[],
subcmd_status: &[],
},
];

/// Commands to ignore (shell builtins, trivial, already rtk).
Expand Down Expand Up @@ -732,4 +740,30 @@ mod tests {
let cmd = "cat <<'EOF'\nhello && world\nEOF";
assert_eq!(split_command_chain(cmd), vec![cmd]);
}

#[test]
fn test_classify_mypy() {
assert_eq!(
classify_command("mypy src/"),
Classification::Supported {
rtk_equivalent: "rtk mypy",
category: "Build",
estimated_savings_pct: 80.0,
status: RtkStatus::Existing,
}
);
}

#[test]
fn test_classify_python_m_mypy() {
assert_eq!(
classify_command("python3 -m mypy --strict"),
Classification::Supported {
rtk_equivalent: "rtk mypy",
category: "Build",
estimated_savings_pct: 80.0,
status: RtkStatus::Existing,
}
);
}
}
148 changes: 2 additions & 146 deletions src/lint_cmd.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::mypy_cmd;
use crate::ruff_cmd;
use crate::tracking;
use crate::utils::{package_manager_exec, truncate};
Expand Down Expand Up @@ -169,7 +170,7 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> {
}
}
"pylint" => filter_pylint_json(&stdout),
"mypy" => filter_mypy_output(&raw),
"mypy" => mypy_cmd::filter_mypy_output(&raw),
_ => filter_generic_lint(&raw),
};

Expand Down Expand Up @@ -402,124 +403,6 @@ fn filter_pylint_json(output: &str) -> String {
result.trim().to_string()
}

/// Filter mypy text output - parse and group by error code and file
fn filter_mypy_output(output: &str) -> String {
// Regex pattern: path/to/file.py:line: error: message [error-code]
let re = Regex::new(r"^(.+\.py):(\d+): (error|warning|note): (.+?) \[(.+?)\]").unwrap();

let mut issues: Vec<(String, String, String, String)> = Vec::new(); // (file, line, level, code)
let mut errors = 0;
let mut warnings = 0;
let mut notes = 0;

for line in output.lines() {
if let Some(caps) = re.captures(line) {
let file = caps.get(1).map_or("", |m| m.as_str());
let line_num = caps.get(2).map_or("", |m| m.as_str());
let level = caps.get(3).map_or("", |m| m.as_str());
let code = caps.get(5).map_or("", |m| m.as_str());

match level {
"error" => errors += 1,
"warning" => warnings += 1,
"note" => notes += 1,
_ => {}
}

issues.push((
file.to_string(),
line_num.to_string(),
level.to_string(),
code.to_string(),
));
}
}

if issues.is_empty() {
// Check if mypy output contains "Success" or similar
if output.contains("Success") || output.trim().is_empty() {
return "✓ Mypy: No issues found".to_string();
}
// Fallback to generic output if no regex matches
return format!("Mypy output:\n{}", truncate(output, 500));
}

// Count unique files
let unique_files: std::collections::HashSet<_> = issues.iter().map(|(f, _, _, _)| f).collect();
let total_files = unique_files.len();

// Group by error code
let mut by_code: HashMap<String, usize> = HashMap::new();
for (_, _, _, code) in &issues {
*by_code.entry(code.clone()).or_insert(0) += 1;
}

// Group by file
let mut by_file: HashMap<&str, usize> = HashMap::new();
for (file, _, _, _) in &issues {
*by_file.entry(file.as_str()).or_insert(0) += 1;
}

let mut file_counts: Vec<_> = by_file.iter().collect();
file_counts.sort_by(|a, b| b.1.cmp(a.1));

// Build output
let mut result = String::new();
result.push_str(&format!(
"Mypy: {} issues in {} files\n",
issues.len(),
total_files
));

if errors > 0 || warnings > 0 {
result.push_str(&format!(" {} errors, {} warnings", errors, warnings));
if notes > 0 {
result.push_str(&format!(", {} notes", notes));
}
result.push('\n');
}

result.push_str("═══════════════════════════════════════\n");

// Show top error codes
let mut code_counts: Vec<_> = by_code.iter().collect();
code_counts.sort_by(|a, b| b.1.cmp(a.1));

if !code_counts.is_empty() {
result.push_str("Top error codes:\n");
for (code, count) in code_counts.iter().take(10) {
result.push_str(&format!(" {} ({}x)\n", code, count));
}
result.push('\n');
}

// Show top files
result.push_str("Top files:\n");
for (file, count) in file_counts.iter().take(10) {
let short_path = compact_path(file);
result.push_str(&format!(" {} ({} issues)\n", short_path, count));

// Show top 3 error codes in this file
let mut file_codes: HashMap<String, usize> = HashMap::new();
for (_f, _, _, code) in issues.iter().filter(|(f, _, _, _)| f == *file) {
*file_codes.entry(code.clone()).or_insert(0) += 1;
}

let mut file_code_counts: Vec<_> = file_codes.iter().collect();
file_code_counts.sort_by(|a, b| b.1.cmp(a.1));

for (code, count) in file_code_counts.iter().take(3) {
result.push_str(&format!(" {} ({})\n", code, count));
}
}

if file_counts.len() > 10 {
result.push_str(&format!("\n... +{} more files\n", file_counts.len() - 10));
}

result.trim().to_string()
}

/// Filter generic linter output (fallback for non-ESLint linters)
fn filter_generic_lint(output: &str) -> String {
let mut warnings = 0;
Expand Down Expand Up @@ -693,33 +576,6 @@ mod tests {
assert!(result.contains("utils.py"));
}

#[test]
fn test_filter_mypy_no_issues() {
let output = "Success: no issues found in 5 source files";
let result = filter_mypy_output(output);
assert!(result.contains("✓ Mypy"));
assert!(result.contains("No issues found"));
}

#[test]
fn test_filter_mypy_with_errors() {
let output = r#"src/main.py:10: error: Incompatible return value type [return-value]
src/main.py:15: error: Argument 1 has incompatible type "str"; expected "int" [arg-type]
src/utils.py:20: error: Name "foo" is not defined [name-defined]
src/utils.py:25: warning: Unused "type: ignore" comment [unused-ignore]
Found 4 errors in 2 files (checked 5 source files)"#;

let result = filter_mypy_output(output);
assert!(result.contains("4 issues"));
assert!(result.contains("2 files"));
assert!(result.contains("3 errors, 1 warnings"));
assert!(result.contains("return-value"));
assert!(result.contains("arg-type"));
assert!(result.contains("name-defined"));
assert!(result.contains("main.py"));
assert!(result.contains("utils.py"));
}

#[test]
fn test_is_python_linter() {
assert!(is_python_linter("ruff"));
Expand Down
12 changes: 12 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ mod lint_cmd;
mod local_llm;
mod log_cmd;
mod ls;
mod mypy_cmd;
mod next_cmd;
mod npm_cmd;
mod parser;
Expand Down Expand Up @@ -498,6 +499,13 @@ enum Commands {
args: Vec<String>,
},

/// Mypy type checker with grouped error output
Mypy {
/// Mypy arguments
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},

/// Pip package manager with compact output (auto-detects uv)
Pip {
/// Pip arguments (e.g., list, outdated, install)
Expand Down Expand Up @@ -1383,6 +1391,10 @@ fn main() -> Result<()> {
pytest_cmd::run(&args, cli.verbose)?;
}

Commands::Mypy { args } => {
mypy_cmd::run(&args, cli.verbose)?;
}

Commands::Pip { args } => {
pip_cmd::run(&args, cli.verbose)?;
}
Expand Down
Loading