mod module_tree;
use std::path::{Path, PathBuf};
use crate::cache::ParseCache;
use cargo_metadata::{Metadata, MetadataCommand};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum CrateInfoError {
#[error("Failed to execute cargo metadata: {0}")]
MetadataExecution(#[from] cargo_metadata::Error),
#[error("workspace support is not yet implemented")]
WorkspaceRoot,
#[error("root package not found in cargo metadata")]
PackageNotFound,
#[error("No crate root file found for package '{0}'")]
NoCrateRoot(String),
#[error("Module path cannot be empty")]
EmptyModulePath,
#[error("Module '{module_path}' not found")]
ModuleNotFound {
module_path: String,
},
#[error("Failed to read file '{path}': {source}")]
FileRead {
path: PathBuf,
source: std::io::Error,
},
#[error("Failed to parse file '{path}': {message}")]
ParseError {
path: PathBuf,
message: String,
},
#[error("Invalid module path segment '{segment}'")]
InvalidModuleSegment {
segment: String,
},
#[error("Resolved path escapes crate root")]
PathTraversal,
}
pub(crate) type Result<T> = std::result::Result<T, CrateInfoError>;
#[derive(Debug, Clone)]
pub(crate) struct ModuleInfo {
module_path: String,
source_file: PathBuf,
}
impl ModuleInfo {
pub(crate) const fn new(module_path: String, source_file: PathBuf) -> Self {
Self {
module_path,
source_file,
}
}
#[must_use]
pub(crate) fn path(&self) -> &str {
&self.module_path
}
#[must_use]
pub(crate) fn source(&self) -> &Path {
&self.source_file
}
}
#[derive(Debug, Clone)]
pub(crate) struct CrateInfo {
metadata: Metadata,
root_package_name: String,
}
impl CrateInfo {
pub(crate) fn new(crate_path: &Path) -> Result<Self> {
let metadata = MetadataCommand::new().current_dir(crate_path).exec()?;
let root_package_name = metadata
.root_package()
.ok_or(CrateInfoError::WorkspaceRoot)?
.name
.to_string();
Ok(Self {
metadata,
root_package_name,
})
}
#[must_use]
pub(crate) fn root_package_name(&self) -> &str {
&self.root_package_name
}
fn root_package(&self) -> Option<&cargo_metadata::Package> {
self.metadata
.packages
.iter()
.find(|p| p.name == self.root_package_name)
}
pub(crate) fn get_module_tree(
&self,
module_path: &str,
recursive: bool,
include_tests: bool,
cache: &mut ParseCache,
) -> Result<Vec<ModuleInfo>> {
let file_path = self.resolve_module(module_path)?;
let normalized_path = self.normalize_module_path(module_path);
let inline_scope = self.compute_inline_scope_for_path(&normalized_path, &file_path);
if recursive {
Self::collect_submodules_recursive(
&file_path,
&normalized_path,
&inline_scope,
include_tests,
cache,
)
} else {
Self::collect_submodules_shallow(&file_path, &normalized_path, include_tests, cache)
}
}
pub(crate) fn resolve_module_path_to_file(&self, module_path: &str) -> Result<PathBuf> {
self.resolve_module(module_path)
}
fn normalize_module_path(&self, module_path: &str) -> String {
let parts: Vec<&str> = module_path.split("::").collect();
if parts.is_empty() {
return module_path.to_owned();
}
let first_part = parts[0];
if first_part == self.root_package_name() || first_part == "lib" {
return if parts.len() == 1 {
String::new()
} else {
parts[1..].join("::")
};
}
if let Some(package) = self.root_package()
&& Self::find_binary_by_file_stem(package, first_part).is_some()
{
return if parts.len() == 1 {
String::new()
} else {
parts[1..].join("::")
};
}
module_path.to_owned()
}
}