Skip to main content

knowdit_sol/
filter.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    path::{Path, PathBuf},
4};
5
6use color_eyre::eyre::{WrapErr, ensure};
7
8/// Configuration for Solidity extraction after CLI/source discovery has expanded globs.
9#[derive(Debug, Clone)]
10pub struct SolidityExtractionConfig {
11    /// Repository root.
12    pub repo_root: PathBuf,
13    /// Solidity source files, relative to `repo_root`, extracted by tree-sitter.
14    pub source_files: Vec<PathBuf>,
15    /// Subset of `source_files` whose functions should be analyzed by the callgraph agent.
16    pub analysis_source_files: Vec<PathBuf>,
17}
18
19impl SolidityExtractionConfig {
20    pub fn new(
21        repo_root: impl Into<PathBuf>,
22        source_files: Vec<PathBuf>,
23        analysis_source_files: Vec<PathBuf>,
24    ) -> Self {
25        Self {
26            repo_root: repo_root.into(),
27            source_files,
28            analysis_source_files,
29        }
30    }
31}
32
33pub fn normalize_relative_source_files(
34    repo_root: &Path,
35    source_files: Vec<PathBuf>,
36) -> Result<Vec<PathBuf>, color_eyre::Report> {
37    let mut seen = BTreeSet::new();
38    let mut normalized = Vec::new();
39    for path in source_files {
40        let path = normalize_relative_path(repo_root, path)?;
41        if seen.insert(path_key(&path)) {
42            normalized.push(path);
43        }
44    }
45    normalized.sort();
46    Ok(normalized)
47}
48
49pub fn filter_analysis_source_files(
50    mut source_files: Vec<PathBuf>,
51    analysis_source_files: Vec<PathBuf>,
52) -> Result<Vec<PathBuf>, color_eyre::Report> {
53    if analysis_source_files.is_empty() {
54        source_files.sort();
55        return Ok(source_files);
56    }
57
58    let sources_by_key = source_files
59        .into_iter()
60        .map(|path| (path_key(&path), path))
61        .collect::<BTreeMap<_, _>>();
62    let mut missing = Vec::new();
63    let mut seen = BTreeSet::new();
64    let mut filtered = Vec::new();
65
66    for path in analysis_source_files {
67        let key = path_key(&path);
68        let Some(source_path) = sources_by_key.get(&key) else {
69            missing.push(path);
70            continue;
71        };
72        if seen.insert(key) {
73            filtered.push(source_path.clone());
74        }
75    }
76
77    ensure!(
78        missing.is_empty(),
79        "analysis source files are not present in the tree-sitter source set: {}",
80        missing
81            .iter()
82            .map(|path| path.display().to_string())
83            .collect::<Vec<_>>()
84            .join(", ")
85    );
86    ensure!(
87        !filtered.is_empty(),
88        "callgraph analysis source set is empty"
89    );
90
91    filtered.sort();
92    Ok(filtered)
93}
94
95fn normalize_relative_path(repo_root: &Path, path: PathBuf) -> Result<PathBuf, color_eyre::Report> {
96    let path = if path.is_absolute() {
97        path.strip_prefix(repo_root)
98            .wrap_err_with(|| {
99                format!(
100                    "source file {} is not under repo root {}",
101                    path.display(),
102                    repo_root.display()
103                )
104            })?
105            .to_path_buf()
106    } else {
107        path
108    };
109    let normalized = path_key(&path);
110    ensure!(!normalized.is_empty(), "source file path is empty");
111    Ok(PathBuf::from(normalized))
112}
113
114fn path_key(path: &Path) -> String {
115    path.to_string_lossy()
116        .trim()
117        .trim_start_matches("./")
118        .to_string()
119}