Skip to main content

_diffctx/edges/semantic/
nim.rs

1use 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}