code-buster 0.11.0

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

use crate::analysis_cache::AnalysisCache;

use regex::Regex;
use serde::Serialize;
use serde_json::{Value, json};

use crate::cli::{ReportFormat, RepositoryViewOptions};
use crate::config::{Config, absolute_root};
use crate::discovery::{classify_target, prepare};
use crate::function_sources;
use crate::summary_command;

pub enum RepositoryViewOutcome {
    Success,
    Findings,
    NotFound,
    Usage,
}

#[derive(Serialize)]
struct RelatedReport<'a> {
    path: &'a str,
    depends_on: &'a [String],
    depended_on_by: &'a [String],
}

#[derive(Serialize)]
struct WhyReport<'a> {
    target: &'a str,
    reachable: bool,
    path: &'a [String],
}

#[derive(Serialize)]
struct PathReport<'a> {
    from: &'a str,
    to: &'a str,
    path: &'a [String],
}

fn included_by_scope(classification: &str, config: &Config) -> bool {
    classification != "generated"
        && (config.include_all
            || classification == "production"
            || (classification == "test" && config.include_tests)
            || (classification == "example" && config.include_examples)
            || (classification == "vendored" && config.include_vendored))
}

fn print_excluded_inspection(
    path: &str,
    classification: &str,
    classification_source: &str,
    matched: Option<&str>,
    json_output: bool,
) -> Result<(), String> {
    if json_output {
        let mut report = json!({
            "path": path,
            "classification": classification,
            "classification_source": classification_source,
            "included": false,
            "reason": "excluded by the default production scope",
            "override": scope_override(classification),
        });
        if let Some(matched) = matched {
            report["matched"] = json!(matched);
        }
        println!(
            "{}",
            serde_json::to_string(&report)
                .map_err(|error| format!("failed to encode inspect report: {error}"))?
        );
    } else {
        println!("path={path}");
        println!("classification={classification}");
        println!("classification_source={classification_source}");
        if let Some(pattern) = matched {
            println!("matched={pattern}");
        }
        println!("included=false");
        println!("reason=excluded by the default production scope");
    }
    Ok(())
}

fn scope_override(classification: &str) -> &'static str {
    match classification {
        "test" => "--include-tests",
        "example" => "--include-examples",
        "vendored" => "--include-vendored",
        _ => "--all",
    }
}

pub fn execute_inspect(options: &RepositoryViewOptions) -> Result<RepositoryViewOutcome, String> {
    let Some(target) = options.targets.first().filter(|target| !target.is_empty()) else {
        eprintln!("inspect requires a project-relative source path");
        return Ok(RepositoryViewOutcome::Usage);
    };
    let normalized = normalize(target);
    let root = absolute_root(&options.common.root)?;
    let config = Config::load(root.clone(), &options.common)?;
    let classification = classify_target(&root, &normalized, &config)?;
    let included = included_by_scope(classification.classification, &config);
    let target_path = root.join(normalized.replace('/', std::path::MAIN_SEPARATOR_STR));
    if target_path.is_file() && !included {
        print_excluded_inspection(
            &normalized,
            classification.classification,
            classification.source,
            classification.matched.as_deref(),
            matches!(options.common.format, ReportFormat::Json),
        )?;
        return Ok(RepositoryViewOutcome::Success);
    }

    let run = summary_command::analyze(&options.common)?;
    let Some(source) = run.prepared.sources.get(&normalized) else {
        eprintln!("target is not a discovered source file: {target}");
        return Ok(RepositoryViewOutcome::NotFound);
    };
    let mut findings = run
        .findings
        .iter()
        .filter(|finding| finding.path == normalized)
        .collect::<Vec<_>>();
    findings.sort_by_key(|finding| {
        !matches!(
            finding.code.as_str(),
            "tab-indent" | "trailing-whitespace" | "long-line"
        )
    });
    if matches!(options.common.format, ReportFormat::Json) {
        let Some(file) = run
            .prepared
            .files
            .iter()
            .find(|file| file.path == normalized)
        else {
            return Err(format!("missing discovered file metadata for {normalized}"));
        };
        let mut cache = AnalysisCache::new(&options.common, Path::new(&run.prepared.root));
        let graph = cache.graph(&run.prepared);
        let mut imported_by = graph
            .nodes()
            .filter(|node| {
                graph
                    .dependencies(node)
                    .any(|dependency| dependency == normalized)
            })
            .map(str::to_owned)
            .collect::<Vec<_>>();
        imported_by.sort_unstable();
        let (imports, exports, functions) = if file.language == "dart" {
            dart_inspection(&normalized, source)
        } else {
            (
                graph.dependencies(&normalized).map(str::to_owned).collect(),
                Vec::new(),
                Vec::new(),
            )
        };
        let flags = findings
            .iter()
            .filter(|finding| finding.code == "feature-flag")
            .map(|finding| {
                finding
                    .message
                    .rsplit_once(": ")
                    .map_or(finding.message.as_str(), |(_, value)| value)
            })
            .collect::<Vec<_>>();
        let finding_lines = findings
            .iter()
            .map(|finding| format!("{}:{} {}", finding.code, finding.line, finding.message))
            .collect::<Vec<_>>();
        let mut report = json!({
            "path": normalized,
            "classification": classification.classification,
            "classification_source": classification.source,
            "included": true,
            "language": file.language,
            "lines": source.split('\n').count(),
            "imports": imports,
            "imported_by": imported_by,
            "exports": exports,
            "flags": flags,
            "functions": functions,
            "findings": finding_lines,
        });
        if let Some(matched) = &classification.matched {
            report["matched"] = json!(matched);
        }
        println!(
            "{}",
            serde_json::to_string(&report)
                .map_err(|error| format!("failed to encode inspect report: {error}"))?
        );
    } else {
        println!("classification={}", classification.classification);
        println!("classification_source={}", classification.source);
        if let Some(pattern) = classification.matched {
            println!("matched={pattern}");
        }
        println!("included=true");
        println!(
            "Code Buster inspect: 1 files, {} actionable findings",
            findings.len()
        );
        for finding in &findings {
            let location = if finding.end_line > finding.line {
                format!("{}:{}-{}", finding.path, finding.line, finding.end_line)
            } else {
                format!("{}:{}", finding.path, finding.line)
            };
            println!(
                "{} {} {location} {}",
                finding.severity.as_str(),
                finding.code,
                finding.message
            );
        }
    }
    Ok(inspect_outcome(
        options.common.fail_on_issues,
        findings.is_empty(),
    ))
}

fn inspect_outcome(fail_on_issues: bool, findings_empty: bool) -> RepositoryViewOutcome {
    if fail_on_issues && !findings_empty {
        RepositoryViewOutcome::Findings
    } else {
        RepositoryViewOutcome::Success
    }
}

fn dart_inspection(path: &str, source: &str) -> (Vec<String>, Vec<String>, Vec<Value>) {
    let imports = DART_DIRECTIVE
        .captures_iter(source)
        .filter_map(|captures| captures.get(1).map(|value| value.as_str().to_owned()))
        .collect();
    let exports = DART_PUBLIC_DECLARATION
        .captures_iter(source)
        .filter_map(|captures| captures.get(1).map(|value| value.as_str()))
        .filter(|name| !name.starts_with('_'))
        .map(str::to_owned)
        .collect();
    let functions = function_sources::extract(&std::collections::BTreeMap::from([(
        path.to_owned(),
        source.to_owned(),
    )]))
    .into_iter()
    .map(|function| json!({"name": function.name, "line": function.line}))
    .collect();
    (imports, exports, functions)
}

pub fn execute_related(options: &RepositoryViewOptions) -> Result<RepositoryViewOutcome, String> {
    let Some(target) = options.targets.first().filter(|target| !target.is_empty()) else {
        eprintln!("related requires a project-relative source path");
        return Ok(RepositoryViewOutcome::Usage);
    };
    let normalized = normalize(target);
    let prepared = prepare(&options.common)?;
    let mut cache = AnalysisCache::new(&options.common, Path::new(&prepared.root));
    let graph = cache.graph(&prepared);
    if !graph.contains(&normalized) {
        eprintln!("target is not a discovered source file: {target}");
        return Ok(RepositoryViewOutcome::NotFound);
    }

    let depends_on = graph
        .dependencies(&normalized)
        .map(str::to_owned)
        .collect::<Vec<_>>();
    let depended_on_by = graph
        .nodes()
        .filter(|node| {
            graph
                .dependencies(node)
                .any(|dependency| dependency == normalized)
        })
        .map(str::to_owned)
        .collect::<Vec<_>>();
    if matches!(options.common.format, ReportFormat::Json) {
        println!(
            "{}",
            serde_json::to_string(&RelatedReport {
                path: &normalized,
                depends_on: &depends_on,
                depended_on_by: &depended_on_by,
            })
            .map_err(|error| format!("failed to encode related report: {error}"))?
        );
    } else {
        println!("Code Buster related: {normalized}");
        let mut related = depends_on
            .iter()
            .chain(&depended_on_by)
            .map(String::as_str)
            .collect::<Vec<_>>();
        related.sort_unstable();
        related.dedup();
        for path in related {
            println!("{path}");
        }
    }
    Ok(RepositoryViewOutcome::Success)
}

pub fn execute_why(options: &RepositoryViewOptions) -> Result<RepositoryViewOutcome, String> {
    let Some(target) = options.targets.first().filter(|target| !target.is_empty()) else {
        eprintln!("why requires a project-relative source path");
        return Ok(RepositoryViewOutcome::Usage);
    };
    let normalized = normalize(target);
    let prepared = prepare(&options.common)?;
    let mut cache = AnalysisCache::new(&options.common, Path::new(&prepared.root));
    let graph = cache.graph(&prepared);
    let mut selected = Vec::new();
    for root in graph.default_roots(&prepared.config.entry_points) {
        let candidate = graph.shortest_path(root, &normalized);
        if !candidate.is_empty() && (selected.is_empty() || candidate.len() < selected.len()) {
            selected = candidate;
        }
    }
    if selected.is_empty() {
        eprintln!("no dependency path reaches: {target}");
        return Ok(RepositoryViewOutcome::NotFound);
    }

    if matches!(options.common.format, ReportFormat::Json) {
        println!(
            "{}",
            serde_json::to_string(&WhyReport {
                target: &normalized,
                reachable: true,
                path: &selected,
            })
            .map_err(|error| format!("failed to encode why report: {error}"))?
        );
    } else {
        println!("{}", selected.join(" -> "));
    }
    Ok(RepositoryViewOutcome::Success)
}

pub fn execute_path(options: &RepositoryViewOptions) -> Result<RepositoryViewOutcome, String> {
    if options.targets.len() != 2 {
        eprintln!("path requires FROM and TO project-relative source paths");
        return Ok(RepositoryViewOutcome::Usage);
    }
    let from = normalize(&options.targets[0]);
    let to = normalize(&options.targets[1]);
    let prepared = prepare(&options.common)?;
    let mut cache = AnalysisCache::new(&options.common, Path::new(&prepared.root));
    let graph = cache.graph(&prepared);
    let path = graph.shortest_path(&from, &to);
    if path.is_empty() {
        eprintln!("no dependency path from {from} to {to}");
        return Ok(RepositoryViewOutcome::NotFound);
    }

    if matches!(options.common.format, ReportFormat::Json) {
        println!(
            "{}",
            serde_json::to_string(&PathReport {
                from: &from,
                to: &to,
                path: &path,
            })
            .map_err(|error| format!("failed to encode path report: {error}"))?
        );
    } else {
        println!("{}", path.join(" -> "));
    }
    Ok(RepositoryViewOutcome::Success)
}

fn normalize(path: &str) -> String {
    path.replace('\\', "/")
}

static DART_DIRECTIVE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r#"(?m)^\s*(?:import|export)\s+['"]([^'"]+)['"]"#)
        .expect("Dart directive regex must compile")
});
static DART_PUBLIC_DECLARATION: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(
        r"(?m)^(?:(?:abstract|base|final|interface|sealed)\s+)*(?:(?:class|enum|mixin|extension|typedef)\s+|(?:[A-Za-z]\w*(?:<[^>\r\n]+>)?\??|void)\s+)([A-Za-z]\w*)",
    )
    .expect("Dart public declaration regex must compile")
});