brink_driver/
diagnostics.rs1use std::collections::HashMap;
4
5use brink_analyzer::AnalysisResult;
6use brink_db::ProjectDb;
7use brink_ir::{Diagnostic, FileId, Severity};
8
9pub struct DiagnosticReport {
11 pub errors: Vec<Diagnostic>,
13 pub warnings: Vec<Diagnostic>,
15}
16
17pub fn collect_diagnostics(
22 db: &ProjectDb,
23 analysis: &AnalysisResult,
24 entry: Option<FileId>,
25) -> DiagnosticReport {
26 let mut errors = Vec::new();
27 let mut warnings = Vec::new();
28
29 let disable_all = entry
31 .and_then(|id| db.suppressions(id))
32 .is_some_and(|s| s.disable_all);
33
34 for id in db.file_ids() {
36 let raw: Vec<Diagnostic> = db.file_diagnostics(id).unwrap_or_default().to_vec();
37 let source = db.source(id).unwrap_or_default();
38 let suppressions = db.suppressions(id).cloned().unwrap_or_default();
39 let filtered = brink_ir::suppressions::apply_suppressions(id, source, raw, &suppressions);
40 for d in filtered {
41 if d.code.severity() == Severity::Error {
42 errors.push(d);
43 } else {
44 warnings.push(d);
45 }
46 }
47 }
48
49 if !disable_all {
51 let mut by_file: HashMap<FileId, Vec<Diagnostic>> = HashMap::new();
52 for d in &analysis.diagnostics {
53 by_file.entry(d.file).or_default().push(d.clone());
54 }
55 let mut file_ids: Vec<_> = by_file.keys().copied().collect();
57 file_ids.sort_by_key(|id| id.0);
58 for fid in file_ids {
59 let diags = by_file.remove(&fid).unwrap_or_default();
60 let source = db.source(fid).unwrap_or_default();
61 let suppressions = db.suppressions(fid).cloned().unwrap_or_default();
62 let filtered =
63 brink_ir::suppressions::apply_suppressions(fid, source, diags, &suppressions);
64 for d in filtered {
65 if d.code.severity() == Severity::Error {
66 errors.push(d);
67 } else {
68 warnings.push(d);
69 }
70 }
71 }
72 }
73
74 DiagnosticReport { errors, warnings }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use brink_analyzer::AnalysisResult;
81 use brink_db::ProjectDb;
82
83 fn empty_analysis() -> AnalysisResult {
84 AnalysisResult {
85 index: brink_ir::SymbolIndex::default(),
86 resolutions: Vec::new(),
87 diagnostics: Vec::new(),
88 }
89 }
90
91 #[test]
92 fn empty_db_returns_empty_report() {
93 let db = ProjectDb::new();
94 let analysis = empty_analysis();
95 let report = collect_diagnostics(&db, &analysis, None);
96 assert!(report.errors.is_empty());
97 assert!(report.warnings.is_empty());
98 }
99
100 #[test]
101 fn lowering_errors_partitioned_correctly() {
102 let mut db = ProjectDb::new();
103 db.set_file("test.ink", "=== \nHello\n".to_string());
105 let analysis = empty_analysis();
106 let entry = db.file_id("test.ink");
107 let report = collect_diagnostics(&db, &analysis, entry);
108 assert!(!report.errors.is_empty());
110 }
111
112 fn run_analysis(db: &ProjectDb) -> AnalysisResult {
113 let inputs = db.analysis_inputs();
114 let file_refs: Vec<_> = inputs
115 .iter()
116 .map(|(id, hir, manifest)| (*id, hir, manifest))
117 .collect();
118 brink_analyzer::analyze(&file_refs)
119 }
120
121 #[test]
122 fn analysis_diagnostics_included_when_no_disable_all() {
123 let mut db = ProjectDb::new();
124 db.set_file("test.ink", "-> missing_knot\n".to_string());
126 let analysis_result = run_analysis(&db);
127 let entry = db.file_id("test.ink");
128 let report = collect_diagnostics(&db, &analysis_result, entry);
129 let total = report.errors.len() + report.warnings.len();
131 assert!(total > 0);
132 }
133
134 #[test]
135 fn disable_all_skips_analysis_diagnostics() {
136 let mut db = ProjectDb::new();
137 db.set_file(
139 "test.ink",
140 "// brink-disable-all\n-> missing_knot\n".to_string(),
141 );
142 let analysis_result = run_analysis(&db);
143 let entry = db.file_id("test.ink");
144 let report = collect_diagnostics(&db, &analysis_result, entry);
145 let analysis_diag_count = analysis_result.diagnostics.len();
149 let report_total = report.errors.len() + report.warnings.len();
151 if analysis_diag_count > 0 {
154 let report_without_disable = collect_diagnostics(&db, &analysis_result, None);
155 let without_total =
156 report_without_disable.errors.len() + report_without_disable.warnings.len();
157 assert!(report_total < without_total);
158 }
159 }
160
161 #[test]
166 fn diagnostic_from_included_file_carries_its_file_id() {
167 let mut db = ProjectDb::new();
168 db.set_file("main.ink", "INCLUDE helper.ink\n-> top\n".to_string());
169 db.set_file("helper.ink", "=== top ===\n-> does_not_exist\n".to_string());
170 let analysis = run_analysis(&db);
171 let entry = db.file_id("main.ink");
172 let helper = db
173 .file_id("helper.ink")
174 .expect("helper.ink should have a FileId");
175 let report = collect_diagnostics(&db, &analysis, entry);
176
177 let all: Vec<_> = report.errors.iter().chain(report.warnings.iter()).collect();
178 assert!(
179 !all.is_empty(),
180 "the unresolved divert in helper.ink should produce a diagnostic"
181 );
182 for d in &all {
185 assert_eq!(
186 d.file, helper,
187 "diagnostic `{}` for an error inside helper.ink should carry \
188 helper.ink's FileId ({:?}), not the entry's ({:?})",
189 d.message, helper, entry
190 );
191 }
192 }
193}