sixtyfps_compilerlib/
diagnostics.rs

1// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
2// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
3
4use std::io::Read;
5use std::path::{Path, PathBuf};
6use std::rc::Rc;
7
8/// Span represent an error location within a file.
9///
10/// Currently, it is just an offset in byte within the file.
11///
12/// When the `proc_macro_span` feature is enabled, it may also hold a proc_macro span.
13#[derive(Debug, Clone)]
14pub struct Span {
15    pub offset: usize,
16    #[cfg(feature = "proc_macro_span")]
17    pub span: Option<proc_macro::Span>,
18}
19
20impl Span {
21    pub fn is_valid(&self) -> bool {
22        self.offset != usize::MAX
23    }
24
25    #[allow(clippy::needless_update)] // needed when `proc_macro_span` is enabled
26    pub fn new(offset: usize) -> Self {
27        Self { offset, ..Default::default() }
28    }
29}
30
31impl Default for Span {
32    fn default() -> Self {
33        Span {
34            offset: usize::MAX,
35            #[cfg(feature = "proc_macro_span")]
36            span: Default::default(),
37        }
38    }
39}
40
41impl PartialEq for Span {
42    fn eq(&self, other: &Span) -> bool {
43        self.offset == other.offset
44    }
45}
46
47#[cfg(feature = "proc_macro_span")]
48impl From<proc_macro::Span> for Span {
49    fn from(span: proc_macro::Span) -> Self {
50        Self { span: Some(span), ..Default::default() }
51    }
52}
53
54/// Returns a span.  This is implemented for tokens and nodes
55pub trait Spanned {
56    fn span(&self) -> Span;
57    fn source_file(&self) -> Option<&SourceFile>;
58    fn to_source_location(&self) -> SourceLocation {
59        SourceLocation { source_file: self.source_file().cloned(), span: self.span() }
60    }
61}
62
63#[derive(Default)]
64pub struct SourceFileInner {
65    path: PathBuf,
66
67    /// Complete source code of the path, used to map from offset to line number
68    source: Option<String>,
69
70    /// The offset of each linebreak
71    line_offsets: once_cell::unsync::OnceCell<Vec<usize>>,
72}
73
74impl std::fmt::Debug for SourceFileInner {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        write!(f, "{:?}", self.path)
77    }
78}
79
80impl SourceFileInner {
81    pub fn new(path: PathBuf, source: String) -> Self {
82        Self { path, source: Some(source), line_offsets: Default::default() }
83    }
84
85    pub fn path(&self) -> &Path {
86        &self.path
87    }
88
89    /// Create a SourceFile that has just a path, but no contents
90    pub fn from_path_only(path: PathBuf) -> Rc<Self> {
91        Rc::new(Self { path, ..Default::default() })
92    }
93
94    fn line_offsets(&self) -> &[usize] {
95        self.line_offsets.get_or_init(|| {
96            self.source
97                .as_ref()
98                .map(|s| {
99                    s.bytes()
100                        .enumerate()
101                        .filter_map(|(i, c)| if c == b'\n' { Some(i) } else { None })
102                        .collect()
103                })
104                .unwrap_or_default()
105        })
106    }
107}
108
109pub type SourceFile = Rc<SourceFileInner>;
110
111pub fn load_from_path(path: &Path) -> Result<String, Diagnostic> {
112    (if path == Path::new("-") {
113        let mut buffer = Vec::new();
114        let r = std::io::stdin().read_to_end(&mut buffer);
115        r.and_then(|_| {
116            String::from_utf8(buffer)
117                .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))
118        })
119    } else {
120        std::fs::read_to_string(path)
121    })
122    .map_err(|err| Diagnostic {
123        message: format!("Could not load {}: {}", path.display(), err),
124        span: SourceLocation {
125            source_file: Some(SourceFileInner::from_path_only(path.to_owned())),
126            span: Default::default(),
127        },
128        level: DiagnosticLevel::Error,
129    })
130}
131
132#[derive(Debug, Clone, Default)]
133pub struct SourceLocation {
134    pub source_file: Option<SourceFile>,
135    pub span: Span,
136}
137
138impl Spanned for SourceLocation {
139    fn span(&self) -> Span {
140        self.span.clone()
141    }
142
143    fn source_file(&self) -> Option<&SourceFile> {
144        self.source_file.as_ref()
145    }
146}
147
148impl Spanned for Option<SourceLocation> {
149    fn span(&self) -> crate::diagnostics::Span {
150        self.as_ref().map(|n| n.span()).unwrap_or_default()
151    }
152
153    fn source_file(&self) -> Option<&SourceFile> {
154        self.as_ref().map(|n| n.source_file.as_ref()).unwrap_or_default()
155    }
156}
157
158/// Diagnostics level (error or warning)
159#[derive(Debug, PartialEq, Copy, Clone)]
160pub enum DiagnosticLevel {
161    Error,
162    Warning,
163}
164
165impl Default for DiagnosticLevel {
166    fn default() -> Self {
167        Self::Error
168    }
169}
170
171#[cfg(feature = "display-diagnostics")]
172impl From<DiagnosticLevel> for codemap_diagnostic::Level {
173    fn from(l: DiagnosticLevel) -> Self {
174        match l {
175            DiagnosticLevel::Error => codemap_diagnostic::Level::Error,
176            DiagnosticLevel::Warning => codemap_diagnostic::Level::Warning,
177        }
178    }
179}
180
181/// This structure represent a diagnostic emitted while compiling .60 code.
182///
183/// It is basically a message, a level (warning or error), attached to a
184/// position in the code
185#[derive(Debug, Clone)]
186pub struct Diagnostic {
187    message: String,
188    span: SourceLocation,
189    level: DiagnosticLevel,
190}
191
192impl Diagnostic {
193    /// Return the level for this diagnostic
194    pub fn level(&self) -> DiagnosticLevel {
195        self.level
196    }
197
198    /// Return a message for this diagnostic
199    pub fn message(&self) -> &str {
200        &self.message
201    }
202
203    /// Returns a tuple with the line (starting at 1) and column number (starting at 0)
204    pub fn line_column(&self) -> (usize, usize) {
205        let offset = self.span.span.offset;
206        let line_offsets = match &self.span.source_file {
207            None => return (0, 0),
208            Some(sl) => sl.line_offsets(),
209        };
210        line_offsets.binary_search(&offset).map_or_else(
211            |line| {
212                if line == 0 {
213                    (line + 1, offset)
214                } else {
215                    (line + 1, line_offsets.get(line - 1).map_or(0, |x| offset - x))
216                }
217            },
218            |line| (line + 1, 0),
219        )
220    }
221
222    /// return the path of the source file where this error is attached
223    pub fn source_file(&self) -> Option<&Path> {
224        self.span.source_file().map(|sf| sf.path())
225    }
226}
227
228impl std::fmt::Display for Diagnostic {
229    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230        if let Some(sf) = self.span.source_file() {
231            let (line, _) = self.line_column();
232            write!(f, "{}:{}: {}", sf.path.display(), line, self.message)
233        } else {
234            write!(f, "{}", self.message)
235        }
236    }
237}
238
239#[derive(Default)]
240pub struct BuildDiagnostics {
241    inner: Vec<Diagnostic>,
242
243    /// This is the list of all loaded files (with or without diagnostic)
244    /// does not include the main file.
245    /// FIXME: this doesn't really belong in the diagnostics, it should be somehow returned in another way
246    /// (maybe in a compilation state that include the diagnostics?)
247    pub all_loaded_files: Vec<PathBuf>,
248}
249
250impl IntoIterator for BuildDiagnostics {
251    type Item = Diagnostic;
252    type IntoIter = <Vec<Diagnostic> as IntoIterator>::IntoIter;
253    fn into_iter(self) -> Self::IntoIter {
254        self.inner.into_iter()
255    }
256}
257
258impl BuildDiagnostics {
259    pub fn push_diagnostic_with_span(
260        &mut self,
261        message: String,
262        span: SourceLocation,
263        level: DiagnosticLevel,
264    ) {
265        debug_assert!(
266            !message.as_str().ends_with('.'),
267            "Error message should not end with a period: ({:?})",
268            message
269        );
270        self.inner.push(Diagnostic { message, span, level });
271    }
272    pub fn push_error_with_span(&mut self, message: String, span: SourceLocation) {
273        self.push_diagnostic_with_span(message, span, DiagnosticLevel::Error)
274    }
275    pub fn push_error(&mut self, message: String, source: &dyn Spanned) {
276        self.push_error_with_span(message, source.to_source_location());
277    }
278    pub fn push_warning_with_span(&mut self, message: String, span: SourceLocation) {
279        self.push_diagnostic_with_span(message, span, DiagnosticLevel::Warning)
280    }
281    pub fn push_warning(&mut self, message: String, source: &dyn Spanned) {
282        self.push_warning_with_span(message, source.to_source_location());
283    }
284    pub fn push_compiler_error(&mut self, error: Diagnostic) {
285        self.inner.push(error);
286    }
287
288    pub fn push_property_deprecation_warning(
289        &mut self,
290        old_property: &str,
291        new_property: &str,
292        source: &dyn Spanned,
293    ) {
294        self.push_diagnostic_with_span(
295            format!(
296                "The property '{}' has been deprecated. Please use '{}' instead",
297                old_property, new_property
298            ),
299            source.to_source_location(),
300            crate::diagnostics::DiagnosticLevel::Warning,
301        )
302    }
303
304    /// Return true if there is at least one compilation error for this file
305    pub fn has_error(&self) -> bool {
306        self.inner.iter().any(|diag| diag.level == DiagnosticLevel::Error)
307    }
308
309    /// Return true if there are no diagnostics (warnings or errors); false otherwise.
310    pub fn is_empty(&self) -> bool {
311        self.inner.is_empty()
312    }
313
314    #[cfg(feature = "display-diagnostics")]
315    fn call_diagnostics<Output>(
316        self,
317        output: &mut Output,
318        mut handle_no_source: Option<&mut dyn FnMut(Diagnostic)>,
319        emitter_factory: impl for<'b> FnOnce(
320            &'b mut Output,
321            Option<&'b codemap::CodeMap>,
322        ) -> codemap_diagnostic::Emitter<'b>,
323    ) {
324        if self.inner.is_empty() {
325            return;
326        }
327
328        let mut codemap = codemap::CodeMap::new();
329        let mut codemap_files = std::collections::HashMap::new();
330
331        let diags: Vec<_> = self
332            .inner
333            .into_iter()
334            .filter_map(|d| {
335                let spans = if !d.span.span.is_valid() {
336                    vec![]
337                } else if let Some(sf) = &d.span.source_file {
338                    if let Some(ref mut handle_no_source) = handle_no_source {
339                        if sf.source.is_none() {
340                            handle_no_source(d);
341                            return None;
342                        }
343                    }
344                    let path: String = sf.path.to_string_lossy().into();
345                    let file = codemap_files.entry(path).or_insert_with(|| {
346                        codemap.add_file(
347                            sf.path.to_string_lossy().into(),
348                            sf.source.clone().unwrap_or_default(),
349                        )
350                    });
351                    let file_span = file.span;
352                    let s = codemap_diagnostic::SpanLabel {
353                        span: file_span
354                            .subspan(d.span.span.offset as u64, d.span.span.offset as u64),
355                        style: codemap_diagnostic::SpanStyle::Primary,
356                        label: None,
357                    };
358                    vec![s]
359                } else {
360                    vec![]
361                };
362                Some(codemap_diagnostic::Diagnostic {
363                    level: d.level.into(),
364                    message: d.message,
365                    code: None,
366                    spans,
367                })
368            })
369            .collect();
370
371        let mut emitter = emitter_factory(output, Some(&codemap));
372        emitter.emit(&diags);
373    }
374
375    #[cfg(feature = "display-diagnostics")]
376    /// Print the diagnostics on the console
377    pub fn print(self) {
378        self.call_diagnostics(&mut (), None, |_, codemap| {
379            codemap_diagnostic::Emitter::stderr(codemap_diagnostic::ColorConfig::Always, codemap)
380        });
381    }
382
383    #[cfg(feature = "display-diagnostics")]
384    /// Print into a string
385    pub fn diagnostics_as_string(self) -> String {
386        let mut output = Vec::new();
387        self.call_diagnostics(&mut output, None, |output, codemap| {
388            codemap_diagnostic::Emitter::vec(output, codemap)
389        });
390
391        String::from_utf8(output).expect(
392            "Internal error: There were errors during compilation but they did not result in valid utf-8 diagnostics!"
393        )
394    }
395
396    #[cfg(all(feature = "proc_macro_span", feature = "display-diagnostics"))]
397    /// Will convert the diagnostics that only have offsets to the actual proc_macro::Span
398    pub fn report_macro_diagnostic(
399        self,
400        span_map: &[crate::parser::Token],
401    ) -> proc_macro::TokenStream {
402        let mut result = proc_macro::TokenStream::default();
403        let mut needs_error = self.has_error();
404        self.call_diagnostics(
405            &mut (),
406            Some(&mut |diag| {
407                let span = diag.span.span.span.or_else(|| {
408                    //let pos =
409                    //span_map.binary_search_by_key(d.span.offset, |x| x.0).unwrap_or_else(|x| x);
410                    //d.span.span = span_map.get(pos).as_ref().map(|x| x.1);
411                    let mut offset = 0;
412                    span_map.iter().find_map(|t| {
413                        if diag.span.span.offset <= offset {
414                            t.span
415                        } else {
416                            offset += t.text.len();
417                            None
418                        }
419                    })
420                });
421                let message = &diag.message;
422                match diag.level {
423                    DiagnosticLevel::Error => {
424                        needs_error = false;
425                        result.extend(proc_macro::TokenStream::from(if let Some(span) = span {
426                            quote::quote_spanned!(span.into() => compile_error!{ #message })
427                        } else {
428                            quote::quote!(compile_error! { #message })
429                        }));
430                    }
431                    // FIXME: find a way to report warnings.
432                    DiagnosticLevel::Warning => (),
433                }
434            }),
435            |_, codemap| {
436                codemap_diagnostic::Emitter::stderr(
437                    codemap_diagnostic::ColorConfig::Always,
438                    codemap,
439                )
440            },
441        );
442        if needs_error {
443            result.extend(proc_macro::TokenStream::from(quote::quote!(
444                compile_error! { "Error occurred" }
445            )))
446        }
447        result
448    }
449
450    pub fn to_string_vec(&self) -> Vec<String> {
451        self.inner.iter().map(|d| d.to_string()).collect()
452    }
453
454    pub fn push_diagnostic(
455        &mut self,
456        message: String,
457        source: &dyn Spanned,
458        level: DiagnosticLevel,
459    ) {
460        self.push_diagnostic_with_span(message, source.to_source_location(), level)
461    }
462
463    pub fn push_internal_error(&mut self, err: Diagnostic) {
464        self.inner.push(err)
465    }
466
467    pub fn iter(&self) -> impl Iterator<Item = &Diagnostic> {
468        self.inner.iter()
469    }
470
471    #[cfg(feature = "display-diagnostics")]
472    #[must_use]
473    pub fn check_and_exit_on_error(self) -> Self {
474        if self.has_error() {
475            self.print();
476            std::process::exit(-1);
477        }
478        self
479    }
480
481    #[cfg(feature = "display-diagnostics")]
482    pub fn print_warnings_and_exit_on_error(self) {
483        let has_error = self.has_error();
484        self.print();
485        if has_error {
486            std::process::exit(-1);
487        }
488    }
489}