mati_core/analysis/resolvers/
python.rs1use std::path::Path;
12
13use super::{FileIndex, LanguageResolver};
14use crate::analysis::parser::ImportStatement;
15use crate::analysis::walker::Language;
16
17pub struct PythonResolver;
19
20impl LanguageResolver for PythonResolver {
21 fn resolve(
22 &self,
23 import: &ImportStatement,
24 importing_file: &str,
25 file_index: &FileIndex,
26 ) -> Option<String> {
27 resolve_python(&import.path, importing_file, file_index)
28 }
29
30 fn language(&self) -> Language {
31 Language::Python
32 }
33
34 fn name(&self) -> &'static str {
35 "python"
36 }
37}
38
39fn resolve_python(
41 import_path: &str,
42 importing_file: &str,
43 file_index: &FileIndex,
44) -> Option<String> {
45 let (base_dir, module_path) = if let Some(stripped) = import_path.strip_prefix('.') {
46 let parent = Path::new(importing_file)
48 .parent()
49 .map(|p| p.to_string_lossy().into_owned())
50 .unwrap_or_default();
51
52 if let Some(double_stripped) = stripped.strip_prefix('.') {
54 let grandparent = Path::new(&parent)
55 .parent()
56 .map(|p| p.to_string_lossy().into_owned())
57 .unwrap_or_default();
58 (grandparent, double_stripped)
59 } else {
60 (parent, stripped)
61 }
62 } else {
63 (String::new(), import_path)
64 };
65
66 let rel = module_path.replace('.', "/");
68
69 let prefix = if base_dir.is_empty() {
70 rel
71 } else {
72 format!("{base_dir}/{rel}")
73 };
74
75 let py_file = format!("{prefix}.py");
77 if file_index.contains(&py_file) {
78 return Some(py_file);
79 }
80
81 let init_file = format!("{prefix}/__init__.py");
83 if file_index.contains(&init_file) {
84 return Some(init_file);
85 }
86
87 None
88}
89
90#[cfg(test)]
93mod tests {
94 use super::*;
95 use crate::analysis::parser::import::ImportKind;
96
97 fn idx(paths: &[&str]) -> FileIndex {
98 FileIndex::new(paths.iter().map(|s| s.to_string()))
99 }
100
101 fn import(path: &str, kind: ImportKind) -> ImportStatement {
102 ImportStatement::new(path, kind, 1)
103 }
104
105 #[test]
106 fn absolute_import_resolves() {
107 let file_index = idx(&["app/main.py", "app/utils.py"]);
108 let resolver = PythonResolver;
109 let result = resolver.resolve(
110 &import("app.utils", ImportKind::Normal),
111 "app/main.py",
112 &file_index,
113 );
114 assert_eq!(result, Some("app/utils.py".into()));
115 }
116
117 #[test]
118 fn relative_import_resolves() {
119 let file_index = idx(&["app/main.py", "app/helpers.py"]);
120 let resolver = PythonResolver;
121 let result = resolver.resolve(
122 &import(".helpers", ImportKind::Relative),
123 "app/main.py",
124 &file_index,
125 );
126 assert_eq!(result, Some("app/helpers.py".into()));
127 }
128
129 #[test]
130 fn package_init_resolves() {
131 let file_index = idx(&["main.py", "pkg/__init__.py"]);
132 let resolver = PythonResolver;
133 let result = resolver.resolve(&import("pkg", ImportKind::Normal), "main.py", &file_index);
134 assert_eq!(result, Some("pkg/__init__.py".into()));
135 }
136
137 #[test]
138 fn double_dot_relative() {
139 let file_index = idx(&["app/sub/deep.py", "app/utils.py"]);
140 let resolver = PythonResolver;
141 let result = resolver.resolve(
142 &import("..utils", ImportKind::Relative),
143 "app/sub/deep.py",
144 &file_index,
145 );
146 assert_eq!(result, Some("app/utils.py".into()));
147 }
148
149 #[test]
150 fn unresolvable_returns_none() {
151 let file_index = idx(&["app/main.py"]);
152 let resolver = PythonResolver;
153 let result = resolver.resolve(
154 &import("app.nonexistent", ImportKind::Normal),
155 "app/main.py",
156 &file_index,
157 );
158 assert_eq!(result, None);
159 }
160}