fxrank_lang_python/
module_map.rs1use std::collections::HashSet;
6
7use fxrank_core::frontend::SourceFile;
8
9pub struct PyModuleMap {
10 keys: HashSet<Vec<String>>,
11 pkg_dirs: HashSet<String>,
13}
14
15impl PyModuleMap {
16 pub fn build(files: &[SourceFile]) -> Self {
17 let mut pkg_dirs = HashSet::new();
18 for f in files {
19 if f.path.ends_with("/__init__.py") || f.path == "__init__.py" {
20 pkg_dirs.insert(dir_of(&f.path));
21 }
22 }
23 let mut keys = HashSet::new();
24 for f in files {
25 if !f.path.ends_with(".py") {
26 continue;
27 }
28 if let Some(k) = dotted_key(&f.path, &pkg_dirs) {
29 keys.insert(k);
30 }
31 }
32 Self { keys, pkg_dirs }
33 }
34
35 pub fn module_of(&self, file_path: &str) -> Option<Vec<String>> {
36 if !file_path.ends_with(".py") {
37 return None;
38 }
39 dotted_key(file_path, &self.pkg_dirs)
40 }
41
42 pub fn is_package(&self, file_path: &str) -> bool {
44 file_path.ends_with("/__init__.py") || file_path == "__init__.py"
45 }
46
47 pub fn resolve_absolute(&self, dotted: &str) -> Option<Vec<String>> {
48 let segs: Vec<String> = dotted.split('.').map(|s| s.to_string()).collect();
49 if self.keys.contains(&segs) {
50 Some(segs)
51 } else {
52 None
53 }
54 }
55
56 pub fn resolve_relative(
64 &self,
65 referencing: &[String],
66 is_package: bool,
67 level: usize,
68 suffix: &str,
69 ) -> Option<Vec<String>> {
70 if level == 0 {
71 return None; }
73 let anchor: Vec<String> = if is_package {
74 referencing.to_vec()
75 } else if referencing.is_empty() {
76 return None;
77 } else {
78 referencing[..referencing.len() - 1].to_vec()
79 };
80 if anchor.is_empty() {
85 return None;
86 }
87 let up = level - 1; if up > anchor.len() {
89 return None; }
91 let mut target: Vec<String> = anchor[..anchor.len() - up].to_vec();
92 if !suffix.is_empty() {
93 target.extend(suffix.split('.').map(|s| s.to_string()));
94 }
95 if self.keys.contains(&target) {
96 Some(target)
97 } else {
98 None
99 }
100 }
101}
102
103fn dir_of(path: &str) -> String {
105 match path.rfind('/') {
106 Some(i) => path[..=i].to_string(),
107 None => String::new(),
108 }
109}
110
111fn dotted_key(path: &str, pkg_dirs: &HashSet<String>) -> Option<Vec<String>> {
115 let stem = path.strip_suffix(".py")?;
116 let (dir_part, file_stem) = match stem.rfind('/') {
118 Some(i) => (&stem[..i], &stem[i + 1..]),
119 None => ("", stem),
120 };
121 let dir_segs: Vec<&str> = if dir_part.is_empty() {
124 Vec::new()
125 } else {
126 dir_part.split('/').collect()
127 };
128 let mut first_pkg = dir_segs.len(); for i in (0..dir_segs.len()).rev() {
131 let prefix = format!("{}/", dir_segs[..=i].join("/"));
132 if pkg_dirs.contains(&prefix) {
133 first_pkg = i;
134 } else {
135 break;
136 }
137 }
138 let mut segs: Vec<String> = dir_segs[first_pkg..]
139 .iter()
140 .map(|s| s.to_string())
141 .collect();
142 if file_stem != "__init__" {
143 segs.push(file_stem.to_string());
144 }
145 Some(segs)
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use fxrank_core::frontend::SourceFile;
152 fn sf(p: &str) -> SourceFile {
153 SourceFile {
154 path: p.into(),
155 text: String::new(),
156 }
157 }
158
159 fn batch() -> Vec<SourceFile> {
160 vec![
161 sf("pkg/__init__.py"),
162 sf("pkg/sub/__init__.py"),
163 sf("pkg/sub/mod.py"),
164 sf("pkg/util.py"),
165 sf("top.py"), ]
167 }
168
169 #[test]
170 fn module_key_via_init_packages() {
171 let m = PyModuleMap::build(&batch());
172 assert_eq!(
173 m.module_of("pkg/sub/mod.py"),
174 Some(vec!["pkg".into(), "sub".into(), "mod".into()])
175 );
176 assert_eq!(
177 m.module_of("pkg/util.py"),
178 Some(vec!["pkg".into(), "util".into()])
179 );
180 assert_eq!(
181 m.module_of("pkg/sub/__init__.py"),
182 Some(vec!["pkg".into(), "sub".into()])
183 );
184 assert_eq!(m.module_of("top.py"), Some(vec!["top".into()]));
185 }
186
187 #[test]
188 fn resolve_absolute_in_batch_only() {
189 let m = PyModuleMap::build(&batch());
190 assert_eq!(
191 m.resolve_absolute("pkg.sub.mod"),
192 Some(vec!["pkg".into(), "sub".into(), "mod".into()])
193 );
194 assert_eq!(
195 m.resolve_absolute("pkg.util"),
196 Some(vec!["pkg".into(), "util".into()])
197 );
198 assert_eq!(m.resolve_absolute("os.path"), None); assert_eq!(m.resolve_absolute("pkg.missing"), None);
200 }
201
202 #[test]
203 fn resolve_relative_via_package_walk() {
204 let m = PyModuleMap::build(&batch());
205 let mod_ref = vec!["pkg".to_string(), "sub".into(), "mod".into()]; assert_eq!(
209 m.resolve_relative(&mod_ref, false, 2, "util"),
210 Some(vec!["pkg".into(), "util".into()])
211 );
212 assert_eq!(
214 m.resolve_relative(&mod_ref, false, 1, "mod"),
215 Some(vec!["pkg".into(), "sub".into(), "mod".into()])
216 );
217 assert_eq!(m.resolve_relative(&["top".into()], false, 3, "x"), None);
219 }
220
221 #[test]
222 fn resolve_relative_from_package_init_anchors_at_itself() {
223 let m = PyModuleMap::build(&batch());
227 let pkg_ref = vec!["pkg".to_string(), "sub".into()]; assert_eq!(
229 m.resolve_relative(&pkg_ref, true, 1, "mod"),
230 Some(vec!["pkg".into(), "sub".into(), "mod".into()])
231 );
232 assert_eq!(
234 m.resolve_relative(&pkg_ref, true, 2, "util"),
235 Some(vec!["pkg".into(), "util".into()])
236 );
237 }
238
239 #[test]
240 fn relative_import_from_top_level_module_is_none() {
241 let m = PyModuleMap::build(&batch());
244 assert_eq!(m.resolve_relative(&["top".into()], false, 1, "util"), None);
245 }
246}