code-buster 0.11.0

Repository-aware static analysis for AI-assisted development
Documentation
use std::sync::LazyLock;

use regex::Regex;

use crate::findings::{Finding, Severity};
use crate::rule_catalog;
use crate::source_analysis_context::SourceAnalysisContext;

pub fn analyze(context: &SourceAnalysisContext<'_>) -> Vec<Finding> {
    let mut findings = Vec::new();
    for (path, source) in context
        .sources()
        .iter()
        .filter(|(path, _)| path.ends_with(".wren"))
    {
        analyze_file(path, source, &mut findings);
    }
    findings
}

fn analyze_file(path: &str, source: &str, findings: &mut Vec<Finding>) {
    let mut loop_depth = 0usize;
    for (index, raw) in source.split('\n').enumerate() {
        let code = DOUBLE_QUOTED.replace_all(raw, "");
        let line = code.trim();
        update_loop_depth(line, &mut loop_depth);
        analyze_line(path, raw, line, index + 1, loop_depth, findings);
    }
}

fn update_loop_depth(line: &str, loop_depth: &mut usize) {
    if line.starts_with("for (") || line.starts_with("while (") {
        *loop_depth += 1;
    }
    if line == "}" && *loop_depth > 0 {
        *loop_depth -= 1;
    }
}

fn analyze_line(
    path: &str,
    raw: &str,
    line: &str,
    line_number: usize,
    loop_depth: usize,
    findings: &mut Vec<Finding>,
) {
    report_system_print(path, line, line_number, findings);
    report_loop_output(path, line, line_number, loop_depth, findings);
    report_fiber_abort(path, line, line_number, findings);
    report_foreign_boundary(path, line, line_number, findings);
    report_fiber_call(path, line, line_number, findings);
    report_broad_import(path, line, line_number, findings);
    report_inheritance(path, line, line_number, findings);
    report_unchecked_number_parse(path, raw, line, line_number, findings);
}

fn report_system_print(path: &str, line: &str, line_number: usize, findings: &mut Vec<Finding>) {
    if line.contains("System.print(") {
        findings.push(finding(
            "wren-system-print",
            path,
            line_number,
            "System.print call left in code",
        ));
    }
}

fn report_loop_output(
    path: &str,
    line: &str,
    line_number: usize,
    loop_depth: usize,
    findings: &mut Vec<Finding>,
) {
    if loop_depth > 0 && (line.contains("System.print(") || line.contains("System.write(")) {
        findings.push(finding(
            "wren-print-in-loop",
            path,
            line_number,
            "console output inside loop",
        ));
    }
}

fn report_fiber_abort(path: &str, line: &str, line_number: usize, findings: &mut Vec<Finding>) {
    if line.contains("Fiber.abort(") {
        findings.push(finding(
            "wren-fiber-abort",
            path,
            line_number,
            "Fiber.abort terminates the current fiber",
        ));
    }
}

fn report_foreign_boundary(
    path: &str,
    line: &str,
    line_number: usize,
    findings: &mut Vec<Finding>,
) {
    if line.starts_with("foreign ") {
        findings.push(finding(
            "wren-foreign-boundary",
            path,
            line_number,
            "foreign API boundary declared",
        ));
    }
}

fn report_fiber_call(path: &str, line: &str, line_number: usize, findings: &mut Vec<Finding>) {
    if line.contains(".call(") && (line.contains("Fiber") || line.contains("fiber")) {
        findings.push(finding(
            "wren-fiber-call",
            path,
            line_number,
            "fiber resumed directly with call",
        ));
    }
}

fn report_broad_import(path: &str, line: &str, line_number: usize, findings: &mut Vec<Finding>) {
    if line.starts_with("import ") && line.contains(" for ") && line.matches(',').count() >= 8 {
        findings.push(finding(
            "wren-broad-import",
            path,
            line_number,
            "broad Wren import list",
        ));
    }
}

fn report_inheritance(path: &str, line: &str, line_number: usize, findings: &mut Vec<Finding>) {
    if line.starts_with("class ") && line.contains(" is ") {
        findings.push(finding(
            "wren-inheritance",
            path,
            line_number,
            "class inheritance used",
        ));
    }
}

fn report_unchecked_number_parse(
    path: &str,
    raw: &str,
    line: &str,
    line_number: usize,
    findings: &mut Vec<Finding>,
) {
    if line.contains("Num.fromString(") && !raw.to_ascii_lowercase().contains("null") {
        findings.push(finding(
            "wren-number-parse-unchecked",
            path,
            line_number,
            "Num.fromString result may be unchecked",
        ));
    }
}

fn finding(code: &str, path: &str, line: usize, message: &str) -> Finding {
    let metadata = rule_catalog::metadata(code).expect("Wren rule metadata must exist");
    Finding {
        code: code.to_owned(),
        severity: Severity::parse(metadata.default_severity)
            .expect("catalog severity must be valid"),
        path: path.to_owned(),
        line,
        end_line: line,
        message: message.to_owned(),
        confidence: "medium".to_owned(),
        why: metadata.why.to_owned(),
        suggestion: metadata.suggestion.to_owned(),
        related_files: Vec::new(),
        snippet: String::new(),
        code_flow: Vec::new(),
    }
}

static DOUBLE_QUOTED: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r#""(?:\\.|[^"\\])*""#).unwrap());

#[cfg(test)]
mod tests {
    use std::collections::{BTreeMap, BTreeSet};

    use super::analyze;
    use crate::source_analysis_context::SourceAnalysisContext;

    #[test]
    fn reports_every_wren_rule_with_original_contracts() {
        let sources = BTreeMap::from([(
            "main.wren".to_owned(),
            "import \"module\" for A, B, C, D, E, F, G, H, I\n\
class Main is Base {\n\
  foreign static nativeCall()\n\
  run(fiber) {\n\
    while (true) {\n\
      System.print(\"tick\")\n\
      System.write(\".\")\n\
    }\n\
    fiber.call()\n\
    var value = Num.fromString(input)\n\
    Fiber.abort(\"stop\")\n\
  }\n\
}"
            .to_owned(),
        )]);
        let context = SourceAnalysisContext::unchanged(&sources);

        let findings = analyze(&context);
        let codes = findings
            .iter()
            .map(|finding| finding.code.as_str())
            .collect::<BTreeSet<_>>();

        assert_eq!(
            codes,
            BTreeSet::from([
                "wren-broad-import",
                "wren-fiber-abort",
                "wren-fiber-call",
                "wren-foreign-boundary",
                "wren-inheritance",
                "wren-number-parse-unchecked",
                "wren-print-in-loop",
                "wren-system-print",
            ])
        );
        assert_eq!(
            findings
                .iter()
                .filter(|finding| finding.code == "wren-print-in-loop")
                .map(|finding| finding.line)
                .collect::<Vec<_>>(),
            vec![6, 7]
        );
        assert!(
            findings
                .iter()
                .all(|finding| finding.end_line == finding.line)
        );
    }

    #[test]
    fn ignores_patterns_inside_strings_and_accepts_same_line_null_checks() {
        let sources = BTreeMap::from([(
            "safe.wren".to_owned(),
            "var text = \"System.print( Fiber.abort( Num.fromString(\"\n\
var parsed = Num.fromString(input) // null is handled by the caller\n\
System.print(\"outside loop\")"
                .to_owned(),
        )]);
        let context = SourceAnalysisContext::unchanged(&sources);

        let findings = analyze(&context);

        assert_eq!(findings.len(), 1);
        assert_eq!(findings[0].code, "wren-system-print");
        assert_eq!(findings[0].line, 3);
    }
}