_diffctx/edges/semantic/
zig.rs1use std::path::{Path, PathBuf};
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5use rustc_hash::{FxHashMap, FxHashSet};
6
7use crate::config::weights::EDGE_WEIGHTS;
8use crate::types::Fragment;
9
10use super::super::EdgeDict;
11use super::super::base::{self, EdgeBuilder, add_edge, discover_files_by_refs};
12
13fn is_zig_file(path: &Path) -> bool {
14 base::file_ext(path) == ".zig"
15}
16
17static IMPORT_RE: Lazy<Regex> =
18 Lazy::new(|| Regex::new(r#"@import\s*\(\s*['"]([^'"]+)['"]"#).unwrap());
19static FN_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*(?:pub\s+)?fn\s+(\w+)").unwrap());
20static STRUCT_RE: Lazy<Regex> = Lazy::new(|| {
21 Regex::new(r"(?m)^\s*(?:pub\s+)?const\s+(\w+)\s*=\s*(?:struct|union|enum|packed struct)")
22 .unwrap()
23});
24static TYPE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([A-Z]\w+)\b").unwrap());
25static CALL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b(\w+)\s*\(").unwrap());
26
27static ZIG_KEYWORDS: Lazy<FxHashSet<&str>> = Lazy::new(|| {
28 [
29 "if",
30 "else",
31 "while",
32 "for",
33 "switch",
34 "return",
35 "break",
36 "continue",
37 "fn",
38 "pub",
39 "const",
40 "var",
41 "struct",
42 "enum",
43 "union",
44 "error",
45 "try",
46 "catch",
47 "unreachable",
48 "undefined",
49 "null",
50 "true",
51 "false",
52 "comptime",
53 "inline",
54 "extern",
55 "export",
56 "test",
57 "defer",
58 "errdefer",
59 ]
60 .iter()
61 .copied()
62 .collect()
63});
64
65fn extract_imports(content: &str) -> FxHashSet<String> {
66 IMPORT_RE
67 .captures_iter(content)
68 .map(|c| c[1].to_string())
69 .collect()
70}
71
72fn extract_defs(content: &str) -> FxHashSet<String> {
73 let mut defs: FxHashSet<String> = FN_RE
74 .captures_iter(content)
75 .map(|c| c[1].to_string())
76 .collect();
77 defs.extend(STRUCT_RE.captures_iter(content).map(|c| c[1].to_string()));
78 defs
79}
80
81pub struct ZigEdgeBuilder;
82
83impl EdgeBuilder for ZigEdgeBuilder {
84 fn build(&self, fragments: &[Fragment], repo_root: Option<&Path>) -> EdgeDict {
85 let frags: Vec<&Fragment> = fragments
86 .iter()
87 .filter(|f| is_zig_file(Path::new(f.path())))
88 .collect();
89 if frags.is_empty() {
90 return FxHashMap::default();
91 }
92
93 let import_w = EDGE_WEIGHTS["zig_import"].forward;
94 let type_w = EDGE_WEIGHTS["zig_type"].forward;
95 let fn_w = EDGE_WEIGHTS["zig_fn"].forward;
96 let reverse_factor = EDGE_WEIGHTS["zig_import"].reverse_factor;
97
98 let idx = base::FragmentIndex::new(fragments, repo_root);
99 let mut name_to_defs: FxHashMap<String, Vec<_>> = FxHashMap::default();
100 for f in &frags {
101 for name in extract_defs(&f.content) {
102 name_to_defs
103 .entry(name.to_lowercase())
104 .or_default()
105 .push(f.id.clone());
106 }
107 }
108
109 let mut edges: EdgeDict = FxHashMap::default();
110
111 for f in &frags {
112 let self_defs = extract_defs(&f.content);
113 for imp in extract_imports(&f.content) {
114 base::link_by_name(&f.id, &imp, &idx, &mut edges, import_w, reverse_factor);
115 }
116 for cap in TYPE_RE.captures_iter(&f.content) {
117 let name = &cap[1];
118 if self_defs.contains(name) {
119 continue;
120 }
121 if let Some(targets) = name_to_defs.get(&name.to_lowercase()) {
122 for t in targets {
123 if t != &f.id {
124 add_edge(&mut edges, &f.id, t, type_w, reverse_factor);
125 }
126 }
127 }
128 }
129 for cap in CALL_RE.captures_iter(&f.content) {
130 let name = &cap[1];
131 if self_defs.contains(name) || ZIG_KEYWORDS.contains(name) {
132 continue;
133 }
134 if let Some(targets) = name_to_defs.get(&name.to_lowercase()) {
135 for t in targets {
136 if t != &f.id {
137 add_edge(&mut edges, &f.id, t, fn_w, reverse_factor);
138 }
139 }
140 }
141 }
142 }
143 edges
144 }
145
146 fn discover_related_files(
147 &self,
148 changed: &[PathBuf],
149 candidates: &[PathBuf],
150 repo_root: Option<&Path>,
151 file_cache: Option<&FxHashMap<PathBuf, String>>,
152 ) -> Vec<PathBuf> {
153 let zig_changed: Vec<&PathBuf> = changed.iter().filter(|f| is_zig_file(f)).collect();
154 if zig_changed.is_empty() {
155 return vec![];
156 }
157 let mut refs = FxHashSet::default();
158 for f in &zig_changed {
159 if let Some(content) = base::read_file_cached(f, file_cache) {
160 refs.extend(extract_imports(&content));
161 }
162 }
163 discover_files_by_refs(&refs, changed, candidates, repo_root)
164 }
165}