use std::collections::HashSet; 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 { let mut reported_let_order_files = HashSet::new(); for diagnostic in &self.diagnostics { if diagnostic.finding.rule_id == "let_order" { if !reported_let_order_files.insert(diagnostic.file_id) { continue; } let file = self.files.get(diagnostic.file_id); writeln!( formatter, "{} [let_order] Let bindings must follow dependency order", file.path.display() )?; let mut findings = self .diagnostics .iter() .filter(|candidate| { candidate.file_id == diagnostic.file_id && candidate.finding.rule_id == "let_order" }) .collect::>(); findings.sort_by_key(|finding| finding.finding.range.start); for finding in findings { let (start, _) = line_range(&file.source, finding.finding.range); let expected_order = finding.finding.expected_order.as_ref().expect("let order"); writeln!( formatter, " let at line {start}: {}", expected_order.join(", ") )?; } continue; } 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 Natu 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, &file.source)); } Ok(Report { files: store, diagnostics, }) }