diffctx 1.12.1

Selects the minimum code an LLM needs to review a git diff: walks the dependency graph outward from changed lines and stops when extra context stops paying for itself
Documentation
use std::path::{Path, PathBuf};

use once_cell::sync::Lazy;
use regex::Regex;
use rustc_hash::{FxHashMap, FxHashSet};

use crate::config::edge_weights::{C_FAMILY_SEMANTIC, SEMANTIC_DISCOVERY};
use crate::config::extensions::C_FAMILY_EXTENSIONS;
use crate::config::weights::EDGE_WEIGHTS;
use crate::types::{Fragment, FragmentId};

use super::super::EdgeDict;
use super::super::base::{self, EdgeBuilder, add_edge};

fn is_c_family(path: &Path) -> bool {
    let ext = base::file_ext(path);
    C_FAMILY_EXTENSIONS.contains(ext.as_str())
}

static HEADER_EXTENSIONS: Lazy<FxHashSet<&str>> = Lazy::new(|| {
    [".h", ".hpp", ".hh", ".hxx", ".h++"]
        .iter()
        .copied()
        .collect()
});

static IMPL_EXTENSIONS: Lazy<FxHashSet<&str>> = Lazy::new(|| {
    [".c", ".cpp", ".cc", ".cxx", ".c++", ".m", ".mm"]
        .iter()
        .copied()
        .collect()
});

static INCLUDE_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r#"(?m)^\s*#\s*(?:include|import)\s*[<"]([^>"]+)[>"]"#).unwrap());
static FUNC_CALL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b(\w+)\s*\(").unwrap());
static TYPE_REF_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([A-Z]\w*)\b").unwrap());
static FUNC_DEF_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"(?m)^\s*(?:[\w*&]+\s+)+(\w+)\s*\(").unwrap());
static TYPE_DEF_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"(?m)^\s*(?:class|struct|enum|union|typedef)\s+([A-Z]\w*)").unwrap());
static INHERITANCE_RE: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"(?:class|struct)\s+(\w+)\s*:\s*(?:public|protected|private)?\s*(\w+)").unwrap()
});

static C_KEYWORDS: Lazy<FxHashSet<&str>> = Lazy::new(|| {
    [
        "if",
        "for",
        "while",
        "switch",
        "case",
        "return",
        "sizeof",
        "typeof",
        "alignof",
        "static_assert",
        "do",
        "else",
        "goto",
        "break",
        "continue",
        "default",
        "register",
        "volatile",
        "extern",
        "typedef",
        "auto",
        "inline",
        "restrict",
        "noexcept",
        "decltype",
        "nullptr",
        "throw",
        "try",
        "catch",
        "delete",
        "new",
        "template",
        "namespace",
        "using",
        "operator",
    ]
    .iter()
    .copied()
    .collect()
});

static C_COMMON_MACROS: Lazy<FxHashSet<&str>> = Lazy::new(|| {
    [
        "NULL", "TRUE", "FALSE", "BOOL", "DWORD", "HANDLE", "VOID", "HRESULT", "LPCTSTR", "LPCSTR",
        "LPWSTR", "INT", "UINT", "LONG", "ULONG", "WORD", "BYTE", "CHAR", "SHORT", "EOF",
        "SIZE_MAX", "INT_MAX", "INT_MIN",
    ]
    .iter()
    .copied()
    .collect()
});

fn extract_includes(content: &str) -> FxHashSet<String> {
    let mut includes = FxHashSet::default();
    for cap in INCLUDE_RE.captures_iter(content) {
        let header = cap[1].to_string();
        if header.contains('/') {
            includes.insert(header.split('/').next_back().unwrap().to_string());
        }
        includes.insert(header);
    }
    includes
}

fn extract_definitions(content: &str) -> (FxHashSet<String>, FxHashSet<String>) {
    let functions: FxHashSet<String> = FUNC_DEF_RE
        .captures_iter(content)
        .map(|c| c[1].to_string())
        .filter(|n| {
            !C_KEYWORDS.contains(n.as_str()) && n.len() > SEMANTIC_DISCOVERY.min_identifier_length
        })
        .collect();
    let types: FxHashSet<String> = TYPE_DEF_RE
        .captures_iter(content)
        .map(|c| c[1].to_string())
        .collect();
    (functions, types)
}

fn extract_references(
    content: &str,
    own_defs: &FxHashSet<String>,
) -> (FxHashSet<String>, FxHashSet<String>) {
    let calls: FxHashSet<String> = FUNC_CALL_RE
        .captures_iter(content)
        .map(|c| c[1].to_string())
        .filter(|n| {
            !C_KEYWORDS.contains(n.as_str())
                && !own_defs.contains(n)
                && !n.starts_with('_')
                && n.len() > SEMANTIC_DISCOVERY.min_identifier_length
        })
        .collect();
    let type_refs: FxHashSet<String> = TYPE_REF_RE
        .captures_iter(content)
        .map(|c| c[1].to_string())
        .filter(|n| {
            !C_COMMON_MACROS.contains(n.as_str())
                && !own_defs.contains(n)
                && n.len() > SEMANTIC_DISCOVERY.min_identifier_length
        })
        .collect();
    (calls, type_refs)
}

pub struct CFamilyEdgeBuilder;

impl EdgeBuilder for CFamilyEdgeBuilder {
    fn build(&self, fragments: &[Fragment], _repo_root: Option<&Path>) -> EdgeDict {
        let c_frags: Vec<&Fragment> = fragments
            .iter()
            .filter(|f| is_c_family(Path::new(f.path())))
            .collect();
        if c_frags.is_empty() {
            return FxHashMap::default();
        }

        let include_weight = EDGE_WEIGHTS["c_include"].forward;
        let call_weight = EDGE_WEIGHTS["c_call"].forward;
        let type_weight = EDGE_WEIGHTS["c_type"].forward;
        let inheritance_weight = EDGE_WEIGHTS["c_inheritance"].forward;
        let reverse_factor = EDGE_WEIGHTS["c_include"].reverse_factor;
        let base_weight = C_FAMILY_SEMANTIC.base_weight;

        let mut header_to_frags: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
        let mut func_defs_map: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
        let mut type_defs_map: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
        let mut frag_own_defs: FxHashMap<FragmentId, FxHashSet<String>> = FxHashMap::default();

        for f in &c_frags {
            let path = Path::new(f.path());
            let name = path
                .file_name()
                .map(|n| n.to_string_lossy().to_string())
                .unwrap_or_default();
            let stem = path
                .file_stem()
                .map(|s| s.to_string_lossy().to_string())
                .unwrap_or_default();

            header_to_frags.entry(name).or_default().push(f.id.clone());
            if !stem.is_empty() {
                header_to_frags
                    .entry(format!("{}.h", stem))
                    .or_default()
                    .push(f.id.clone());
                header_to_frags
                    .entry(format!("{}.hpp", stem))
                    .or_default()
                    .push(f.id.clone());
            }

            let (functions, types) = extract_definitions(&f.content);
            let mut own_defs = FxHashSet::default();
            for func in &functions {
                func_defs_map
                    .entry(func.clone())
                    .or_default()
                    .push(f.id.clone());
                own_defs.insert(func.clone());
            }
            for t in &types {
                type_defs_map
                    .entry(t.clone())
                    .or_default()
                    .push(f.id.clone());
                own_defs.insert(t.clone());
            }
            frag_own_defs.insert(f.id.clone(), own_defs);
        }

        let mut edges: EdgeDict = FxHashMap::default();

        for f in &c_frags {
            for inc in extract_includes(&f.content) {
                let inc_name = if inc.contains('/') {
                    inc.split('/').next_back().unwrap().to_string()
                } else {
                    inc.clone()
                };
                for target_id in header_to_frags.get(&inc_name).unwrap_or(&vec![]) {
                    if target_id != &f.id {
                        add_edge(&mut edges, &f.id, target_id, include_weight, reverse_factor);
                    }
                }
            }

            let own_defs = frag_own_defs.get(&f.id).cloned().unwrap_or_default();
            let (calls, type_refs) = extract_references(&f.content, &own_defs);

            for call in &calls {
                for def_id in func_defs_map.get(call).unwrap_or(&vec![]) {
                    if def_id != &f.id {
                        add_edge(&mut edges, &f.id, def_id, call_weight, reverse_factor);
                    }
                }
            }

            for t in &type_refs {
                for def_id in type_defs_map.get(t).unwrap_or(&vec![]) {
                    if def_id != &f.id {
                        add_edge(&mut edges, &f.id, def_id, type_weight, reverse_factor);
                    }
                }
            }

            for cap in INHERITANCE_RE.captures_iter(&f.content) {
                let base = cap[2].to_string();
                for def_id in type_defs_map.get(&base).unwrap_or(&vec![]) {
                    if def_id != &f.id {
                        add_edge(
                            &mut edges,
                            &f.id,
                            def_id,
                            inheritance_weight,
                            reverse_factor,
                        );
                    }
                }
            }
        }

        let mut by_stem: FxHashMap<String, Vec<&Fragment>> = FxHashMap::default();
        for f in &c_frags {
            let stem = Path::new(f.path())
                .file_stem()
                .map(|s| s.to_string_lossy().to_lowercase())
                .unwrap_or_default();
            by_stem.entry(stem).or_default().push(f);
        }

        for (_stem, group) in &by_stem {
            if group.len() < 2 {
                continue;
            }
            let headers: Vec<&&Fragment> = group
                .iter()
                .filter(|f| {
                    HEADER_EXTENSIONS.contains(base::file_ext(Path::new(f.path())).as_str())
                })
                .collect();
            let impls: Vec<&&Fragment> = group
                .iter()
                .filter(|f| IMPL_EXTENSIONS.contains(base::file_ext(Path::new(f.path())).as_str()))
                .collect();
            for h in &headers {
                for imp in &impls {
                    add_edge(&mut edges, &h.id, &imp.id, base_weight, reverse_factor);
                }
            }
        }

        edges
    }

    fn discover_related_files(
        &self,
        changed: &[PathBuf],
        candidates: &[PathBuf],
        _repo_root: Option<&Path>,
        _file_cache: Option<&FxHashMap<PathBuf, String>>,
    ) -> Vec<PathBuf> {
        let c_changed: Vec<&PathBuf> = changed.iter().filter(|f| is_c_family(f)).collect();
        if c_changed.is_empty() {
            return vec![];
        }

        let changed_set: FxHashSet<PathBuf> = changed.iter().cloned().collect();
        let mut discovered: FxHashSet<PathBuf> = FxHashSet::default();
        let mut frontier: Vec<PathBuf> = c_changed.iter().map(|f| (*f).clone()).collect();

        for _ in 0..SEMANTIC_DISCOVERY.max_depth {
            let mut hop_found: Vec<PathBuf> = Vec::new();

            let mut included_headers: FxHashSet<String> = FxHashSet::default();
            for f in &frontier {
                if let Ok(content) = std::fs::read_to_string(f) {
                    included_headers.extend(extract_includes(&content));
                }
            }

            let mut changed_names: FxHashSet<String> = FxHashSet::default();
            for f in &frontier {
                if let Some(name) = f.file_name() {
                    changed_names.insert(name.to_string_lossy().to_string());
                }
                if let Some(stem) = f.file_stem() {
                    let s = stem.to_string_lossy().to_string();
                    changed_names.insert(format!("{}.h", s));
                    changed_names.insert(format!("{}.hpp", s));
                }
            }

            for candidate in candidates {
                if changed_set.contains(candidate)
                    || discovered.contains(candidate)
                    || !is_c_family(candidate)
                {
                    continue;
                }
                let cand_name = candidate
                    .file_name()
                    .map(|n| n.to_string_lossy().to_string())
                    .unwrap_or_default();
                if included_headers.contains(&cand_name) {
                    hop_found.push(candidate.clone());
                    continue;
                }
                if let Ok(content) = std::fs::read_to_string(candidate) {
                    let cand_includes = extract_includes(&content);
                    for inc in &cand_includes {
                        let inc_name = if inc.contains('/') {
                            inc.split('/').next_back().unwrap().to_string()
                        } else {
                            inc.clone()
                        };
                        if changed_names.contains(&inc_name) {
                            hop_found.push(candidate.clone());
                            break;
                        }
                    }
                }
            }

            let new_files: Vec<PathBuf> = hop_found
                .into_iter()
                .filter(|f| !discovered.contains(f))
                .collect();
            if new_files.is_empty() {
                break;
            }
            discovered.extend(new_files.iter().cloned());
            frontier = new_files;
        }

        let mut result: Vec<PathBuf> = discovered.into_iter().collect();
        result.sort();
        result
    }
}