1use std::{
2 collections::{BTreeMap, BTreeSet},
3 path::{Path, PathBuf},
4};
5
6use color_eyre::eyre::{WrapErr, ensure};
7
8#[derive(Debug, Clone)]
10pub struct SolidityExtractionConfig {
11 pub repo_root: PathBuf,
13 pub source_files: Vec<PathBuf>,
15 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}