mod ast; mod file_store; mod parser; mod rules; mod scope_graph; use scope_graph::ScopeGraph; pub use file_store::{FileId, FileStore, SourceFile}; pub use rules::{Diagnostic, Finding}; #[derive(Debug)] pub struct Report { pub files: FileStore, pub diagnostics: Vec, } impl Report { pub fn is_clean(&self) -> bool { self.diagnostics.is_empty() } } impl std::fmt::Display for Report { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for diagnostic in &self.diagnostics { let file = self.files.get(diagnostic.file_id); let (start, end) = line_range(&file.source, diagnostic.finding.range); let location = if start == end { start.to_string() } else { format!("{start}-{end}") }; writeln!(formatter, "{}:{location} {diagnostic}", file.path.display())?; } Ok(()) } } fn line_range(source: &str, range: ast::SourceRange) -> (usize, usize) { let line_at = |offset| { source .bytes() .take(offset) .filter(|byte| *byte == b'\n') .count() + 1 }; let start = line_at(range.start); let end = line_at(range.end); (start, end) } /// Analyzes source files and returns the diagnostics Xatu finds. pub fn future_sight(files: Vec) -> status::Result { let mut store = FileStore::default(); let mut diagnostics = Vec::new(); for file in files { let file_id = store.add(file); let file = store.get(file_id); let parsed = parser::parse_file(file_id, &file.source); if !parsed.errors.is_empty() { return Err(status::Status::failed_precondition(format!( "failed to parse {}", file.path.display() ))); } let graph = ScopeGraph::from_file(&parsed); diagnostics.extend(rules::run_rules(&parsed, &graph)); } Ok(Report { files: store, diagnostics, }) }