_diffctx/edges/semantic/
nim.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_nim_file(path: &Path) -> bool {
14 let ext = base::file_ext(path);
15 ext == ".nim" || ext == ".nims"
16}
17
18static IMPORT_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*import\s+([\w/,\s]+)").unwrap());
19static FROM_IMPORT_RE: Lazy<Regex> =
20 Lazy::new(|| Regex::new(r"(?m)^\s*from\s+([\w/]+)\s+import\s+(.+)").unwrap());
21static INCLUDE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*include\s+([\w/]+)").unwrap());
22static PROC_RE: Lazy<Regex> = Lazy::new(|| {
23 Regex::new(r"(?m)^\s*(?:proc|func|method|iterator|converter|template|macro)\s+(\w+)").unwrap()
24});
25static TYPE_SINGLE_RE: Lazy<Regex> = Lazy::new(|| {
26 Regex::new(r"(?m)^\s+(\w+)\*?\s*=\s*(?:object|ref|enum|distinct|concept)").unwrap()
27});
28static CALL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b(\w+)\s*[\(\[]").unwrap());
29
30static NIM_KEYWORDS: Lazy<FxHashSet<&str>> = Lazy::new(|| {
31 [
32 "if",
33 "elif",
34 "else",
35 "when",
36 "case",
37 "of",
38 "for",
39 "while",
40 "block",
41 "break",
42 "continue",
43 "return",
44 "result",
45 "proc",
46 "func",
47 "method",
48 "var",
49 "let",
50 "const",
51 "type",
52 "import",
53 "from",
54 "include",
55 "export",
56 "template",
57 "macro",
58 "iterator",
59 "converter",
60 "object",
61 "ref",
62 "ptr",
63 "nil",
64 "true",
65 "false",
66 "and",
67 "or",
68 "not",
69 "xor",
70 "div",
71 "mod",
72 "echo",
73 "assert",
74 "doAssert",
75 "len",
76 "add",
77 "del",
78 "new",
79 "newSeq",
80 ]
81 .iter()
82 .copied()
83 .collect()
84});
85
86fn extract_imports(content: &str) -> FxHashSet<String> {
87 let mut refs = FxHashSet::default();
88 for cap in IMPORT_RE.captures_iter(content) {
89 for part in cap[1].split(',') {
90 let name = part.trim().split('/').last().unwrap_or("").trim();
91 if !name.is_empty() {
92 refs.insert(name.to_string());
93 }
94 }
95 }
96 for cap in FROM_IMPORT_RE.captures_iter(content) {
97 refs.insert(cap[1].split('/').last().unwrap_or(&cap[1]).to_string());
98 }
99 refs.extend(
100 INCLUDE_RE
101 .captures_iter(content)
102 .map(|c| c[1].split('/').last().unwrap_or(&c[1]).to_string()),
103 );
104 refs
105}
106
107fn extract_defs(content: &str) -> FxHashSet<String> {
108 let mut defs: FxHashSet<String> = PROC_RE
109 .captures_iter(content)
110 .map(|c| c[1].to_string())
111 .collect();
112 defs.extend(
113 TYPE_SINGLE_RE
114 .captures_iter(content)
115 .map(|c| c[1].to_string()),
116 );
117 defs
118}
119
120pub struct NimEdgeBuilder;
121
122impl EdgeBuilder for NimEdgeBuilder {
123 fn build(&self, fragments: &[Fragment], repo_root: Option<&Path>) -> EdgeDict {
124 let frags: Vec<&Fragment> = fragments
125 .iter()
126 .filter(|f| is_nim_file(Path::new(f.path())))
127 .collect();
128 if frags.is_empty() {
129 return FxHashMap::default();
130 }
131
132 let import_w = EDGE_WEIGHTS["nim_import"].forward;
133 let type_w = EDGE_WEIGHTS["nim_type"].forward;
134 let fn_w = EDGE_WEIGHTS["nim_fn"].forward;
135 let reverse_factor = EDGE_WEIGHTS["nim_import"].reverse_factor;
136
137 let idx = base::FragmentIndex::new(fragments, repo_root);
138 let mut name_to_defs: FxHashMap<String, Vec<_>> = FxHashMap::default();
139 for f in &frags {
140 for name in extract_defs(&f.content) {
141 name_to_defs
142 .entry(name.to_lowercase())
143 .or_default()
144 .push(f.id.clone());
145 }
146 }
147
148 let mut edges: EdgeDict = FxHashMap::default();
149
150 for f in &frags {
151 let self_defs = extract_defs(&f.content);
152 for imp in extract_imports(&f.content) {
153 base::link_by_name(&f.id, &imp, &idx, &mut edges, import_w, reverse_factor);
154 }
155 for cap in CALL_RE.captures_iter(&f.content) {
156 let name = &cap[1];
157 if self_defs.contains(name) || NIM_KEYWORDS.contains(name) {
158 continue;
159 }
160 let w = if name.starts_with(|c: char| c.is_uppercase()) {
161 type_w
162 } else {
163 fn_w
164 };
165 if let Some(targets) = name_to_defs.get(&name.to_lowercase()) {
166 for t in targets {
167 if t != &f.id {
168 add_edge(&mut edges, &f.id, t, w, reverse_factor);
169 }
170 }
171 }
172 }
173 }
174 edges
175 }
176
177 fn discover_related_files(
178 &self,
179 changed: &[PathBuf],
180 candidates: &[PathBuf],
181 repo_root: Option<&Path>,
182 file_cache: Option<&FxHashMap<PathBuf, String>>,
183 ) -> Vec<PathBuf> {
184 let nim_changed: Vec<&PathBuf> = changed.iter().filter(|f| is_nim_file(f)).collect();
185 if nim_changed.is_empty() {
186 return vec![];
187 }
188 let mut refs = FxHashSet::default();
189 for f in &nim_changed {
190 if let Some(content) = base::read_file_cached(f, file_cache) {
191 refs.extend(extract_imports(&content));
192 }
193 }
194 discover_files_by_refs(&refs, changed, candidates, repo_root)
195 }
196}