use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use anyhow::Context;
use anyhow::Result;
use syn::UseTree;
use syn::visit::Visit;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum PathOrigin {
Relative,
Crate,
}
pub(super) struct ExtractedPaths {
pub use_paths: Vec<(Vec<String>, PathOrigin)>,
pub expr_paths: Vec<(Vec<String>, PathOrigin)>,
pub use_renames: Vec<UseRename>,
}
pub(super) struct UseRename {
pub alias: String,
pub original_path: Vec<String>,
}
pub(super) struct SourceCache {
contents: HashMap<PathBuf, String>,
files_by_dir: HashMap<PathBuf, Vec<PathBuf>>,
parsed: HashMap<PathBuf, syn::File>,
extracted_paths: HashMap<PathBuf, ExtractedPaths>,
}
impl SourceCache {
pub fn build(roots: &[&Path]) -> Result<Self> {
let mut contents = HashMap::new();
for root in roots {
for file in rust_source_files(root)? {
contents
.entry(file.clone())
.or_insert(fs::read_to_string(&file).with_context(|| {
format!("failed to pre-read source file {}", file.display())
})?);
}
}
let mut files_by_dir: HashMap<PathBuf, Vec<PathBuf>> = HashMap::new();
for path in contents.keys() {
if let Some(parent) = path.parent() {
files_by_dir
.entry(parent.to_path_buf())
.or_default()
.push(path.clone());
}
}
let mut parsed = HashMap::new();
for (path, source) in &contents {
if let Ok(ast) = syn::parse_file(source) {
parsed.insert(path.clone(), ast);
}
}
let mut extracted_paths = HashMap::new();
for (path, ast) in &parsed {
extracted_paths.insert(path.clone(), extract_paths(ast));
}
Ok(Self {
contents,
files_by_dir,
parsed,
extracted_paths,
})
}
pub fn source_files_under(&self, dir: &Path) -> Vec<&Path> {
self.files_by_dir
.iter()
.filter(|(d, _)| d.starts_with(dir))
.flat_map(|(_, files)| files.iter().map(PathBuf::as_path))
.collect()
}
pub fn read_source(&self, path: &Path) -> Result<&str> {
self.contents
.get(path)
.map(String::as_str)
.with_context(|| format!("source file not in cache: {}", path.display()))
}
pub fn parsed_file(&self, path: &Path) -> Option<&syn::File> { self.parsed.get(path) }
pub fn extracted_paths(&self, path: &Path) -> Option<&ExtractedPaths> {
self.extracted_paths.get(path)
}
}
pub(super) fn analysis_source_root_for(
crate_root_file: &Path,
package_root: &Path,
) -> Option<PathBuf> {
let source_root = crate_root_file.parent()?.to_path_buf();
let canonical_crate_root =
fs::canonicalize(crate_root_file).unwrap_or_else(|_| crate_root_file.to_path_buf());
let canonical_package_root =
fs::canonicalize(package_root).unwrap_or_else(|_| package_root.to_path_buf());
let relative = canonical_crate_root
.strip_prefix(&canonical_package_root)
.ok()?;
let first_component = relative.components().next()?.as_os_str().to_str()?;
matches!(first_component, "src" | "examples" | "tests" | "benches").then_some(source_root)
}
pub(super) fn module_path_from_boundary_file(
src_root: &Path,
boundary_file: &Path,
) -> Option<Vec<String>> {
let relative = boundary_file.strip_prefix(src_root).ok()?;
let mut components = relative
.components()
.map(|component| component.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>();
let last = components.last_mut()?;
*last = last.strip_suffix(".rs")?.to_string();
if matches!(components.as_slice(), [name] if name == "lib" || name == "main") {
Some(Vec::new())
} else {
Some(components)
}
}
pub(super) fn module_path_from_source_file(
src_root: &Path,
source_file: &Path,
) -> Option<Vec<String>> {
if source_file.file_name().and_then(|name| name.to_str()) == Some("mod.rs") {
module_path_from_dir(src_root, source_file.parent()?)
} else {
module_path_from_boundary_file(src_root, source_file)
}
}
pub(super) fn module_path_from_dir(src_root: &Path, module_dir: &Path) -> Option<Vec<String>> {
let relative = module_dir.strip_prefix(src_root).ok()?;
let components = relative
.components()
.map(|component| component.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>();
(!components.is_empty()).then_some(components)
}
pub(super) fn first_line_matching(source: &str, needle: &str) -> Option<usize> {
source
.lines()
.position(|line| line.contains(needle))
.map(|index| index + 1)
}
pub(super) fn flatten_use_tree(prefix: Vec<String>, tree: &UseTree, out: &mut Vec<Vec<String>>) {
match tree {
UseTree::Path(path) => {
let mut next = prefix;
next.push(path.ident.to_string());
flatten_use_tree(next, &path.tree, out);
},
UseTree::Name(name) => {
let mut next = prefix;
next.push(name.ident.to_string());
out.push(next);
},
UseTree::Rename(rename) => {
let mut next = prefix;
next.push(rename.ident.to_string());
next.push(rename.rename.to_string());
out.push(next);
},
UseTree::Group(group) => {
for item in &group.items {
flatten_use_tree(prefix.clone(), item, out);
}
},
UseTree::Glob(_) => {
let mut next = prefix;
next.push("*".to_string());
out.push(next);
},
}
}
fn rust_source_files(src_root: &Path) -> Result<Vec<PathBuf>> {
let mut files = Vec::new();
collect_rust_source_files(src_root, &mut files)?;
Ok(files)
}
fn collect_rust_source_files(dir: &Path, files: &mut Vec<PathBuf>) -> Result<()> {
for entry in fs::read_dir(dir)
.with_context(|| format!("failed to read source directory {}", dir.display()))?
{
let entry = entry?;
let path = entry.path();
if path.is_dir() {
collect_rust_source_files(&path, files)?;
} else if path.extension().and_then(|ext| ext.to_str()) == Some("rs") {
files.push(path);
}
}
Ok(())
}
pub(super) fn path_origin(raw: &[String]) -> PathOrigin {
if raw.first().map(String::as_str) == Some("crate") {
PathOrigin::Crate
} else {
PathOrigin::Relative
}
}
struct PathExtractor {
use_paths: Vec<(Vec<String>, PathOrigin)>,
expr_paths: Vec<(Vec<String>, PathOrigin)>,
use_renames: Vec<UseRename>,
inside_use_item: bool,
}
impl<'ast> Visit<'ast> for PathExtractor {
fn visit_item_use(&mut self, item_use: &'ast syn::ItemUse) {
let mut flat = Vec::new();
flatten_use_tree(Vec::new(), &item_use.tree, &mut flat);
for raw in flat {
let origin = path_origin(&raw);
self.use_paths.push((raw, origin));
}
extract_use_renames(Vec::new(), &item_use.tree, &mut self.use_renames);
self.inside_use_item = true;
syn::visit::visit_item_use(self, item_use);
self.inside_use_item = false;
}
fn visit_path(&mut self, path: &'ast syn::Path) {
if !self.inside_use_item {
let segments: Vec<String> = path.segments.iter().map(|s| s.ident.to_string()).collect();
let origin = path_origin(&segments);
self.expr_paths.push((segments, origin));
}
syn::visit::visit_path(self, path);
}
}
pub(super) fn extract_paths(file: &syn::File) -> ExtractedPaths {
let mut extractor = PathExtractor {
use_paths: Vec::new(),
expr_paths: Vec::new(),
use_renames: Vec::new(),
inside_use_item: false,
};
extractor.visit_file(file);
ExtractedPaths {
use_paths: extractor.use_paths,
expr_paths: extractor.expr_paths,
use_renames: extractor.use_renames,
}
}
pub(super) fn extract_use_renames(prefix: Vec<String>, tree: &UseTree, out: &mut Vec<UseRename>) {
match tree {
UseTree::Path(path) => {
let mut next = prefix;
next.push(path.ident.to_string());
extract_use_renames(next, &path.tree, out);
},
UseTree::Rename(rename) => {
let mut original_path = prefix;
original_path.push(rename.ident.to_string());
out.push(UseRename {
alias: rename.rename.to_string(),
original_path,
});
},
UseTree::Group(group) => {
for item in &group.items {
extract_use_renames(prefix.clone(), item, out);
}
},
UseTree::Name(_) | UseTree::Glob(_) => {},
}
}