use crate::crate_name::CrateName;
use crate::error::ParseError;
use crate::file_path::WorkspaceFilePath;
use crate::path::SymbolPath;
use crate::resolver::{CrateLayout, EntryPoint};
#[derive(Debug, Clone)]
pub struct SymbolPathResolver {
crate_name: CrateName,
layout: CrateLayout,
entry_point: EntryPoint,
}
impl SymbolPathResolver {
pub fn new(crate_name: impl AsRef<str>) -> Self {
Self {
crate_name: CrateName::new_unchecked(crate_name.as_ref()),
layout: CrateLayout::Root,
entry_point: EntryPoint::Lib,
}
}
pub fn with_layout(crate_name: impl AsRef<str>, layout: CrateLayout) -> Self {
Self {
crate_name: CrateName::new_unchecked(crate_name.as_ref()),
layout,
entry_point: EntryPoint::Lib,
}
}
pub fn with_layout_and_entry(
crate_name: impl AsRef<str>,
layout: CrateLayout,
entry_point: EntryPoint,
) -> Self {
Self {
crate_name: CrateName::new_unchecked(crate_name.as_ref()),
layout,
entry_point,
}
}
pub fn from_crate_name(crate_name: CrateName) -> Self {
Self {
crate_name,
layout: CrateLayout::Root,
entry_point: EntryPoint::Lib,
}
}
pub fn from_crate_name_with_layout(crate_name: CrateName, layout: CrateLayout) -> Self {
Self {
crate_name,
layout,
entry_point: EntryPoint::Lib,
}
}
pub fn from_crate_name_with_layout_and_entry(
crate_name: CrateName,
layout: CrateLayout,
entry_point: EntryPoint,
) -> Self {
Self {
crate_name,
layout,
entry_point,
}
}
pub fn from_workspace_path(path: &WorkspaceFilePath) -> Result<Self, ParseError> {
Ok(Self {
crate_name: path.crate_name().clone(),
layout: CrateLayout::from_workspace_file_path(path),
entry_point: EntryPoint::from_path(path.as_relative()),
})
}
pub fn crate_name(&self) -> &CrateName {
&self.crate_name
}
pub fn entry_point(&self) -> EntryPoint {
self.entry_point
}
pub fn layout(&self) -> &CrateLayout {
&self.layout
}
pub fn resolve_module(&self, path: &WorkspaceFilePath) -> Result<SymbolPath, ParseError> {
SymbolPath::from_file_path(&self.crate_name, path)
}
pub fn resolve_item(
&self,
path: &WorkspaceFilePath,
item_name: &str,
) -> Result<SymbolPath, ParseError> {
let module = self.resolve_module(path)?;
module.child(item_name)
}
pub fn resolve_nested(
&self,
path: &WorkspaceFilePath,
segments: &[&str],
) -> Result<SymbolPath, ParseError> {
let mut current = self.resolve_module(path)?;
for seg in segments {
current = current.child(*seg)?;
}
Ok(current)
}
pub fn module_path_str(&self, path: &WorkspaceFilePath) -> String {
SymbolPath::module_path_str(path)
}
pub fn to_workspace_file_path(
&self,
symbol_path: &SymbolPath,
workspace_root: std::sync::Arc<std::path::Path>,
) -> WorkspaceFilePath {
let relative = self.symbol_path_to_relative(symbol_path);
WorkspaceFilePath::new_unchecked(
std::path::PathBuf::from(relative),
workspace_root,
self.crate_name.clone(),
)
}
pub fn symbol_path_to_relative(&self, symbol_path: &SymbolPath) -> String {
let depth = symbol_path.depth();
let src_dir = self.layout.src_dir();
let src_prefix = src_dir.to_string_lossy();
let entry_file = self.entry_point.file_name();
if depth == 0 {
return format!("{}/{}", src_prefix, entry_file);
}
match depth {
0..=2 => format!("{}/{}", src_prefix, entry_file),
_ => {
let mut parts = Vec::new();
for i in 1..=(depth - 2) {
if let Some(seg) = symbol_path.segment(i) {
parts.push(seg.name().to_string());
}
}
if parts.is_empty() {
format!("{}/{}", src_prefix, entry_file)
} else {
format!("{}/{}.rs", src_prefix, parts.join("/"))
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_path(relative: &str) -> WorkspaceFilePath {
WorkspaceFilePath::new_for_test(relative, "/workspace", "my_crate")
}
#[test]
fn test_new() {
let resolver = SymbolPathResolver::new("my_crate");
assert_eq!(resolver.crate_name().as_str(), "my_crate");
}
#[test]
fn test_from_workspace_path() {
let path = make_path("src/lib.rs");
let resolver = SymbolPathResolver::from_workspace_path(&path).unwrap();
assert_eq!(resolver.crate_name().as_str(), "my_crate");
}
#[test]
fn test_resolve_module_lib() {
let resolver = SymbolPathResolver::new("my_crate");
let path = make_path("src/lib.rs");
let symbol = resolver.resolve_module(&path).unwrap();
assert_eq!(symbol.to_string(), "my_crate");
}
#[test]
fn test_resolve_module_nested() {
let resolver = SymbolPathResolver::new("my_crate");
let path = make_path("src/foo/bar.rs");
let symbol = resolver.resolve_module(&path).unwrap();
assert_eq!(symbol.to_string(), "my_crate::foo::bar");
}
#[test]
fn test_resolve_item() {
let resolver = SymbolPathResolver::new("my_crate");
let path = make_path("src/lib.rs");
let symbol = resolver.resolve_item(&path, "MyStruct").unwrap();
assert_eq!(symbol.to_string(), "my_crate::MyStruct");
}
#[test]
fn test_resolve_nested() {
let resolver = SymbolPathResolver::new("my_crate");
let path = make_path("src/lib.rs");
let symbol = resolver.resolve_nested(&path, &["Foo", "new"]).unwrap();
assert_eq!(symbol.to_string(), "my_crate::Foo::new");
}
#[test]
fn test_module_path_str() {
let resolver = SymbolPathResolver::new("my_crate");
let path = make_path("src/foo/bar.rs");
assert_eq!(resolver.module_path_str(&path), "my_crate::foo::bar");
}
#[test]
fn test_symbol_path_to_relative_crate_root() {
let resolver = SymbolPathResolver::new("test_crate");
let symbol = SymbolPath::parse("test_crate").unwrap();
assert_eq!(resolver.symbol_path_to_relative(&symbol), "src/lib.rs");
}
#[test]
fn test_symbol_path_to_relative_crate_item() {
let resolver = SymbolPathResolver::new("test_crate");
let symbol = SymbolPath::parse("test_crate::Config").unwrap();
assert_eq!(resolver.symbol_path_to_relative(&symbol), "src/lib.rs");
}
#[test]
fn test_symbol_path_to_relative_module_item() {
let resolver = SymbolPathResolver::new("test_crate");
let symbol = SymbolPath::parse("test_crate::models::User").unwrap();
assert_eq!(resolver.symbol_path_to_relative(&symbol), "src/models.rs");
}
#[test]
fn test_symbol_path_to_relative_nested_module() {
let resolver = SymbolPathResolver::new("test_crate");
let symbol = SymbolPath::parse("test_crate::foo::bar::Baz").unwrap();
assert_eq!(resolver.symbol_path_to_relative(&symbol), "src/foo/bar.rs");
}
#[test]
fn test_symbol_path_to_relative_deep_nested() {
let resolver = SymbolPathResolver::new("test_crate");
let symbol = SymbolPath::parse("test_crate::a::b::c::Item").unwrap();
assert_eq!(resolver.symbol_path_to_relative(&symbol), "src/a/b/c.rs");
}
#[test]
fn test_to_workspace_file_path() {
use std::path::Path;
use std::sync::Arc;
let resolver = SymbolPathResolver::new("test_crate");
let symbol = SymbolPath::parse("test_crate::models::User").unwrap();
let workspace_root = Arc::from(Path::new("/project"));
let file_path = resolver.to_workspace_file_path(&symbol, workspace_root);
assert_eq!(file_path.as_relative().to_str().unwrap(), "src/models.rs");
}
#[test]
fn test_roundtrip_conversion() {
use std::path::Path;
use std::sync::Arc;
let resolver = SymbolPathResolver::new("my_crate");
let workspace_root = Arc::from(Path::new("/project"));
let original_file = make_path("src/foo/bar.rs");
let symbol = resolver.resolve_module(&original_file).unwrap();
assert_eq!(symbol.to_string(), "my_crate::foo::bar");
let item_symbol = symbol.child("MyStruct").unwrap();
assert_eq!(item_symbol.to_string(), "my_crate::foo::bar::MyStruct");
let recovered_file = resolver.to_workspace_file_path(&item_symbol, workspace_root);
assert_eq!(
recovered_file.as_relative().to_str().unwrap(),
"src/foo/bar.rs"
);
}
#[test]
fn test_with_layout_in_crates() {
let resolver =
SymbolPathResolver::with_layout("my_crate", CrateLayout::in_crates("my-crate"));
let symbol = SymbolPath::parse("my_crate::Config").unwrap();
assert_eq!(
resolver.symbol_path_to_relative(&symbol),
"crates/my-crate/src/lib.rs"
);
}
#[test]
fn test_with_layout_in_crates_nested() {
let resolver =
SymbolPathResolver::with_layout("my_crate", CrateLayout::in_crates("my-crate"));
let symbol = SymbolPath::parse("my_crate::models::User").unwrap();
assert_eq!(
resolver.symbol_path_to_relative(&symbol),
"crates/my-crate/src/models.rs"
);
}
#[test]
fn test_with_layout_custom() {
let resolver =
SymbolPathResolver::with_layout("my_crate", CrateLayout::custom("packages/core"));
let symbol = SymbolPath::parse("my_crate::models::User").unwrap();
assert_eq!(
resolver.symbol_path_to_relative(&symbol),
"packages/core/src/models.rs"
);
}
#[test]
fn test_from_workspace_path_infers_layout() {
let path =
WorkspaceFilePath::new_for_test("crates/my-crate/src/lib.rs", "/workspace", "my_crate");
let resolver = SymbolPathResolver::from_workspace_path(&path).unwrap();
assert_eq!(resolver.crate_name().as_str(), "my_crate");
assert_eq!(
resolver.layout(),
&CrateLayout::InCrates {
crate_dir_name: "my-crate".to_string()
}
);
}
#[test]
fn test_roundtrip_with_workspace_layout() {
use std::path::Path;
use std::sync::Arc;
let resolver =
SymbolPathResolver::with_layout("my_crate", CrateLayout::in_crates("my-crate"));
let workspace_root = Arc::from(Path::new("/project"));
let symbol = SymbolPath::parse("my_crate::foo::bar::Item").unwrap();
let file = resolver.to_workspace_file_path(&symbol, workspace_root);
assert_eq!(
file.as_relative().to_str().unwrap(),
"crates/my-crate/src/foo/bar.rs"
);
}
}