bctx-weave 0.1.29

bctx-weave — FilterMesh lens pipeline, CLI interception, domain compression
Documentation
use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;

// "Linting 'File.swift' (3/42)"
static PROGRESS_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"(?m)^Linting '[^']+' \(\d+/\d+\)\n?").unwrap());
// SwiftLint finding: "path/to/File.swift:10:5: warning: rule_name: message"
static FINDING_RE: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"^([^:]+\.swift:\d+:\d+): (warning|error): ([a-zA-Z_]+): (.+)$").unwrap()
});

pub fn compress_swiftlint(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let s = PROGRESS_RE.replace_all(&cleaned, "");

    if s.trim().is_empty() {
        return "swiftlint: no issues".to_string();
    }

    let mut by_rule: std::collections::HashMap<String, (Vec<String>, bool)> =
        std::collections::HashMap::new(); // rule -> (locations, is_error)
    let mut other_lines: Vec<&str> = Vec::new();

    for line in s.lines() {
        let t = line.trim();
        if t.is_empty() {
            continue;
        }
        if let Some(caps) = FINDING_RE.captures(t) {
            let loc = caps[1].to_string();
            let is_error = &caps[2] == "error";
            let rule = caps[3].to_string();
            let entry = by_rule.entry(rule).or_insert((Vec::new(), false));
            entry.0.push(loc);
            entry.1 |= is_error;
            continue;
        }
        // Summary lines
        if t.starts_with("Done") || t.contains("violations") || t.contains("error") {
            other_lines.push(line);
        }
    }

    if by_rule.is_empty() {
        return compactor::collapse_blanks(&s);
    }

    let total: usize = by_rule.values().map(|(v, _)| v.len()).sum();
    let error_count: usize = by_rule
        .values()
        .filter(|(_, is_err)| *is_err)
        .map(|(v, _)| v.len())
        .sum();

    let mut rules: Vec<(&String, &(Vec<String>, bool))> = by_rule.iter().collect();
    // Errors first, then warnings, each sorted by name
    rules.sort_by(|(a_name, (_, a_err)), (b_name, (_, b_err))| {
        b_err.cmp(a_err).then(a_name.cmp(b_name))
    });

    let mut result: Vec<String> = Vec::new();
    for (rule, (locs, is_error)) in &rules {
        let level = if *is_error { "error" } else { "warning" };
        if locs.len() == 1 {
            result.push(format!("{level}: {rule}{}", locs[0]));
        } else {
            result.push(format!("{level}: {rule}{}) — {}", locs.len(), locs[0]));
        }
    }
    result.extend(other_lines.iter().map(|l| l.to_string()));
    result.push(format!(
        "swiftlint: {total} issues ({error_count} errors, {} warnings)",
        total - error_count
    ));
    result.join("\n")
}

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

    #[test]
    fn strips_progress_lines() {
        let raw = "Linting 'Foo.swift' (1/5)\nLinting 'Bar.swift' (2/5)\nSources/Foo.swift:10:5: warning: line_length: Line should be 120 characters or less.\n";
        let out = compress_swiftlint(&raw);
        assert!(!out.contains("Linting 'Foo.swift'"), "{out}");
        assert!(
            out.contains("line_length") || out.contains("warning"),
            "{out}"
        );
    }

    #[test]
    fn groups_by_rule() {
        let raw = "Foo.swift:10:5: warning: trailing_whitespace: Trailing Whitespace Violation.\nBar.swift:20:1: warning: trailing_whitespace: Trailing Whitespace Violation.\nBaz.swift:5:3: error: force_cast: Force Cast Violation.\n";
        let out = compress_swiftlint(&raw);
        assert!(out.contains("trailing_whitespace"), "{out}");
        assert!(out.contains("×2") || out.contains("force_cast"), "{out}");
    }

    #[test]
    fn errors_shown_before_warnings() {
        let raw = "Foo.swift:1:1: warning: line_length: too long.\nBar.swift:2:2: error: force_cast: bad cast.\n";
        let out = compress_swiftlint(&raw);
        let err_pos = out.find("force_cast").unwrap_or(usize::MAX);
        let warn_pos = out.find("line_length").unwrap_or(usize::MAX);
        assert!(
            err_pos < warn_pos,
            "errors should come before warnings: {out}"
        );
    }
}