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