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}
39
40impl ResolverContext {
41 pub fn new(project_root: &Path, file_paths: Vec<String>) -> Self {
42 let file_set: std::collections::HashSet<String> = file_paths.iter().cloned().collect();
43
44 let tsconfig_paths = load_tsconfig_paths(project_root);
45 let go_module = load_go_module(project_root);
46 let dart_package = load_dart_package(project_root);
47
48 Self {
49 project_root: project_root.to_path_buf(),
50 file_paths,
51 tsconfig_paths,
52 go_module,
53 dart_package,
54 file_set,
55 }
56 }
57
58 fn file_exists(&self, rel_path: &str) -> bool {
59 self.file_set.contains(rel_path)
60 }
61}
62
63pub fn resolve_imports(
64 imports: &[ImportInfo],
65 file_path: &str,
66 ext: &str,
67 ctx: &ResolverContext,
68) -> Vec<ResolvedImport> {
69 imports
70 .iter()
71 .map(|imp| {
72 let (resolved, is_external) = resolve_one(imp, file_path, ext, ctx);
73 ResolvedImport {
74 source: imp.source.clone(),
75 resolved_path: resolved,
76 is_external,
77 line: imp.line,
78 }
79 })
80 .collect()
81}
82
83fn resolve_one(
84 imp: &ImportInfo,
85 file_path: &str,
86 ext: &str,
87 ctx: &ResolverContext,
88) -> (Option<String>, bool) {
89 match ext {
90 "ts" | "tsx" | "js" | "jsx" => resolve_ts(imp, file_path, ctx),
91 "rs" => resolve_rust(imp, file_path, ctx),
92 "py" => resolve_python(imp, file_path, ctx),
93 "go" => resolve_go(imp, ctx),
94 "java" => resolve_java(imp, ctx),
95 "c" | "h" | "cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => {
96 resolve_c_like(imp, file_path, ctx)
97 }
98 "rb" => resolve_ruby(imp, file_path, ctx),
99 "php" => resolve_php(imp, file_path, ctx),
100 "sh" | "bash" => resolve_bash(imp, file_path, ctx),
101 "dart" => resolve_dart(imp, file_path, ctx),
102 "zig" => resolve_zig(imp, file_path, ctx),
103 "kt" | "kts" => resolve_kotlin(imp, ctx),
104 "cs" => resolve_csharp(imp, ctx),
105 "swift" => resolve_swift(imp, file_path, ctx),
106 "scala" | "sc" => resolve_scala(imp, ctx),
107 "ex" | "exs" => resolve_elixir(imp, file_path, ctx),
108 _ => (None, true),
109 }
110}
111
112mod languages;
113#[allow(clippy::wildcard_imports)]
114use languages::*;
115
116fn load_tsconfig_paths(root: &Path) -> HashMap<String, String> {
121 let mut paths = HashMap::new();
122
123 let candidates = ["tsconfig.json", "tsconfig.base.json", "jsconfig.json"];
124 for name in &candidates {
125 let tsconfig_path = root.join(name);
126 if let Ok(content) = std::fs::read_to_string(&tsconfig_path) {
127 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
128 if let Some(compiler) = json.get("compilerOptions") {
129 let base_url = compiler
130 .get("baseUrl")
131 .and_then(|v| v.as_str())
132 .unwrap_or(".");
133
134 if let Some(path_map) = compiler.get("paths").and_then(|v| v.as_object()) {
135 for (pattern, targets) in path_map {
136 if let Some(first_target) = targets
137 .as_array()
138 .and_then(|a| a.first())
139 .and_then(|v| v.as_str())
140 {
141 let resolved = if base_url == "." {
142 first_target.to_string()
143 } else {
144 format!("{base_url}/{first_target}")
145 };
146 paths.insert(pattern.clone(), resolved);
147 }
148 }
149 }
150 }
151 }
152 break;
153 }
154 }
155
156 paths
157}
158
159fn load_go_module(root: &Path) -> Option<String> {
160 let go_mod = root.join("go.mod");
161 let content = std::fs::read_to_string(go_mod).ok()?;
162 for line in content.lines() {
163 let trimmed = line.trim();
164 if trimmed.starts_with("module ") {
165 return Some(trimmed.strip_prefix("module ")?.trim().to_string());
166 }
167 }
168 None
169}
170
171fn load_dart_package(root: &Path) -> Option<String> {
172 let pubspec = root.join("pubspec.yaml");
173 let content = std::fs::read_to_string(pubspec).ok()?;
174 for line in content.lines() {
175 let trimmed = line.trim();
176 if let Some(rest) = trimmed.strip_prefix("name:") {
177 let name = rest.trim();
178 if !name.is_empty() {
179 return Some(name.to_string());
180 }
181 }
182 }
183 None
184}
185
186fn normalize_path(path: &Path) -> String {
191 let mut parts: Vec<&str> = Vec::new();
192 for component in path.components() {
193 match component {
194 std::path::Component::ParentDir => {
195 parts.pop();
196 }
197 std::path::Component::Normal(s) => {
198 parts.push(s.to_str().unwrap_or(""));
199 }
200 _ => {}
201 }
202 }
203 parts.join("/")
204}
205
206#[cfg(test)]
207mod tests;