lean_ctx_syntax/import_resolver/
mod.rs1use std::collections::HashMap;
20use std::path::{Path, PathBuf};
21
22use super::deep_queries::ImportInfo;
23
24#[derive(Debug, Clone)]
25pub struct ResolvedImport {
26 pub source: String,
27 pub resolved_path: Option<String>,
28 pub is_external: bool,
29 pub line: usize,
30}
31
32#[derive(Debug)]
33pub struct ResolverContext {
34 pub project_root: PathBuf,
35 pub file_paths: Vec<String>,
36 pub tsconfig_paths: HashMap<String, String>,
37 pub go_module: Option<String>,
38 pub dart_package: Option<String>,
39 file_set: std::collections::HashSet<String>,
40 csharp_ns_index: HashMap<String, String>,
44}
45
46impl ResolverContext {
47 pub fn new(
52 project_root: &Path,
53 file_paths: Vec<String>,
54 file_contents: &HashMap<String, String>,
55 ) -> Self {
56 let file_set: std::collections::HashSet<String> = file_paths.iter().cloned().collect();
57
58 let tsconfig_paths = load_tsconfig_paths(project_root);
59 let go_module = load_go_module(project_root);
60 let dart_package = load_dart_package(project_root);
61 let csharp_ns_index =
62 build_csharp_namespace_index(project_root, &file_paths, file_contents);
63
64 Self {
65 project_root: project_root.to_path_buf(),
66 file_paths,
67 tsconfig_paths,
68 go_module,
69 dart_package,
70 file_set,
71 csharp_ns_index,
72 }
73 }
74
75 fn file_exists(&self, rel_path: &str) -> bool {
76 self.file_set.contains(rel_path)
77 }
78
79 fn csharp_namespace_file(&self, namespace_path: &str) -> Option<&str> {
82 self.csharp_ns_index.get(namespace_path).map(String::as_str)
83 }
84}
85
86fn build_csharp_namespace_index(
98 project_root: &Path,
99 file_paths: &[String],
100 file_contents: &HashMap<String, String>,
101) -> HashMap<String, String> {
102 let mut cs_files: Vec<&String> = file_paths
103 .iter()
104 .filter(|f| {
105 Path::new(f.as_str())
106 .extension()
107 .and_then(|e| e.to_str())
108 .is_some_and(|e| e.eq_ignore_ascii_case("cs"))
109 })
110 .collect();
111 if cs_files.is_empty() {
112 return HashMap::new();
113 }
114 cs_files.sort();
115
116 let mut map: HashMap<String, String> = HashMap::new();
117
118 const MAX_CS_FILES_READ: usize = 5000;
121 for file in cs_files.iter().take(MAX_CS_FILES_READ) {
122 let content: Option<std::borrow::Cow<'_, str>> = match file_contents.get(*file) {
123 Some(c) => Some(std::borrow::Cow::Borrowed(c.as_str())),
124 None => read_file_head(&project_root.join(file.as_str()), 64 * 1024)
125 .map(std::borrow::Cow::Owned),
126 };
127 let Some(content) = content else { continue };
128 for ns in extract_csharp_namespaces(&content) {
129 let key = ns.replace('.', "/");
130 map.entry(key).or_insert_with(|| (*file).clone());
131 }
132 }
133
134 for file in &cs_files {
136 let dir = Path::new(file.as_str())
137 .parent()
138 .map(|p| p.to_string_lossy().replace('\\', "/"))
139 .unwrap_or_default();
140 let segs: Vec<&str> = dir.split('/').filter(|s| !s.is_empty()).collect();
141 for start in 0..segs.len() {
142 let key = segs[start..].join("/");
143 map.entry(key).or_insert_with(|| (*file).clone());
144 }
145 }
146 map
147}
148
149fn extract_csharp_namespaces(content: &str) -> Vec<String> {
151 let mut out: Vec<String> = Vec::new();
152 for line in content.lines() {
153 let Some(rest) = line.trim_start().strip_prefix("namespace ") else {
154 continue;
155 };
156 let name: String = rest
157 .trim_start()
158 .chars()
159 .take_while(|c| c.is_alphanumeric() || *c == '.' || *c == '_')
160 .collect();
161 if !name.is_empty() && !out.contains(&name) {
162 out.push(name);
163 }
164 }
165 out
166}
167
168fn read_file_head(path: &Path, max_bytes: usize) -> Option<String> {
171 use std::io::Read;
172 let mut f = std::fs::File::open(path).ok()?;
173 let mut buf = vec![0u8; max_bytes];
174 let n = f.read(&mut buf).ok()?;
175 buf.truncate(n);
176 Some(String::from_utf8_lossy(&buf).into_owned())
177}
178
179pub fn resolve_imports(
180 imports: &[ImportInfo],
181 file_path: &str,
182 ext: &str,
183 ctx: &ResolverContext,
184) -> Vec<ResolvedImport> {
185 imports
186 .iter()
187 .map(|imp| {
188 let (resolved, is_external) = resolve_one(imp, file_path, ext, ctx);
189 ResolvedImport {
190 source: imp.source.clone(),
191 resolved_path: resolved,
192 is_external,
193 line: imp.line,
194 }
195 })
196 .collect()
197}
198
199fn resolve_one(
200 imp: &ImportInfo,
201 file_path: &str,
202 ext: &str,
203 ctx: &ResolverContext,
204) -> (Option<String>, bool) {
205 match ext {
206 "ts" | "tsx" | "js" | "jsx" => resolve_ts(imp, file_path, ctx),
207 "rs" => resolve_rust(imp, file_path, ctx),
208 "py" => resolve_python(imp, file_path, ctx),
209 "go" => resolve_go(imp, ctx),
210 "java" => resolve_java(imp, ctx),
211 "c" | "h" | "cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => {
212 resolve_c_like(imp, file_path, ctx)
213 }
214 "rb" => resolve_ruby(imp, file_path, ctx),
215 "php" => resolve_php(imp, file_path, ctx),
216 "sh" | "bash" => resolve_bash(imp, file_path, ctx),
217 "dart" => resolve_dart(imp, file_path, ctx),
218 "zig" => resolve_zig(imp, file_path, ctx),
219 "kt" | "kts" => resolve_kotlin(imp, ctx),
220 "cs" => resolve_csharp(imp, ctx),
221 "swift" => resolve_swift(imp, file_path, ctx),
222 "scala" | "sc" => resolve_scala(imp, ctx),
223 "ex" | "exs" => resolve_elixir(imp, file_path, ctx),
224 "gd" | "tscn" => resolve_gd(imp, file_path, ctx),
227 "lua" | "luau" => resolve_lua(imp, file_path, ctx),
228 _ => (None, true),
229 }
230}
231
232pub mod languages;
233#[allow(clippy::wildcard_imports)]
234use languages::*;
235
236fn load_tsconfig_paths(root: &Path) -> HashMap<String, String> {
241 let mut paths = HashMap::new();
242
243 let candidates = ["tsconfig.json", "tsconfig.base.json", "jsconfig.json"];
244 for name in &candidates {
245 let tsconfig_path = root.join(name);
246 if let Ok(content) = std::fs::read_to_string(&tsconfig_path) {
247 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content)
248 && let Some(compiler) = json.get("compilerOptions")
249 {
250 let base_url = compiler
251 .get("baseUrl")
252 .and_then(|v| v.as_str())
253 .unwrap_or(".");
254
255 if let Some(path_map) = compiler.get("paths").and_then(|v| v.as_object()) {
256 for (pattern, targets) in path_map {
257 if let Some(first_target) = targets
258 .as_array()
259 .and_then(|a| a.first())
260 .and_then(|v| v.as_str())
261 {
262 let resolved = if base_url == "." {
263 first_target.to_string()
264 } else {
265 format!("{base_url}/{first_target}")
266 };
267 paths.insert(pattern.clone(), resolved);
268 }
269 }
270 }
271 }
272 break;
273 }
274 }
275
276 paths
277}
278
279fn load_go_module(root: &Path) -> Option<String> {
280 let go_mod = root.join("go.mod");
281 let content = std::fs::read_to_string(go_mod).ok()?;
282 for line in content.lines() {
283 let trimmed = line.trim();
284 if trimmed.starts_with("module ") {
285 return Some(trimmed.strip_prefix("module ")?.trim().to_string());
286 }
287 }
288 None
289}
290
291fn load_dart_package(root: &Path) -> Option<String> {
292 let pubspec = root.join("pubspec.yaml");
293 let content = std::fs::read_to_string(pubspec).ok()?;
294 for line in content.lines() {
295 let trimmed = line.trim();
296 if let Some(rest) = trimmed.strip_prefix("name:") {
297 let name = rest.trim();
298 if !name.is_empty() {
299 return Some(name.to_string());
300 }
301 }
302 }
303 None
304}
305
306fn normalize_path(path: &Path) -> String {
311 let mut parts: Vec<&str> = Vec::new();
312 for component in path.components() {
313 match component {
314 std::path::Component::ParentDir => {
315 parts.pop();
316 }
317 std::path::Component::Normal(s) => {
318 parts.push(s.to_str().unwrap_or(""));
319 }
320 _ => {}
321 }
322 }
323 parts.join("/")
324}
325
326#[cfg(test)]
327pub mod tests;