1use alloc::{boxed::Box, vec::Vec};
4use core::fmt;
5
6use diag_lang::{Code, Diagnostic, Label, Severity, SourceMap};
7use syntax_lang::Span;
8
9#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct Error {
64 diagnostics: Vec<Diagnostic>,
65 line: u32,
66 column: u32,
67 path: Option<Box<str>>,
69}
70
71impl Error {
72 pub(crate) fn new(diagnostics: Vec<Diagnostic>, text: &str) -> Self {
74 let offset = diagnostics
75 .first()
76 .map_or(0, |d| d.primary().span().start().to_usize());
77 let (line, column) = line_col(text, offset);
78 Self {
79 diagnostics,
80 line,
81 column,
82 path: None,
83 }
84 }
85
86 pub(crate) fn in_map(diagnostics: Vec<Diagnostic>, map: &SourceMap) -> Self {
89 let mut error = Self {
90 diagnostics,
91 line: 1,
92 column: 1,
93 path: None,
94 };
95 let at = error
96 .diagnostics
97 .first()
98 .map(|d| d.primary().span().start());
99 if let Some((id, local)) = at.and_then(|at| map.locate(at)) {
100 if let Some(file) = map.source(id) {
101 let (line, column) = line_col(file.text(), local.to_usize());
102 error.line = line;
103 error.column = column;
104 error.path = Some(Box::from(file.name()));
105 }
106 }
107 error
108 }
109
110 pub(crate) fn located(diagnostics: Vec<Diagnostic>, line: u32, column: u32) -> Self {
112 Self {
113 diagnostics,
114 line,
115 column,
116 path: None,
117 }
118 }
119
120 #[inline]
135 #[must_use]
136 pub fn diagnostics(&self) -> &[Diagnostic] {
137 &self.diagnostics
138 }
139}
140
141impl fmt::Display for Error {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 let message = self.diagnostics.first().map_or("", |d| d.message());
144 if let Some(path) = &self.path {
145 write!(f, "{path}:")?;
146 }
147 write!(f, "{}:{}: {}", self.line, self.column, message)?;
148 match self.diagnostics.len() {
149 0 | 1 => Ok(()),
150 2 => f.write_str(" (and 1 more error)"),
151 n => write!(f, " (and {} more errors)", n - 1),
152 }
153 }
154}
155
156impl core::error::Error for Error {}
157
158pub(crate) fn line_col(text: &str, offset: usize) -> (u32, u32) {
160 let mut offset = offset.min(text.len());
161 while !text.is_char_boundary(offset) {
162 offset -= 1;
163 }
164 let before = &text[..offset];
165 let line_start = before.rfind('\n').map_or(0, |i| i + 1);
166 let line = before.bytes().filter(|b| *b == b'\n').count() + 1;
167 let column = before[line_start..].chars().count() + 1;
168 (saturate(line), saturate(column))
169}
170
171fn saturate(n: usize) -> u32 {
172 u32::try_from(n).unwrap_or(u32::MAX)
173}
174
175pub(crate) fn coded(code: Code, span: Span, message: impl Into<Box<str>>) -> Diagnostic {
177 Diagnostic::new(Severity::Error, message, Label::unlabelled(span)).with_code(code)
178}
179
180#[derive(Debug)]
182pub(crate) struct Report {
183 diagnostics: Vec<Diagnostic>,
184 errors: usize,
185 budget: u64,
187}
188
189impl Default for Report {
190 fn default() -> Self {
191 Self {
192 diagnostics: Vec::new(),
193 errors: 0,
194 budget: crate::suggest::BUDGET,
195 }
196 }
197}
198
199impl Report {
200 pub(crate) fn suggest<'a>(
203 &mut self,
204 name: &str,
205 candidates: impl Iterator<Item = &'a str>,
206 limit: usize,
207 ) -> Option<alloc::string::String> {
208 crate::suggest::suggest_with(name, candidates, limit, &mut self.budget)
209 }
210
211 pub(crate) fn error(&mut self, code: Code, span: Span, message: impl Into<Box<str>>) {
213 self.diagnostic(coded(code, span, message));
214 }
215
216 pub(crate) fn error_help(
218 &mut self,
219 code: Code,
220 span: Span,
221 message: impl Into<Box<str>>,
222 help: impl Into<Box<str>>,
223 ) {
224 self.diagnostic(coded(code, span, message).with_help(help));
225 }
226
227 pub(crate) fn warning(&mut self, code: Code, span: Span, message: impl Into<Box<str>>) {
229 self.diagnostic(
230 Diagnostic::new(Severity::Warning, message, Label::unlabelled(span)).with_code(code),
231 );
232 }
233
234 pub(crate) fn diagnostic(&mut self, diagnostic: Diagnostic) {
236 if diagnostic.severity() == Severity::Error {
237 self.errors += 1;
238 }
239 self.diagnostics.push(diagnostic);
240 }
241
242 pub(crate) fn is_clean(&self) -> bool {
244 self.errors == 0
245 }
246
247 pub(crate) fn errors(&self) -> usize {
249 self.errors
250 }
251
252 fn sort(&mut self) {
254 self.diagnostics
255 .sort_by_key(|d| d.primary().span().start().to_u32());
256 }
257
258 pub(crate) fn into_error(mut self, text: &str) -> Error {
260 self.sort();
261 Error::new(self.diagnostics, text)
262 }
263
264 pub(crate) fn into_error_v2(self, text: &str) -> Error {
268 let diagnostics = sorted_for_sketch(self.diagnostics, &|_| 0);
269 Error::new(diagnostics, text)
270 }
271
272 pub(crate) fn into_warnings_v2(self) -> Vec<Diagnostic> {
274 sorted_for_sketch(self.diagnostics, &|_| 0)
275 }
276
277 pub(crate) fn into_sketch_error(self, map: &SourceMap, rank: &dyn Fn(Span) -> u32) -> Error {
281 let diagnostics = sorted_for_sketch(self.diagnostics, rank);
282 Error::in_map(diagnostics, map)
283 }
284
285 pub(crate) fn into_warnings(mut self) -> Vec<Diagnostic> {
287 self.sort();
288 self.diagnostics
289 }
290
291 pub(crate) fn into_sketch_warnings(self, rank: &dyn Fn(Span) -> u32) -> Vec<Diagnostic> {
293 sorted_for_sketch(self.diagnostics, rank)
294 }
295}
296
297fn sorted_for_sketch(
299 mut diagnostics: Vec<Diagnostic>,
300 rank: &dyn Fn(Span) -> u32,
301) -> Vec<Diagnostic> {
302 diagnostics.sort_by(|a, b| {
303 let (sa, sb) = (a.primary().span(), b.primary().span());
304 rank(sa)
305 .cmp(&rank(sb))
306 .then(sa.start().cmp(&sb.start()))
307 .then(sa.end().cmp(&sb.end()))
308 .then(a.code().cmp(&b.code()))
309 .then(a.message().cmp(b.message()))
310 });
311 diagnostics.dedup_by(|a, b| {
312 a.primary().span() == b.primary().span()
313 && a.code() == b.code()
314 && a.message() == b.message()
315 });
316 diagnostics
317}
318
319#[cfg(test)]
320mod tests {
321 #![allow(clippy::unwrap_used, clippy::expect_used)]
322
323 use alloc::string::ToString;
324
325 use super::*;
326 use crate::codes;
327
328 #[test]
329 fn test_line_col_counts_characters_not_bytes() {
330 let text = "ab\nçd\n";
331 assert_eq!(line_col(text, 0), (1, 1));
332 assert_eq!(line_col(text, 3), (2, 1));
333 assert_eq!(line_col(text, 5), (2, 2));
335 assert_eq!(line_col(text, 999), (3, 1));
336 }
337
338 #[test]
339 fn test_line_col_inside_multibyte_char_does_not_panic() {
340 assert_eq!(line_col("ç", 1), (1, 1));
341 }
342
343 #[test]
344 fn test_display_counts_additional_errors() {
345 let mut report = Report::default();
346 report.error(codes::MISSING, Span::new(4, 5), "second");
347 report.error(codes::MISSING, Span::new(0, 1), "first");
348 let err = report.into_error("abcdef");
349 assert_eq!(err.to_string(), "1:1: first (and 1 more error)");
350
351 let mut report = Report::default();
352 for _ in 0..3 {
353 report.error(codes::MISSING, Span::new(0, 0), "same");
354 }
355 assert_eq!(
356 report.into_error("").to_string(),
357 "1:1: same (and 2 more errors)"
358 );
359 }
360
361 #[test]
362 fn test_report_help_is_attached() {
363 let mut report = Report::default();
364 report.error_help(codes::MISSING, Span::new(0, 1), "bad", "fix it");
365 assert!(!report.is_clean());
366 let err = report.into_error("x");
367 assert_eq!(err.diagnostics()[0].help().collect::<Vec<_>>(), ["fix it"]);
368 assert_eq!(err.diagnostics()[0].code(), Some(codes::MISSING));
369 }
370
371 #[test]
372 fn test_report_warnings_keep_it_clean() {
373 let mut report = Report::default();
374 report.warning(codes::UNUSED_RULE, Span::new(0, 1), "unused");
375 assert!(report.is_clean());
376 assert_eq!(report.errors(), 0);
377 let warnings = report.into_warnings();
378 assert_eq!(warnings.len(), 1);
379 assert_eq!(warnings[0].severity(), Severity::Warning);
380 }
381
382 #[test]
383 fn test_sketch_errors_name_the_file_and_dedup() {
384 let mut map = SourceMap::new();
385 let _ = map.add("a.lsf", "x\n").expect("fits");
386 let _ = map.add("b.lsf", "yy\nzz\n").expect("fits");
387 let mut report = Report::default();
388 report.error(codes::MISSING, Span::new(6, 7), "late");
389 report.error(codes::MISSING, Span::new(6, 7), "late");
390 report.error(codes::MISSING, Span::new(1, 2), "early");
391 let err = report.into_sketch_error(&map, &|_| 0);
392 assert_eq!(err.diagnostics().len(), 2);
393 assert_eq!(err.to_string(), "a.lsf:1:2: early (and 1 more error)");
394 }
395}