mod module_tree;
use std::fmt;
use std::path::{Path, PathBuf};
use crate::cache::ParseCache;
use tracing::info;
use cargo_metadata::{Metadata, MetadataCommand};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ModuleVisibility {
Public,
Crate,
Super,
InPath(String),
Inherited,
}
impl fmt::Display for ModuleVisibility {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Public => f.write_str("pub"),
Self::Crate => f.write_str("pub(crate)"),
Self::Super => f.write_str("pub(super)"),
Self::InPath(path) => write!(f, "pub(in {path})"),
Self::Inherited => Ok(()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum TargetKind {
Lib,
Bin,
Test,
}
impl fmt::Display for TargetKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Lib => f.write_str("lib"),
Self::Bin => f.write_str("bin"),
Self::Test => f.write_str("test"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TargetInfo {
kind: TargetKind,
name: String,
}
impl TargetInfo {
#[must_use]
pub fn new(kind: TargetKind, name: impl Into<String>) -> Self {
Self {
kind,
name: name.into(),
}
}
#[must_use]
pub const fn kind(&self) -> &TargetKind {
&self.kind
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
}
impl fmt::Display for TargetInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind {
TargetKind::Lib => fmt::Display::fmt(&self.kind, f),
TargetKind::Bin | TargetKind::Test => write!(f, "{}:{}", self.kind, self.name),
}
}
}
#[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,
}
impl CrateInfoError {
#[must_use]
pub const fn is_module_not_found(&self) -> bool {
matches!(self, Self::ModuleNotFound { .. })
}
}
pub(crate) type Result<T> = std::result::Result<T, CrateInfoError>;
#[derive(Debug, Clone)]
pub struct ModuleInfo {
module_path: String,
source_file: PathBuf,
visibility: ModuleVisibility,
target: TargetInfo,
}
impl ModuleInfo {
#[must_use]
pub fn new(
module_path: impl Into<String>,
source_file: PathBuf,
visibility: ModuleVisibility,
target: TargetInfo,
) -> Self {
Self {
module_path: module_path.into(),
source_file,
visibility,
target,
}
}
#[must_use]
pub fn path(&self) -> &str {
&self.module_path
}
#[must_use]
pub fn source(&self) -> &Path {
&self.source_file
}
#[must_use]
pub const fn visibility(&self) -> &ModuleVisibility {
&self.visibility
}
#[must_use]
pub const fn target(&self) -> &TargetInfo {
&self.target
}
#[must_use]
pub fn with_path(self, new_path: impl Into<String>) -> Self {
Self {
module_path: new_path.into(),
..self
}
}
}
#[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
}
pub(crate) fn all_targets(&self, include_tests: bool) -> Vec<(TargetInfo, PathBuf)> {
let Some(package) = self.root_package() else {
return vec![];
};
let mut result = Vec::new();
for target in &package.targets {
let src_path = target.src_path.as_std_path().to_path_buf();
if target.is_lib() {
result.push((
TargetInfo::new(TargetKind::Lib, target.name.as_str()),
src_path,
));
} else if target.is_bin() {
result.push((
TargetInfo::new(TargetKind::Bin, target.name.as_str()),
src_path,
));
} else if include_tests && target.kind.contains(&cargo_metadata::TargetKind::Test) {
result.push((
TargetInfo::new(TargetKind::Test, target.name.as_str()),
src_path,
));
}
}
result.sort_by(|a, b| {
a.0.kind()
.cmp(b.0.kind())
.then_with(|| a.0.name().cmp(b.0.name()))
});
result
}
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,
target: &TargetInfo,
cache: &mut ParseCache,
) -> Result<Vec<ModuleInfo>> {
let file_path = self.resolve_module(module_path)?;
let normalized_path = self.normalize_module_path(module_path);
info!(
"Module tree: '{}' \u{2192} {}, recursive={recursive}",
normalized_path,
file_path.display()
);
let inline_scope = self.compute_inline_scope_for_path(&normalized_path, &file_path);
let root_visibility =
self.compute_root_visibility(&normalized_path, &file_path, &inline_scope, cache)?;
if recursive {
Self::collect_submodules_recursive(
&file_path,
&normalized_path,
root_visibility,
&inline_scope,
include_tests,
target,
cache,
)
} else {
Self::collect_submodules_shallow(
&file_path,
&normalized_path,
root_visibility,
include_tests,
target,
cache,
)
}
}
pub(crate) fn resolve_module_path_to_file(&self, module_path: &str) -> Result<PathBuf> {
self.resolve_module(module_path)
}
pub(crate) fn get_module_tree_for_file(
src_path: &Path,
target: &TargetInfo,
include_cfg_tests: bool,
cache: &mut ParseCache,
) -> Result<Vec<ModuleInfo>> {
Self::collect_submodules_recursive_crate_root(src_path, include_cfg_tests, target, cache)
}
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()
}
}