code-buster 0.11.0

Repository-aware static analysis for AI-assisted development
Documentation
use std::collections::{BTreeMap, BTreeSet};
use std::sync::LazyLock;

use regex::Regex;

use crate::findings::{Finding, Severity};

struct ModelShape {
    path: String,
    name: String,
    line: usize,
    fields: BTreeSet<String>,
    parent: Option<String>,
}

pub fn overlapping_models(sources: &BTreeMap<String, String>) -> Vec<Finding> {
    let models = collect_models(sources);
    let candidate_index = index_candidates(&models);
    emit_overlaps(&models, group_candidate_pairs(&candidate_index))
}

fn collect_models(sources: &BTreeMap<String, String>) -> Vec<ModelShape> {
    let mut models = Vec::new();
    for (path, source) in sources {
        if !path.ends_with(".dart") {
            continue;
        }
        for captures in CLASS_DECLARATION.captures_iter(source) {
            let (Some(complete), Some(name)) = (captures.get(0), captures.name("name")) else {
                continue;
            };
            let name = name.as_str();
            if name.starts_with('_') {
                continue;
            }
            let opening = complete.end() - 1;
            let Some(closing) = matching_brace(source, opening) else {
                continue;
            };
            let fields = class_fields(&source[opening + 1..closing]);
            if fields.len() < 4 {
                continue;
            }
            models.push(ModelShape {
                path: path.clone(),
                name: name.to_owned(),
                line: line_at(source, complete.start()),
                fields,
                parent: captures
                    .name("parent")
                    .map(|value| value.as_str().to_owned()),
            });
        }
    }
    models
}

fn index_candidates(models: &[ModelShape]) -> BTreeMap<&str, Vec<usize>> {
    let mut index = BTreeMap::<&str, Vec<usize>>::new();
    for (model_index, model) in models.iter().enumerate() {
        for field in &model.fields {
            index.entry(field.as_str()).or_default().push(model_index);
        }
    }
    index
}

fn group_candidate_pairs(
    candidate_index: &BTreeMap<&str, Vec<usize>>,
) -> BTreeMap<(usize, usize), usize> {
    let mut shared_counts = BTreeMap::new();
    for model_indices in candidate_index.values() {
        for left_offset in 0..model_indices.len() {
            let left_index = model_indices[left_offset];
            for &right_index in &model_indices[left_offset + 1..] {
                *shared_counts.entry((left_index, right_index)).or_insert(0) += 1;
            }
        }
    }
    shared_counts
}

struct QualifiedPair {
    shared_count: usize,
    union_size: usize,
}

fn qualify_pair(
    left: &ModelShape,
    right: &ModelShape,
    shared_count: usize,
) -> Option<QualifiedPair> {
    if left.path == right.path
        || left.parent.as_deref() == Some(right.name.as_str())
        || right.parent.as_deref() == Some(left.name.as_str())
    {
        return None;
    }
    let union_size = left.fields.len() + right.fields.len() - shared_count;
    if shared_count < 4 || shared_count * 5 < union_size * 4 {
        return None;
    }
    Some(QualifiedPair {
        shared_count,
        union_size,
    })
}

fn emit_overlaps(
    models: &[ModelShape],
    grouped_pairs: BTreeMap<(usize, usize), usize>,
) -> Vec<Finding> {
    let mut findings = Vec::new();
    for ((left_index, right_index), shared_count) in grouped_pairs {
        let (Some(left), Some(right)) = (models.get(left_index), models.get(right_index)) else {
            continue;
        };
        let Some(pair) = qualify_pair(left, right, shared_count) else {
            continue;
        };
        let mut shared_names = String::new();
        for name in left.fields.intersection(&right.fields) {
            if !shared_names.is_empty() {
                shared_names.push_str(", ");
            }
            shared_names.push_str(name);
        }
        findings.push(Finding {
            code: "dart-overlapping-data-model".to_owned(),
            severity: Severity::Info,
            path: left.path.clone(),
            line: left.line,
            end_line: left.line,
            message: format!(
                "{} and {} duplicate {} data fields",
                left.name, right.name, pair.shared_count
            ),
            confidence: "medium".to_owned(),
            why: "Separate cross-file data models have strongly overlapping fields.".to_owned(),
            suggestion: "Confirm distinct contracts or share a common value object.".to_owned(),
            related_files: vec![format!("{}:{}", right.path, right.line)],
            snippet: format!(
                "shared fields: {shared_names}; overlap: {}/{}",
                pair.shared_count, pair.union_size
            ),
            code_flow: Vec::new(),
        });
    }
    findings
}

struct ClassFieldScanner<'a> {
    lines: std::str::Split<'a, char>,
    depth: isize,
}

impl<'a> ClassFieldScanner<'a> {
    fn new(body: &'a str) -> Self {
        Self {
            lines: body.split('\n'),
            depth: 0,
        }
    }
}

impl<'a> Iterator for ClassFieldScanner<'a> {
    type Item = &'a str;

    fn next(&mut self) -> Option<Self::Item> {
        for raw in self.lines.by_ref() {
            let line = raw.split("//").next().unwrap_or_default().trim();
            let is_field_declaration = self.depth == 0
                && line.ends_with(';')
                && !line.contains('(')
                && !line.starts_with("static ")
                && !line.starts_with("const ");
            self.depth += raw.bytes().filter(|byte| *byte == b'{').count() as isize;
            self.depth -= raw.bytes().filter(|byte| *byte == b'}').count() as isize;
            if is_field_declaration {
                return Some(line);
            }
        }
        None
    }
}

fn class_fields(body: &str) -> BTreeSet<String> {
    let mut fields = BTreeSet::new();
    for line in ClassFieldScanner::new(body) {
        let declaration = line
            .trim_end_matches(';')
            .split('=')
            .next()
            .unwrap_or_default()
            .trim();
        for (index, part) in declaration.split(',').enumerate() {
            finalize_field(&mut fields, part, index == 0);
        }
    }
    fields
}

fn finalize_field(fields: &mut BTreeSet<String>, declaration: &str, is_first: bool) {
    let mut tokens = declaration.split_whitespace();
    let name = if is_first {
        tokens.next_back()
    } else {
        tokens.next()
    };
    if let Some(name) = name
        && IDENTIFIER.is_match(name)
        && !name.starts_with('_')
        && !matches!(name, "final" | "late" | "var")
    {
        fields.insert(name.to_owned());
    }
}

fn matching_brace(source: &str, opening: usize) -> Option<usize> {
    let mut depth = 0usize;
    for (offset, byte) in source.as_bytes()[opening..].iter().enumerate() {
        if *byte == b'{' {
            depth += 1;
        } else if *byte == b'}' {
            depth = depth.checked_sub(1)?;
            if depth == 0 {
                return Some(opening + offset);
            }
        }
    }
    None
}

fn line_at(source: &str, offset: usize) -> usize {
    source[..offset]
        .bytes()
        .filter(|byte| *byte == b'\n')
        .count()
        + 1
}

static CLASS_DECLARATION: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"(?m)^[ \t]*(?:(?:abstract|base|final|interface|sealed)[ \t]+)*class[ \t]+(?P<name>[A-Za-z_]\w*)(?:[ \t]+extends[ \t]+(?P<parent>[A-Za-z_]\w*(?:<[^>{}]*>)?))?[^{}]*\{")
        .expect("Dart class declaration regex must compile")
});
static IDENTIFIER: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"^[A-Za-z_]\w*$").expect("identifier regex must compile"));