brink_driver/
diagnostics.rs1use brink_analyzer::AnalysisResult;
4use brink_db::{FileDiagnostics, ProjectDb, partition_diagnostics};
5use brink_ir::{Diagnostic, FileId};
6
7pub struct DiagnosticReport {
9 pub errors: Vec<Diagnostic>,
11 pub warnings: Vec<Diagnostic>,
17}
18
19pub fn collect_diagnostics(
27 db: &ProjectDb,
28 analysis: &AnalysisResult,
29 entry: Option<FileId>,
30) -> DiagnosticReport {
31 let disable_all = entry
33 .and_then(|id| db.suppressions(id))
34 .is_some_and(|s| s.disable_all);
35
36 let inputs: Vec<FileDiagnostics<'_>> = db
37 .file_ids()
38 .filter_map(|id| {
39 Some(FileDiagnostics {
40 file: id,
41 source: db.source(id)?,
42 suppressions: db.suppressions(id)?,
43 lowering: db.file_diagnostics(id)?,
44 })
45 })
46 .collect();
47
48 let opts = db.analysis_options();
49 let types = opts.type_policy();
50 let (errors, warnings) = partition_diagnostics(
51 &inputs,
52 &analysis.diagnostics,
53 disable_all,
54 types,
55 &opts.lints,
56 );
57 DiagnosticReport { errors, warnings }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63 use brink_analyzer::AnalysisResult;
64 use brink_db::ProjectDb;
65
66 fn empty_analysis() -> AnalysisResult {
67 AnalysisResult {
68 index: std::sync::Arc::new(brink_ir::SymbolIndex::default()),
69 resolutions: Vec::new(),
70 diagnostics: Vec::new(),
71 symbol_meta: std::collections::BTreeMap::new(),
72 }
73 }
74
75 #[test]
76 fn empty_db_returns_empty_report() {
77 let db = ProjectDb::new();
78 let analysis = empty_analysis();
79 let report = collect_diagnostics(&db, &analysis, None);
80 assert!(report.errors.is_empty());
81 assert!(report.warnings.is_empty());
82 }
83
84 #[test]
85 fn lowering_errors_partitioned_correctly() {
86 let mut db = ProjectDb::new();
87 db.set_file("test.ink", "=== \nHello\n".to_string());
89 let analysis = empty_analysis();
90 let entry = db.file_id("test.ink");
91 let report = collect_diagnostics(&db, &analysis, entry);
92 assert!(!report.errors.is_empty());
94 }
95
96 fn run_analysis(db: &ProjectDb) -> AnalysisResult {
97 let inputs = db.analysis_inputs();
98 let file_refs: Vec<_> = inputs
99 .iter()
100 .map(|(id, hir, manifest)| (*id, hir, manifest))
101 .collect();
102 brink_analyzer::analyze(&file_refs)
103 }
104
105 #[test]
106 fn analysis_diagnostics_included_when_no_disable_all() {
107 let mut db = ProjectDb::new();
108 db.set_file("test.ink", "-> missing_knot\n".to_string());
110 let analysis_result = run_analysis(&db);
111 let entry = db.file_id("test.ink");
112 let report = collect_diagnostics(&db, &analysis_result, entry);
113 let total = report.errors.len() + report.warnings.len();
115 assert!(total > 0);
116 }
117
118 #[test]
119 fn disable_all_skips_analysis_diagnostics() {
120 let mut db = ProjectDb::new();
121 db.set_file(
123 "test.ink",
124 "// brink-disable-all\n-> missing_knot\n".to_string(),
125 );
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 analysis_diag_count = analysis_result.diagnostics.len();
133 let report_total = report.errors.len() + report.warnings.len();
135 if analysis_diag_count > 0 {
138 let report_without_disable = collect_diagnostics(&db, &analysis_result, None);
139 let without_total =
140 report_without_disable.errors.len() + report_without_disable.warnings.len();
141 assert!(report_total < without_total);
142 }
143 }
144
145 #[test]
150 fn diagnostic_from_included_file_carries_its_file_id() {
151 let mut db = ProjectDb::new();
152 db.set_file("main.ink", "INCLUDE helper.ink\n-> top\n".to_string());
153 db.set_file("helper.ink", "=== top ===\n-> does_not_exist\n".to_string());
154 let analysis = run_analysis(&db);
155 let entry = db.file_id("main.ink");
156 let helper = db
157 .file_id("helper.ink")
158 .expect("helper.ink should have a FileId");
159 let report = collect_diagnostics(&db, &analysis, entry);
160
161 let all: Vec<_> = report.errors.iter().chain(report.warnings.iter()).collect();
162 assert!(
163 !all.is_empty(),
164 "the unresolved divert in helper.ink should produce a diagnostic"
165 );
166 for d in &all {
169 assert_eq!(
170 d.file, helper,
171 "diagnostic `{}` for an error inside helper.ink should carry \
172 helper.ink's FileId ({:?}), not the entry's ({:?})",
173 d.message, helper, entry
174 );
175 }
176 }
177
178 #[test]
184 fn analysis_diagnostic_from_included_file_carries_its_file_id() {
185 let mut db = ProjectDb::new();
186 db.set_file("main.ink", "INCLUDE helper.ink\n-> top\n".to_string());
187 db.set_file(
189 "helper.ink",
190 "=== top ===\n-> END\nunreachable line\n".to_string(),
191 );
192 let analysis = run_analysis(&db);
193 let entry = db.file_id("main.ink");
194 let helper = db
195 .file_id("helper.ink")
196 .expect("helper.ink should have a FileId");
197 let report = collect_diagnostics(&db, &analysis, entry);
198
199 let e033s: Vec<_> = report
200 .errors
201 .iter()
202 .chain(report.warnings.iter())
203 .filter(|d| d.code == brink_ir::DiagnosticCode::E033)
204 .collect();
205 assert!(
206 !e033s.is_empty(),
207 "the unreachable line in helper.ink should produce an E033"
208 );
209 for d in &e033s {
210 assert_eq!(
211 d.file, helper,
212 "E033 for unreachable code inside helper.ink should carry \
213 helper.ink's FileId ({helper:?}), not the entry's ({entry:?})"
214 );
215 }
216 }
217}