use std::collections::HashMap;
use brink_analyzer::AnalysisResult;
use brink_db::ProjectDb;
use brink_ir::{Diagnostic, FileId, Severity};
pub struct DiagnosticReport {
pub errors: Vec<Diagnostic>,
pub warnings: Vec<Diagnostic>,
}
pub fn collect_diagnostics(
db: &ProjectDb,
analysis: &AnalysisResult,
entry: Option<FileId>,
) -> DiagnosticReport {
let mut errors = Vec::new();
let mut warnings = Vec::new();
let disable_all = entry
.and_then(|id| db.suppressions(id))
.is_some_and(|s| s.disable_all);
for id in db.file_ids() {
let raw: Vec<Diagnostic> = db.file_diagnostics(id).unwrap_or_default().to_vec();
let source = db.source(id).unwrap_or_default();
let suppressions = db.suppressions(id).cloned().unwrap_or_default();
let filtered = brink_ir::suppressions::apply_suppressions(id, source, raw, &suppressions);
for d in filtered {
if d.code.severity() == Severity::Error {
errors.push(d);
} else {
warnings.push(d);
}
}
}
if !disable_all {
let mut by_file: HashMap<FileId, Vec<Diagnostic>> = HashMap::new();
for d in &analysis.diagnostics {
by_file.entry(d.file).or_default().push(d.clone());
}
let mut file_ids: Vec<_> = by_file.keys().copied().collect();
file_ids.sort_by_key(|id| id.0);
for fid in file_ids {
let diags = by_file.remove(&fid).unwrap_or_default();
let source = db.source(fid).unwrap_or_default();
let suppressions = db.suppressions(fid).cloned().unwrap_or_default();
let filtered =
brink_ir::suppressions::apply_suppressions(fid, source, diags, &suppressions);
for d in filtered {
if d.code.severity() == Severity::Error {
errors.push(d);
} else {
warnings.push(d);
}
}
}
}
DiagnosticReport { errors, warnings }
}
#[cfg(test)]
mod tests {
use super::*;
use brink_analyzer::AnalysisResult;
use brink_db::ProjectDb;
fn empty_analysis() -> AnalysisResult {
AnalysisResult {
index: brink_ir::SymbolIndex::default(),
resolutions: Vec::new(),
diagnostics: Vec::new(),
symbol_meta: std::collections::BTreeMap::new(),
}
}
#[test]
fn empty_db_returns_empty_report() {
let db = ProjectDb::new();
let analysis = empty_analysis();
let report = collect_diagnostics(&db, &analysis, None);
assert!(report.errors.is_empty());
assert!(report.warnings.is_empty());
}
#[test]
fn lowering_errors_partitioned_correctly() {
let mut db = ProjectDb::new();
db.set_file("test.ink", "=== \nHello\n".to_string());
let analysis = empty_analysis();
let entry = db.file_id("test.ink");
let report = collect_diagnostics(&db, &analysis, entry);
assert!(!report.errors.is_empty());
}
fn run_analysis(db: &ProjectDb) -> AnalysisResult {
let inputs = db.analysis_inputs();
let file_refs: Vec<_> = inputs
.iter()
.map(|(id, hir, manifest)| (*id, hir, manifest))
.collect();
brink_analyzer::analyze(&file_refs)
}
#[test]
fn analysis_diagnostics_included_when_no_disable_all() {
let mut db = ProjectDb::new();
db.set_file("test.ink", "-> missing_knot\n".to_string());
let analysis_result = run_analysis(&db);
let entry = db.file_id("test.ink");
let report = collect_diagnostics(&db, &analysis_result, entry);
let total = report.errors.len() + report.warnings.len();
assert!(total > 0);
}
#[test]
fn disable_all_skips_analysis_diagnostics() {
let mut db = ProjectDb::new();
db.set_file(
"test.ink",
"// brink-disable-all\n-> missing_knot\n".to_string(),
);
let analysis_result = run_analysis(&db);
let entry = db.file_id("test.ink");
let report = collect_diagnostics(&db, &analysis_result, entry);
let analysis_diag_count = analysis_result.diagnostics.len();
let report_total = report.errors.len() + report.warnings.len();
if analysis_diag_count > 0 {
let report_without_disable = collect_diagnostics(&db, &analysis_result, None);
let without_total =
report_without_disable.errors.len() + report_without_disable.warnings.len();
assert!(report_total < without_total);
}
}
#[test]
fn diagnostic_from_included_file_carries_its_file_id() {
let mut db = ProjectDb::new();
db.set_file("main.ink", "INCLUDE helper.ink\n-> top\n".to_string());
db.set_file("helper.ink", "=== top ===\n-> does_not_exist\n".to_string());
let analysis = run_analysis(&db);
let entry = db.file_id("main.ink");
let helper = db
.file_id("helper.ink")
.expect("helper.ink should have a FileId");
let report = collect_diagnostics(&db, &analysis, entry);
let all: Vec<_> = report.errors.iter().chain(report.warnings.iter()).collect();
assert!(
!all.is_empty(),
"the unresolved divert in helper.ink should produce a diagnostic"
);
for d in &all {
assert_eq!(
d.file, helper,
"diagnostic `{}` for an error inside helper.ink should carry \
helper.ink's FileId ({:?}), not the entry's ({:?})",
d.message, helper, entry
);
}
}
#[test]
fn analysis_diagnostic_from_included_file_carries_its_file_id() {
let mut db = ProjectDb::new();
db.set_file("main.ink", "INCLUDE helper.ink\n-> top\n".to_string());
db.set_file(
"helper.ink",
"=== top ===\n-> END\nunreachable line\n".to_string(),
);
let analysis = run_analysis(&db);
let entry = db.file_id("main.ink");
let helper = db
.file_id("helper.ink")
.expect("helper.ink should have a FileId");
let report = collect_diagnostics(&db, &analysis, entry);
let e033s: Vec<_> = report
.errors
.iter()
.chain(report.warnings.iter())
.filter(|d| d.code == brink_ir::DiagnosticCode::E033)
.collect();
assert!(
!e033s.is_empty(),
"the unreachable line in helper.ink should produce an E033"
);
for d in &e033s {
assert_eq!(
d.file, helper,
"E033 for unreachable code inside helper.ink should carry \
helper.ink's FileId ({helper:?}), not the entry's ({entry:?})"
);
}
}
}