use std::path::{Component, Path, PathBuf};
use std::sync::Arc;
use crate::crate_name::CrateName;
use crate::error::ResolveError;
use crate::file_path::WorkspaceFilePath;
use crate::metadata::WorkspaceMetadataProvider;
use crate::path::SymbolPath;
use crate::symbol_resolver::SymbolPathResolver;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EntryPoint {
#[default]
Lib,
Bin,
}
impl EntryPoint {
pub fn file_name(&self) -> &'static str {
match self {
Self::Lib => "lib.rs",
Self::Bin => "main.rs",
}
}
pub fn from_path(path: &std::path::Path) -> Self {
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if file_name == "main.rs" {
return Self::Bin;
}
}
Self::Lib
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WorkspaceType {
#[default]
Workspace,
Crate,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum CrateLayout {
#[default]
Root,
InCrates {
crate_dir_name: String,
},
Custom {
prefix: PathBuf,
},
}
impl CrateLayout {
pub fn in_crates(crate_dir_name: impl Into<String>) -> Self {
Self::InCrates {
crate_dir_name: crate_dir_name.into(),
}
}
pub fn custom(prefix: impl Into<PathBuf>) -> Self {
Self::Custom {
prefix: prefix.into(),
}
}
pub fn src_dir(&self) -> PathBuf {
match self {
Self::Root => PathBuf::from("src"),
Self::InCrates { crate_dir_name } => {
PathBuf::from(format!("crates/{}/src", crate_dir_name))
}
Self::Custom { prefix } => prefix.join("src"),
}
}
pub fn to_workspace_relative(&self, crate_relative: impl AsRef<Path>) -> PathBuf {
let crate_relative = crate_relative.as_ref();
match self {
Self::Root => {
crate_relative.to_path_buf()
}
Self::InCrates { crate_dir_name } => {
PathBuf::from(format!("crates/{}", crate_dir_name)).join(crate_relative)
}
Self::Custom { prefix } => {
prefix.join(crate_relative)
}
}
}
pub fn from_workspace_file_path(path: &WorkspaceFilePath) -> Self {
let path_str = path.as_relative().to_string_lossy();
if let Some(idx) = path_str.find("crates/") {
let after_crates = &path_str[idx + 7..];
if let Some(end_idx) = after_crates.find('/') {
let crate_dir_name = &after_crates[..end_idx];
return Self::InCrates {
crate_dir_name: crate_dir_name.to_string(),
};
}
}
if path_str.starts_with("src/") {
return Self::Root;
}
if let Some(idx) = path_str.find("/src/") {
let prefix = &path_str[..idx];
return Self::Custom {
prefix: PathBuf::from(prefix),
};
}
Self::Root
}
}
#[derive(Debug, Clone)]
pub struct WorkspacePathResolver {
workspace_root: Arc<Path>,
workspace_type: WorkspaceType,
}
impl WorkspacePathResolver {
pub fn new(workspace_root: PathBuf) -> Self {
Self {
workspace_root: Arc::from(workspace_root),
workspace_type: WorkspaceType::default(),
}
}
pub fn with_type(workspace_root: PathBuf, workspace_type: WorkspaceType) -> Self {
Self {
workspace_root: Arc::from(workspace_root),
workspace_type,
}
}
pub fn workspace_type(&self) -> WorkspaceType {
self.workspace_type
}
pub fn resolve_with_provider<P: WorkspaceMetadataProvider>(
&self,
path: impl AsRef<Path>,
provider: &P,
) -> Result<WorkspaceFilePath, ResolveError> {
let path = path.as_ref();
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()?.join(path)
};
let normalized = normalize_path(&absolute);
let relative = normalized
.strip_prefix(&*self.workspace_root)
.map_err(|_| ResolveError::OutsideWorkspace {
path: normalized.clone(),
workspace: self.workspace_root.to_path_buf(),
})?
.to_path_buf();
let temp_crate = CrateName::new_unchecked("__temp__");
let temp_path = WorkspaceFilePath::new_unchecked(
relative.clone(),
Arc::clone(&self.workspace_root),
temp_crate,
);
let crate_name = provider
.crate_for_file(&temp_path)
.ok_or_else(|| ResolveError::CrateNotFound(normalized.clone()))?;
Ok(WorkspaceFilePath::new_unchecked(
relative,
Arc::clone(&self.workspace_root),
crate_name,
))
}
pub fn resolve_strict_with_provider<P: WorkspaceMetadataProvider>(
&self,
path: impl AsRef<Path>,
provider: &P,
) -> Result<WorkspaceFilePath, ResolveError> {
let workspace_path = self.resolve_with_provider(path, provider)?;
let absolute = workspace_path.to_absolute();
if !absolute.exists() {
return Err(ResolveError::FileNotFound(absolute));
}
Ok(workspace_path)
}
pub fn resolve_relative_with_crate(
&self,
relative: impl AsRef<Path>,
crate_name: CrateName,
) -> WorkspaceFilePath {
let relative = relative.as_ref();
let normalized = normalize_path(relative);
WorkspaceFilePath::new_unchecked(normalized, Arc::clone(&self.workspace_root), crate_name)
}
pub fn resolve_relative_with_provider<P: WorkspaceMetadataProvider>(
&self,
relative: impl AsRef<Path>,
provider: &P,
) -> Option<WorkspaceFilePath> {
let relative = relative.as_ref();
let normalized = normalize_path(relative);
let temp_crate = CrateName::new_unchecked("__temp__");
let temp_path = WorkspaceFilePath::new_unchecked(
normalized.clone(),
Arc::clone(&self.workspace_root),
temp_crate,
);
let crate_name = provider.crate_for_file(&temp_path)?;
Some(WorkspaceFilePath::new_unchecked(
normalized,
Arc::clone(&self.workspace_root),
crate_name,
))
}
pub fn workspace_root(&self) -> &Path {
&self.workspace_root
}
pub fn workspace_root_arc(&self) -> Arc<Path> {
Arc::clone(&self.workspace_root)
}
pub fn module_to_file(
&self,
module_path: &SymbolPath,
crate_name: &CrateName,
span_file: Option<&WorkspaceFilePath>,
) -> WorkspaceFilePath {
if module_path.depth() == 1 {
if let Some(span_file) = span_file {
return span_file.clone();
}
}
let symbol_resolver = SymbolPathResolver::from_crate_name(crate_name.clone());
if let Ok(virtual_child) = module_path.child("_") {
symbol_resolver.to_workspace_file_path(&virtual_child, self.workspace_root_arc())
} else {
symbol_resolver.to_workspace_file_path(module_path, self.workspace_root_arc())
}
}
pub fn resolve(&self, path: impl AsRef<Path>) -> Result<WorkspaceFilePath, ResolveError> {
let path = path.as_ref();
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()?.join(path)
};
let normalized = normalize_path(&absolute);
let relative = normalized
.strip_prefix(&*self.workspace_root)
.map_err(|_| ResolveError::OutsideWorkspace {
path: normalized.clone(),
workspace: self.workspace_root.to_path_buf(),
})?
.to_path_buf();
let crate_name = infer_crate_name(&relative);
Ok(WorkspaceFilePath::new_unchecked(
relative,
Arc::clone(&self.workspace_root),
crate_name,
))
}
pub fn resolve_relative(&self, relative: impl AsRef<Path>) -> Option<WorkspaceFilePath> {
let relative = relative.as_ref();
let normalized = normalize_path(relative);
let crate_name = infer_crate_name(&normalized);
Some(WorkspaceFilePath::new_unchecked(
normalized,
Arc::clone(&self.workspace_root),
crate_name,
))
}
pub fn validate_crate_path(
&self,
path: &str,
workspace_members: &[String],
) -> Result<(), ResolveError> {
if self.workspace_type != WorkspaceType::Workspace {
return Ok(());
}
if !path.starts_with("crate::") {
return Ok(());
}
if workspace_members.len() <= 1 {
return Ok(());
}
let first_member = workspace_members.first().cloned().unwrap_or_default();
let crate_name = first_member
.split('/')
.next_back()
.unwrap_or(&first_member)
.to_string();
let module_suffix = path.strip_prefix("crate::").unwrap_or("");
let example_file_path = if module_suffix.is_empty() {
format!("{}/src/lib.rs", first_member)
} else {
format!(
"{}/src/{}.rs",
first_member,
module_suffix.replace("::", "/")
)
};
Err(ResolveError::AmbiguousCratePath {
path: path.to_string(),
example_crate_path: first_member,
example_file_path,
example_crate_name: crate_name,
})
}
}
fn infer_crate_name(path: &Path) -> CrateName {
let path_str = path.to_string_lossy();
if let Some(idx) = path_str.find("crates/") {
let after_crates = &path_str[idx + 7..];
if let Some(end_idx) = after_crates.find('/') {
let crate_name = &after_crates[..end_idx];
return CrateName::new_unchecked(crate_name);
}
}
if path_str.contains("/src/") || path_str.starts_with("src/") {
return CrateName::new_unchecked("crate");
}
CrateName::new_unchecked("crate")
}
fn normalize_path(path: &Path) -> PathBuf {
let mut components = Vec::new();
for comp in path.components() {
match comp {
Component::ParentDir => {
if let Some(Component::Normal(_)) = components.last() {
components.pop();
}
}
Component::CurDir => {
}
c => components.push(c),
}
}
components.iter().collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_path() {
assert_eq!(
normalize_path(Path::new("/foo/bar/../baz")),
PathBuf::from("/foo/baz")
);
assert_eq!(
normalize_path(Path::new("/foo/./bar")),
PathBuf::from("/foo/bar")
);
assert_eq!(
normalize_path(Path::new("/foo/bar/../../baz")),
PathBuf::from("/baz")
);
assert_eq!(
normalize_path(Path::new("foo/bar/../baz")),
PathBuf::from("foo/baz")
);
}
#[test]
fn test_resolve_relative_with_crate() {
let resolver = WorkspacePathResolver::new(PathBuf::from("/workspace"));
let crate_name = CrateName::new_for_test("my_crate");
let path = resolver.resolve_relative_with_crate("src/lib.rs", crate_name);
assert_eq!(path.as_relative(), Path::new("src/lib.rs"));
assert_eq!(path.workspace_root(), Path::new("/workspace"));
assert_eq!(path.crate_name().as_str(), "my_crate");
}
#[test]
fn test_resolve_relative_with_dots_and_crate() {
let resolver = WorkspacePathResolver::new(PathBuf::from("/workspace"));
let crate_name = CrateName::new_for_test("my_crate");
let path = resolver.resolve_relative_with_crate("src/../src/./lib.rs", crate_name);
assert_eq!(path.as_relative(), Path::new("src/lib.rs"));
assert_eq!(path.crate_name().as_str(), "my_crate");
}
#[test]
fn test_resolve_relative_with_provider() {
use crate::metadata::MockMetadataProvider;
let resolver = WorkspacePathResolver::new(PathBuf::from("/workspace"));
let provider = MockMetadataProvider::new("/workspace", "test_crate");
let path = resolver
.resolve_relative_with_provider("src/lib.rs", &provider)
.unwrap();
assert_eq!(path.as_relative(), Path::new("src/lib.rs"));
assert_eq!(path.crate_name().as_str(), "test_crate");
}
#[test]
fn test_crate_layout_to_workspace_relative_root() {
let layout = CrateLayout::Root;
let result = layout.to_workspace_relative("src/lib.rs");
assert_eq!(result, PathBuf::from("src/lib.rs"));
}
#[test]
fn test_crate_layout_to_workspace_relative_in_crates() {
let layout = CrateLayout::in_crates("my-crate");
let result = layout.to_workspace_relative("src/lib.rs");
assert_eq!(result, PathBuf::from("crates/my-crate/src/lib.rs"));
}
#[test]
fn test_crate_layout_to_workspace_relative_in_crates_nested() {
let layout = CrateLayout::in_crates("my-crate");
let result = layout.to_workspace_relative("src/foo/bar.rs");
assert_eq!(result, PathBuf::from("crates/my-crate/src/foo/bar.rs"));
}
#[test]
fn test_crate_layout_to_workspace_relative_custom() {
let layout = CrateLayout::custom("packages/core");
let result = layout.to_workspace_relative("src/lib.rs");
assert_eq!(result, PathBuf::from("packages/core/src/lib.rs"));
}
#[test]
fn test_crate_layout_to_workspace_relative_main_rs() {
let layout = CrateLayout::in_crates("my-cli");
let result = layout.to_workspace_relative("src/main.rs");
assert_eq!(result, PathBuf::from("crates/my-cli/src/main.rs"));
}
}