Skip to main content

lutra_compiler/
error.rs

1use std::collections::HashMap;
2use std::fmt::{self, Write};
3use std::path::{Path, PathBuf};
4
5use itertools::Itertools;
6
7use crate::codespan;
8use crate::diagnostic::{Diagnostic, DiagnosticCode};
9
10#[derive(thiserror::Error, Debug)]
11pub enum Error {
12    #[error("io error: {0}")]
13    Io(#[from] std::io::Error),
14
15    #[error("invalid path: {path}")]
16    InvalidPath { path: PathBuf },
17
18    #[error("cannot find project root")]
19    CannotFindProjectRoot,
20
21    #[error("cannot read source file at {file}:\n  {io}")]
22    CannotReadSourceFile {
23        file: std::path::PathBuf,
24        io: std::io::Error,
25    },
26
27    #[error("{}", DisplayMessages(.diagnostics))]
28    Compile { diagnostics: Vec<DiagnosticMessage> },
29
30    #[error("{message}")]
31    UnsupportedExternal { message: String },
32}
33
34impl Error {
35    pub(crate) fn from_diagnostics(
36        diagnostics: Vec<Diagnostic>,
37        sources: &impl crate::project::SourceProvider,
38    ) -> Self {
39        let diagnostics = compose_diagnostic_messages(diagnostics, sources);
40        Error::Compile { diagnostics }
41    }
42}
43
44#[derive(Debug)]
45pub struct DiagnosticMessage {
46    diagnostic: Diagnostic,
47    display: String,
48    range: codespan::Range,
49    additional_ranges: Vec<Option<codespan::Range>>,
50}
51
52impl DiagnosticMessage {
53    pub fn code(&self) -> &'static str {
54        self.diagnostic.code.get()
55    }
56
57    pub fn message(&self) -> &str {
58        &self.diagnostic.message
59    }
60
61    pub fn span(&self) -> &Option<crate::Span> {
62        &self.diagnostic.span
63    }
64
65    pub fn additional(&self) -> Vec<Additional<'_>> {
66        self.diagnostic
67            .additional
68            .iter()
69            .enumerate()
70            .map(|(i, additional)| Additional {
71                additional,
72                range: self.additional_ranges.get(i).and_then(|x| x.as_ref()),
73            })
74            .collect()
75    }
76
77    pub fn display(&self) -> &str {
78        &self.display
79    }
80
81    pub fn range(&self) -> &codespan::Range {
82        &self.range
83    }
84}
85
86#[derive(Debug)]
87pub struct Additional<'d> {
88    additional: &'d crate::diagnostic::Additional,
89    range: Option<&'d codespan::Range>,
90}
91
92impl<'d> Additional<'d> {
93    pub fn message(&self) -> &'d str {
94        &self.additional.message
95    }
96
97    pub fn span(&self) -> Option<codespan::Span> {
98        self.additional.span
99    }
100
101    pub fn range(&self) -> Option<&'d codespan::Range> {
102        self.range
103    }
104}
105
106fn compose_diagnostic_messages(
107    diagnostics: Vec<Diagnostic>,
108    sources: &impl crate::project::SourceProvider,
109) -> Vec<DiagnosticMessage> {
110    let mut cache = FileTreeCache::new(sources);
111
112    let mut messages = Vec::with_capacity(diagnostics.len());
113    for diagnostic in diagnostics {
114        let Some(span) = diagnostic.span else {
115            panic!(
116                "missing diagnostic span: [{:?}] {}, {:#?}",
117                diagnostic.code, diagnostic.message, diagnostic.additional
118            );
119        };
120
121        let range = compose_range(span, sources, &mut cache);
122
123        let display = compose_display(&diagnostic, sources, &mut cache);
124
125        let mut additional_ranges = Vec::with_capacity(diagnostic.additional.len());
126        for a in &diagnostic.additional {
127            let range = a.span.map(|s| compose_range(s, sources, &mut cache));
128            additional_ranges.push(range);
129        }
130
131        messages.push(DiagnosticMessage {
132            diagnostic,
133            display,
134            range,
135            additional_ranges,
136        });
137    }
138    messages
139}
140
141fn compose_display<S>(
142    diagnostic: &Diagnostic,
143    sources: &impl crate::project::SourceProvider,
144    cache: &mut FileTreeCache<S>,
145) -> String
146where
147    S: crate::project::SourceProvider,
148{
149    use ariadne::{Config, Label, Report, ReportKind};
150
151    let config = Config::default().with_color(false);
152
153    let span = diagnostic.span.unwrap();
154    let (source_path, _) = sources.get_by_id(span.source_id).unwrap();
155    let span = std::ops::Range::from(span);
156
157    let kind = match diagnostic.code.get_severity() {
158        crate::diagnostic::Severity::Warning => ReportKind::Warning,
159        crate::diagnostic::Severity::Error => ReportKind::Error,
160    };
161
162    let mut report = Report::build(kind, (source_path, span.clone()))
163        .with_config(config)
164        .with_label(Label::new((source_path, span)).with_message(&diagnostic.message));
165
166    if diagnostic.code != DiagnosticCode::CUSTOM {
167        report = report.with_code(diagnostic.code.get());
168    }
169
170    let mut notes = String::new();
171    for additional in &diagnostic.additional {
172        if let Some(span) = additional.span {
173            let span = std::ops::Range::from(span);
174            report.add_label(Label::new((source_path, span)).with_message(&diagnostic.message))
175        } else {
176            notes += &additional.message;
177            notes += "\n";
178        }
179    }
180    if !notes.is_empty() {
181        report.set_note(notes);
182    }
183
184    let mut out = Vec::new();
185    report.finish().write(cache, &mut out).unwrap();
186    let out = String::from_utf8(out).unwrap();
187    out.lines().map(|l| l.trim_end()).join("\n")
188}
189
190fn compose_range<S>(
191    span: codespan::Span,
192    sources: &impl crate::project::SourceProvider,
193    cache: &mut FileTreeCache<S>,
194) -> codespan::Range
195where
196    S: crate::project::SourceProvider,
197{
198    use ariadne::Cache;
199
200    let (source_path, _content) = sources.get_by_id(span.source_id).unwrap();
201    let source = cache.fetch(&source_path).unwrap();
202    let source_len = source.len();
203
204    let Some(start) = source.get_byte_line(span.start as usize) else {
205        panic!("span {span:?} is out of bounds of the source (len = {source_len})",);
206    };
207    let start = codespan::LineColumn {
208        line: start.1 as u32,
209        column: start.2 as u32,
210    };
211
212    let Some(end) = source.get_byte_line(span.start as usize + span.len as usize) else {
213        panic!("span {span:?} is out of bounds of the source (len = {source_len})",);
214    };
215    let end = codespan::LineColumn {
216        line: end.1 as u32,
217        column: end.2 as u32,
218    };
219    codespan::Range { start, end }
220}
221
222struct FileTreeCache<'a, S: crate::project::SourceProvider> {
223    provider: &'a S,
224    cache: HashMap<PathBuf, ariadne::Source>,
225}
226impl<'a, S: crate::project::SourceProvider> FileTreeCache<'a, S> {
227    fn new(file_tree: &'a S) -> Self {
228        FileTreeCache {
229            provider: file_tree,
230            cache: HashMap::new(),
231        }
232    }
233}
234
235impl<'a, S: crate::project::SourceProvider> ariadne::Cache<&Path> for FileTreeCache<'a, S> {
236    type Storage = String;
237
238    fn fetch(
239        &mut self,
240        path: &&Path,
241    ) -> Result<&ariadne::Source<<Self as ariadne::Cache<&Path>>::Storage>, impl std::fmt::Debug>
242    {
243        let (_, content) = match self.provider.get_by_path(path) {
244            Some(v) => v,
245            None => return Err(format!("Unknown file `{path:?}`")),
246        };
247
248        Ok(self
249            .cache
250            .entry((*path).to_owned())
251            .or_insert_with(|| ariadne::Source::from(content.to_string())))
252    }
253
254    fn display<'b>(&self, id: &'b &Path) -> Option<impl std::fmt::Display + 'b> {
255        if id.as_os_str().is_empty() {
256            Some(self.provider.get_root().file_name()?.display().to_string())
257        } else {
258            Some(id.display().to_string())
259        }
260    }
261}
262
263struct DisplayMessages<'a>(&'a Vec<DiagnosticMessage>);
264
265impl<'a> std::fmt::Display for DisplayMessages<'a> {
266    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267        for d in self.0 {
268            f.write_str(&d.display)?;
269            f.write_char('\n')?;
270        }
271        Ok(())
272    }
273}