use crate::graph::error::{GraphBuilderError, GraphResult};
use crate::graph::node::Span;
use std::path::{Component, Path};
pub fn resolve_import_path(source_file: &Path, import_path: &str) -> GraphResult<String> {
let import_path = import_path.trim();
if import_path.is_empty() {
return Err(GraphBuilderError::ParseError {
span: Span::default(),
reason: "Empty import path".to_string(),
});
}
if !import_path.starts_with('.') && !import_path.starts_with('/') {
return Ok(import_path.to_string());
}
if import_path.starts_with('/') {
let path = Path::new(import_path);
return normalize_path(path).ok_or_else(|| GraphBuilderError::ParseError {
span: Span::default(),
reason: format!("Failed to normalize absolute path: {import_path}"),
});
}
let source_dir = source_file
.parent()
.ok_or_else(|| GraphBuilderError::ParseError {
span: Span::default(),
reason: format!(
"Source file has no parent directory: {}",
source_file.display()
),
})?;
let full_path = source_dir.join(import_path);
normalize_path(&full_path).ok_or_else(|| GraphBuilderError::ParseError {
span: Span::default(),
reason: format!("Failed to normalize import path: {}", full_path.display()),
})
}
pub fn normalize_path(path: &Path) -> Option<String> {
let mut components = Vec::new();
for component in path.components() {
match component {
Component::CurDir => {
}
Component::ParentDir => {
if components.is_empty() {
return None;
}
components.pop();
}
Component::Normal(name) => {
components.push(name.to_string_lossy().to_string());
}
Component::RootDir => {
components.clear();
components.push(String::new()); }
Component::Prefix(_) => {
components.push(component.as_os_str().to_string_lossy().to_string());
}
}
}
if components.is_empty() {
Some(".".to_string())
} else if components.len() == 1 && components[0].is_empty() {
Some("/".to_string())
} else {
let is_absolute = !components.is_empty() && components[0].is_empty();
let parts: Vec<&str> = components
.iter()
.filter(|s| !s.is_empty())
.map(std::string::String::as_str)
.collect();
if is_absolute {
Some(format!("/{}", parts.join("/")))
} else {
Some(parts.join("/"))
}
}
}
pub fn resolve_python_import(
source_file: &Path,
import_path: &str,
_is_from_import: bool,
) -> GraphResult<String> {
let import_path = import_path.trim();
if import_path.is_empty() {
return Err(GraphBuilderError::ParseError {
span: Span::default(),
reason: "Empty Python import path".to_string(),
});
}
if !import_path.starts_with('.') {
return Ok(import_path.to_string());
}
let leading_dots = import_path.chars().take_while(|&c| c == '.').count();
let module_name = &import_path[leading_dots..];
let source_dir = source_file
.parent()
.ok_or_else(|| GraphBuilderError::ParseError {
span: Span::default(),
reason: format!(
"Python file has no parent directory: {}",
source_file.display()
),
})?;
let mut target_dir = source_dir.to_path_buf();
for _ in 1..leading_dots {
target_dir = target_dir
.parent()
.ok_or_else(|| GraphBuilderError::ParseError {
span: Span::default(),
reason: format!("Too many leading dots in import: {import_path}"),
})?
.to_path_buf();
}
let resolved_path = if module_name.is_empty() {
target_dir
} else {
let module_path = module_name.replace('.', "/");
target_dir.join(module_path)
};
normalize_path(&resolved_path).ok_or_else(|| GraphBuilderError::ParseError {
span: Span::default(),
reason: format!(
"Failed to normalize Python import path: {}",
resolved_path.display()
),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_import_path_relative() {
let source = Path::new("src/components/Button.js");
assert_eq!(
resolve_import_path(source, "./Icon").unwrap(),
"src/components/Icon"
);
assert_eq!(
resolve_import_path(source, "../utils/helpers").unwrap(),
"src/utils/helpers"
);
assert_eq!(
resolve_import_path(source, "./../lib/db").unwrap(),
"src/lib/db"
);
}
#[test]
fn test_resolve_import_path_package() {
let source = Path::new("src/app/main.js");
assert_eq!(resolve_import_path(source, "react").unwrap(), "react");
assert_eq!(
resolve_import_path(source, "@types/node").unwrap(),
"@types/node"
);
assert_eq!(
resolve_import_path(source, "lodash/fp").unwrap(),
"lodash/fp"
);
}
#[test]
fn test_resolve_import_path_absolute() {
let source = Path::new("src/app/main.js");
assert_eq!(
resolve_import_path(source, "/usr/lib/module").unwrap(),
"/usr/lib/module"
);
}
#[test]
fn test_normalize_path() {
assert_eq!(
normalize_path(Path::new("src/./app/./main.js")).unwrap(),
"src/app/main.js"
);
assert_eq!(
normalize_path(Path::new("src/app/../lib/db")).unwrap(),
"src/lib/db"
);
assert_eq!(
normalize_path(Path::new("a/b/c/../../d/./e")).unwrap(),
"a/d/e"
);
}
#[test]
fn test_normalize_path_invalid() {
assert!(normalize_path(Path::new("a/../..")).is_none());
}
#[test]
fn test_resolve_python_import_absolute() {
let source = Path::new("mypackage/module.py");
assert_eq!(resolve_python_import(source, "os", false).unwrap(), "os");
assert_eq!(
resolve_python_import(source, "os.path", false).unwrap(),
"os.path"
);
}
#[test]
fn test_resolve_python_import_relative() {
let source = Path::new("mypackage/subpkg/module.py");
assert_eq!(
resolve_python_import(source, ".", true).unwrap(),
"mypackage/subpkg"
);
assert_eq!(
resolve_python_import(source, ".sibling", true).unwrap(),
"mypackage/subpkg/sibling"
);
assert_eq!(
resolve_python_import(source, "..", true).unwrap(),
"mypackage"
);
assert_eq!(
resolve_python_import(source, "..other_subpkg", true).unwrap(),
"mypackage/other_subpkg"
);
}
#[test]
fn test_resolve_python_import_nested_dots() {
let source = Path::new("pkg/sub1/sub2/module.py");
assert_eq!(
resolve_python_import(source, "...toplevel", true).unwrap(),
"pkg/toplevel"
);
}
#[test]
fn test_different_path_separators() {
let source_unix = Path::new("src/components/Button.js");
assert_eq!(
resolve_import_path(source_unix, "./Icon").unwrap(),
"src/components/Icon"
);
}
#[test]
fn test_relative_import_collision_fix() {
let file1 = Path::new("src/foo/index.js");
let file2 = Path::new("src/bar/index.js");
let resolved1 = resolve_import_path(file1, "./utils").unwrap();
let resolved2 = resolve_import_path(file2, "./utils").unwrap();
assert_eq!(resolved1, "src/foo/utils");
assert_eq!(resolved2, "src/bar/utils");
assert_ne!(resolved1, resolved2);
}
}