prview 0.6.0

PR Review & Artifact Generator - cross-language PR analysis tool
Documentation
use super::LintFinding;
use regex::Regex;
use std::sync::LazyLock;

static FINDING_RE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"^\s+(\d+):(\d+)\s+([✖⚠ℹ×‼i])\s+(.+?)(?:\s{2,}(\S[\S-]*))?\s*$").unwrap()
});

fn symbol_to_level(symbol: &str) -> &'static str {
    match symbol {
        "" | "×" => "error",
        "" | "" => "warning",
        "" | "i" => "note",
        _ => "warning",
    }
}

pub fn parse_stylelint_output(output: &str) -> Vec<LintFinding> {
    let mut findings = Vec::new();
    let mut current_file: Option<String> = None;

    for line in output.lines() {
        let trimmed = line.trim_end();
        if trimmed.is_empty() {
            continue;
        }

        if let Some(caps) = FINDING_RE.captures(trimmed) {
            let Some(file) = current_file.as_ref() else {
                continue;
            };

            findings.push(LintFinding {
                file: file.clone(),
                line: caps[1].parse().unwrap_or_default(),
                column: caps[2].parse().ok(),
                level: symbol_to_level(&caps[3]),
                message: caps[4].trim().to_string(),
                rule_id: caps
                    .get(5)
                    .map(|m: regex::Match<'_>| m.as_str().to_string()),
                source: "stylelint",
            });
            continue;
        }

        if !line.starts_with(' ') && !line.starts_with('\t') {
            current_file = Some(trimmed.to_string());
        }
    }

    findings
}

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

    #[test]
    fn parses_error_symbol() {
        let output = "src/style.css\n  10:5  ✖  Unexpected unit  unit-disallowed-list\n";
        let findings = parse_stylelint_output(output);
        assert_eq!(findings.len(), 1);
        assert_eq!(findings[0].level, "error");
        assert_eq!(findings[0].rule_id.as_deref(), Some("unit-disallowed-list"));
    }

    #[test]
    fn parses_warning_symbol() {
        let output = "src/a.css\n  2:1  ⚠  Expected indentation  indentation\n";
        let findings = parse_stylelint_output(output);
        assert_eq!(findings.len(), 1);
        assert_eq!(findings[0].level, "warning");
    }

    #[test]
    fn parses_css_syntax_error_without_rule_id() {
        let output = "src/bad.css\n  1:1  ✖  Unknown word (CssSyntaxError)\n";
        let findings = parse_stylelint_output(output);
        assert_eq!(findings.len(), 1);
        assert_eq!(findings[0].level, "error");
        assert_eq!(findings[0].rule_id, None);
        assert!(findings[0].message.contains("CssSyntaxError"));
    }

    #[test]
    fn parses_multiple_files() {
        let output = "a.css\n  1:1  ✖  msg1  rule1\n\nb.css\n  3:2  ⚠  msg2  rule2\n";
        let findings = parse_stylelint_output(output);
        assert_eq!(findings.len(), 2);
        assert_eq!(findings[0].file, "a.css");
        assert_eq!(findings[1].file, "b.css");
    }

    #[test]
    fn returns_empty_for_empty_output() {
        assert!(parse_stylelint_output("").is_empty());
    }
}