use std::collections::HashSet;
use fxrank_core::frontend::SourceFile;
pub struct PyModuleMap {
keys: HashSet<Vec<String>>,
pkg_dirs: HashSet<String>,
}
impl PyModuleMap {
pub fn build(files: &[SourceFile]) -> Self {
let mut pkg_dirs = HashSet::new();
for f in files {
if f.path.ends_with("/__init__.py") || f.path == "__init__.py" {
pkg_dirs.insert(dir_of(&f.path));
}
}
let mut keys = HashSet::new();
for f in files {
if !f.path.ends_with(".py") {
continue;
}
if let Some(k) = dotted_key(&f.path, &pkg_dirs) {
keys.insert(k);
}
}
Self { keys, pkg_dirs }
}
pub fn module_of(&self, file_path: &str) -> Option<Vec<String>> {
if !file_path.ends_with(".py") {
return None;
}
dotted_key(file_path, &self.pkg_dirs)
}
pub fn is_package(&self, file_path: &str) -> bool {
file_path.ends_with("/__init__.py") || file_path == "__init__.py"
}
pub fn resolve_absolute(&self, dotted: &str) -> Option<Vec<String>> {
let segs: Vec<String> = dotted.split('.').map(|s| s.to_string()).collect();
if self.keys.contains(&segs) {
Some(segs)
} else {
None
}
}
pub fn resolve_relative(
&self,
referencing: &[String],
is_package: bool,
level: usize,
suffix: &str,
) -> Option<Vec<String>> {
if level == 0 {
return None; }
let anchor: Vec<String> = if is_package {
referencing.to_vec()
} else if referencing.is_empty() {
return None;
} else {
referencing[..referencing.len() - 1].to_vec()
};
if anchor.is_empty() {
return None;
}
let up = level - 1; if up > anchor.len() {
return None; }
let mut target: Vec<String> = anchor[..anchor.len() - up].to_vec();
if !suffix.is_empty() {
target.extend(suffix.split('.').map(|s| s.to_string()));
}
if self.keys.contains(&target) {
Some(target)
} else {
None
}
}
}
fn dir_of(path: &str) -> String {
match path.rfind('/') {
Some(i) => path[..=i].to_string(),
None => String::new(),
}
}
fn dotted_key(path: &str, pkg_dirs: &HashSet<String>) -> Option<Vec<String>> {
let stem = path.strip_suffix(".py")?;
let (dir_part, file_stem) = match stem.rfind('/') {
Some(i) => (&stem[..i], &stem[i + 1..]),
None => ("", stem),
};
let dir_segs: Vec<&str> = if dir_part.is_empty() {
Vec::new()
} else {
dir_part.split('/').collect()
};
let mut first_pkg = dir_segs.len(); for i in (0..dir_segs.len()).rev() {
let prefix = format!("{}/", dir_segs[..=i].join("/"));
if pkg_dirs.contains(&prefix) {
first_pkg = i;
} else {
break;
}
}
let mut segs: Vec<String> = dir_segs[first_pkg..]
.iter()
.map(|s| s.to_string())
.collect();
if file_stem != "__init__" {
segs.push(file_stem.to_string());
}
Some(segs)
}
#[cfg(test)]
mod tests {
use super::*;
use fxrank_core::frontend::SourceFile;
fn sf(p: &str) -> SourceFile {
SourceFile {
path: p.into(),
text: String::new(),
}
}
fn batch() -> Vec<SourceFile> {
vec![
sf("pkg/__init__.py"),
sf("pkg/sub/__init__.py"),
sf("pkg/sub/mod.py"),
sf("pkg/util.py"),
sf("top.py"), ]
}
#[test]
fn module_key_via_init_packages() {
let m = PyModuleMap::build(&batch());
assert_eq!(
m.module_of("pkg/sub/mod.py"),
Some(vec!["pkg".into(), "sub".into(), "mod".into()])
);
assert_eq!(
m.module_of("pkg/util.py"),
Some(vec!["pkg".into(), "util".into()])
);
assert_eq!(
m.module_of("pkg/sub/__init__.py"),
Some(vec!["pkg".into(), "sub".into()])
);
assert_eq!(m.module_of("top.py"), Some(vec!["top".into()]));
}
#[test]
fn resolve_absolute_in_batch_only() {
let m = PyModuleMap::build(&batch());
assert_eq!(
m.resolve_absolute("pkg.sub.mod"),
Some(vec!["pkg".into(), "sub".into(), "mod".into()])
);
assert_eq!(
m.resolve_absolute("pkg.util"),
Some(vec!["pkg".into(), "util".into()])
);
assert_eq!(m.resolve_absolute("os.path"), None); assert_eq!(m.resolve_absolute("pkg.missing"), None);
}
#[test]
fn resolve_relative_via_package_walk() {
let m = PyModuleMap::build(&batch());
let mod_ref = vec!["pkg".to_string(), "sub".into(), "mod".into()]; assert_eq!(
m.resolve_relative(&mod_ref, false, 2, "util"),
Some(vec!["pkg".into(), "util".into()])
);
assert_eq!(
m.resolve_relative(&mod_ref, false, 1, "mod"),
Some(vec!["pkg".into(), "sub".into(), "mod".into()])
);
assert_eq!(m.resolve_relative(&["top".into()], false, 3, "x"), None);
}
#[test]
fn resolve_relative_from_package_init_anchors_at_itself() {
let m = PyModuleMap::build(&batch());
let pkg_ref = vec!["pkg".to_string(), "sub".into()]; assert_eq!(
m.resolve_relative(&pkg_ref, true, 1, "mod"),
Some(vec!["pkg".into(), "sub".into(), "mod".into()])
);
assert_eq!(
m.resolve_relative(&pkg_ref, true, 2, "util"),
Some(vec!["pkg".into(), "util".into()])
);
}
#[test]
fn relative_import_from_top_level_module_is_none() {
let m = PyModuleMap::build(&batch());
assert_eq!(m.resolve_relative(&["top".into()], false, 1, "util"), None);
}
}