use std::path::PathBuf;
use colored::*;
use indexmap::map::Entry;
use miette::{Diagnostic, NamedSource, SourceSpan};
use smallvec::SmallVec;
use thiserror::Error;
use DependencyTreeError::*;
use super::parse_imports::ImportStatement;
use super::source_file::SourceFile;
use super::ModulePathResolver;
use crate::{
AdditionalScanDirectory, FxIndexMap, FxIndexSet, ImportPathPart, SourceFilePath,
SourceModuleName,
};
#[derive(Debug, Error, Diagnostic)]
pub enum DependencyTreeError {
#[error("Source file not found: {path}")]
SourceNotFound { path: SourceFilePath },
#[error("Cannot find import `{path}` in this scope")]
#[diagnostic(help("Maybe a typo or a missing file."))]
ImportPathNotFound {
path: String,
stmt: ImportStatement,
#[source_code]
src: NamedSource<String>,
#[label("Import statement")]
import_bit: SourceSpan,
},
}
#[derive(Default)]
struct MaxRecursionLimiter {
files_visited: Vec<(String, usize, String)>, }
impl MaxRecursionLimiter {
const MAX_RECURSION_DEPTH: usize = 16;
fn push(&mut self, import_stmt: &ImportStatement, source: &SourceFile) -> &mut Self {
let import_str = &source.content[import_stmt.range()];
self.files_visited.push((
source.file_path.to_string(),
import_stmt.source_location.line_number,
import_str.to_string(),
));
self
}
fn pop(&mut self) -> &mut Self {
self.files_visited.pop();
self
}
fn check_depth(&self) {
if self.files_visited.len() > Self::MAX_RECURSION_DEPTH {
let visited_files = self
.files_visited
.iter()
.map(|(path, line, import)| {
format!(
"\n{}:{}: {}",
path.to_string().cyan(),
line.to_string().purple(),
import.to_string().yellow()
)
})
.rev()
.collect::<String>();
panic!(
"{}\n{}\n{}\n",
"Recursion limit exceeded".red(),
"This error may be due to a circular dependency. The files visited during the recursion were:".red(),
visited_files
);
}
}
}
#[derive(Debug, Clone)]
pub struct SourceWithFullDependenciesResult<'a> {
pub source_file: &'a SourceFile,
pub full_dependencies: SmallVec<[&'a SourceFile; 16]>,
}
#[derive(Debug)]
pub struct DependencyTree {
resolver: ModulePathResolver,
parsed_sources: FxIndexMap<SourceFilePath, SourceFile>,
entry_points: FxIndexSet<SourceFilePath>,
}
impl DependencyTree {
pub fn try_build(
workspace_root: PathBuf,
entry_module_prefix: Option<String>,
entry_points: Vec<SourceFilePath>, additional_scan_dirs: Vec<AdditionalScanDirectory>,
) -> Result<Self, DependencyTreeError> {
let resolver =
ModulePathResolver::new(workspace_root, entry_module_prefix, additional_scan_dirs);
let mut tree = Self {
resolver,
parsed_sources: Default::default(),
entry_points: Default::default(),
};
for entry_point in entry_points {
tree.entry_points.insert(entry_point.clone());
tree.crawl_source(entry_point, None, &mut MaxRecursionLimiter::default())?
}
Ok(tree)
}
fn crawl_import_module(
&mut self,
parent_source_path: &SourceFilePath,
import_stmt: &ImportStatement,
import_path_part: &ImportPathPart,
limiter: &mut MaxRecursionLimiter,
) -> Result<(), DependencyTreeError> {
let possible_source_path = self
.resolver
.generate_best_possible_paths(import_path_part, parent_source_path)
.into_iter()
.find(|(_, path)| path.is_file());
let Some(parent_source) = self.parsed_sources.get_mut(parent_source_path) else {
unreachable!("{:?} source code as not parsed", parent_source_path)
};
let Some((module_name, source_path)) = possible_source_path else {
return Err(ImportPathNotFound {
stmt: import_stmt.clone(),
path: import_path_part.to_string(),
import_bit: (&import_stmt.source_location).into(),
src: NamedSource::new(
parent_source_path.to_string(),
parent_source.content.clone(),
),
});
};
parent_source.add_direct_dependency(source_path.clone());
limiter.push(import_stmt, parent_source).check_depth();
if !self.parsed_sources.contains_key(&source_path) {
self
.crawl_source(source_path, Some(module_name), limiter)
.expect("failed to crawl import path");
}
limiter.pop();
Ok(())
}
fn crawl_source(
&mut self,
source_path: SourceFilePath,
module_name: Option<SourceModuleName>,
limiter: &mut MaxRecursionLimiter,
) -> Result<(), DependencyTreeError> {
match self.parsed_sources.entry(source_path.clone()) {
Entry::Occupied(_) => {} Entry::Vacant(entry) => {
let content = entry.key().read_contents().or(Err(SourceNotFound {
path: entry.key().clone(),
}))?;
let source_file =
SourceFile::create(entry.key().clone(), module_name.clone(), content);
entry.insert(source_file);
}
};
let source_file = self.parsed_sources.get(&source_path).unwrap();
for import_stmt in &source_file.imports.clone() {
for import_path_part in import_stmt.get_import_path_parts() {
self.crawl_import_module(&source_path, import_stmt, &import_path_part, limiter)?
}
}
Ok(())
}
pub fn all_files_including_dependencies(&self) -> FxIndexSet<SourceFilePath> {
self.parsed_sources.keys().cloned().collect()
}
pub fn parsed_files(&self) -> Vec<&SourceFile> {
self.parsed_sources.values().collect()
}
pub fn get_full_dependency_for(
&self,
source_path: &SourceFilePath,
) -> FxIndexSet<SourceFilePath> {
self
.parsed_sources
.get(source_path)
.iter()
.flat_map(|source| {
source
.direct_dependencies
.iter()
.flat_map(|dep| {
let mut deps = FxIndexSet::default();
let sub_deps = self.get_full_dependency_for(dep);
deps.extend(sub_deps);
deps.insert(dep.clone());
deps
})
.collect::<FxIndexSet<_>>()
})
.collect()
}
pub fn get_source_files_with_full_dependencies(
&self,
) -> Vec<SourceWithFullDependenciesResult<'_>> {
self
.entry_points
.iter()
.map(|entry_point| {
let source_file = self.parsed_sources.get(entry_point).unwrap();
let full_dependencies = self
.get_full_dependency_for(entry_point)
.iter()
.map(|dep| self.parsed_sources.get(dep).unwrap())
.collect();
SourceWithFullDependenciesResult {
source_file,
full_dependencies,
}
})
.collect()
}
}