prx 0.5.9

Praxis — agent-native Unix tools. Single binary replacing grep, cat, find, sed, diff for AI coding agents.
use super::{Diagnostic, ParsedResult, define_regex};

define_regex!(
    ISSUE_RE,
    r"^(.+?\.pyi?):(\d+)(?::(\d+))?: (error|warning|note): (.+)$"
);
define_regex!(SUMMARY_RE, r"^Found (\d+) errors? in (\d+) files?");
define_regex!(SUCCESS_RE, r"^Success: no issues");

pub fn parse(output: &str) -> ParsedResult {
    let mut failures = Vec::new();
    let mut warnings = Vec::new();
    let mut summary = String::new();

    for line in output.lines() {
        if let Some(caps) = ISSUE_RE.captures(line) {
            let severity = &caps[4];
            if severity == "note" {
                continue;
            }
            let location = if let Some(col) = caps.get(3) {
                format!("{}:{}:{}", &caps[1], &caps[2], col.as_str())
            } else {
                format!("{}:{}", &caps[1], &caps[2])
            };

            let diag = Diagnostic {
                name: format!("mypy/{severity}"),
                location: Some(location),
                message: caps[5].to_string(),
            };

            if severity == "error" {
                failures.push(diag);
            } else {
                warnings.push(diag);
            }
            continue;
        }

        if let Some(caps) = SUMMARY_RE.captures(line) {
            summary = format!(
                "{} error(s) in {} file(s)",
                &caps[1].to_string(),
                &caps[2].to_string()
            );
        } else if SUCCESS_RE.is_match(line) {
            summary = "no issues found".to_string();
        }
    }

    if summary.is_empty() {
        summary = format!("{} error(s), {} warning(s)", failures.len(), warnings.len());
    }

    ParsedResult {
        summary,
        passed: 0,
        failed: failures.len(),
        skipped: 0,
        failures,
        warnings,
        tail: None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_mypy_errors() {
        let output = "\
src/auth.py:42: error: Incompatible return value type (got \"str\", expected \"int\")
src/auth.py:55: error: Name \"foo\" is not defined
Found 2 errors in 1 file (checked 5 source files)
";
        let result = parse(output);
        assert_eq!(result.failures.len(), 2);
        assert_eq!(result.failed, 2);
        assert_eq!(
            result.failures[0].location.as_deref(),
            Some("src/auth.py:42")
        );
        assert!(result.summary.contains("2 error"));
    }

    #[test]
    fn parse_mypy_success() {
        let output = "Success: no issues found in 5 source files\n";
        let result = parse(output);
        assert_eq!(result.failed, 0);
        assert!(result.summary.contains("no issues"));
    }

    #[test]
    fn parse_mypy_ignores_notes() {
        let output = "\
src/auth.py:42: error: Incompatible types
src/auth.py:42: note: Expected: int
";
        let result = parse(output);
        assert_eq!(result.failures.len(), 1);
        assert_eq!(result.warnings.len(), 0);
    }
}