Skip to main content

lean_ctx/core/import_resolver/
mod.rs

1//! Import-to-file resolution (AST-driven import strings → project paths).
2//!
3//! Resolves import strings from `deep_queries::ImportInfo` to actual file paths
4//! within a project. Handles language-specific module systems:
5//! - TypeScript/JavaScript: relative paths, index files, package.json, tsconfig paths
6//! - Python: dotted modules, __init__.py, relative imports
7//! - Rust: crate/super/self resolution, mod.rs
8//! - Go: go.mod module path, package = directory
9//! - Java: package-to-directory mapping
10//! - C/C++: local includes (best-effort)
11//! - Ruby: require_relative (best-effort)
12//! - PHP: include/require (best-effort)
13//! - Bash: source/. (best-effort)
14//! - Dart: relative + `package:<name>/` (best-effort)
15//! - Zig: @import("path.zig") (best-effort)
16
17use 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
116// ---------------------------------------------------------------------------
117// Config Loaders
118// ---------------------------------------------------------------------------
119
120fn 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
186// ---------------------------------------------------------------------------
187// Helpers
188// ---------------------------------------------------------------------------
189
190fn 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;