i_slint_compiler/
diagnostics.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use std::io::Read;
5use std::path::{Path, PathBuf};
6use std::rc::Rc;
7
8use crate::parser::TextSize;
9use std::collections::BTreeSet;
10
11/// Span represent an error location within a file.
12///
13/// Currently, it is just an offset in byte within the file.
14///
15/// When the `proc_macro_span` feature is enabled, it may also hold a proc_macro span.
16#[derive(Debug, Clone)]
17pub struct Span {
18    pub offset: usize,
19    #[cfg(feature = "proc_macro_span")]
20    pub span: Option<proc_macro::Span>,
21}
22
23impl Span {
24    pub fn is_valid(&self) -> bool {
25        self.offset != usize::MAX
26    }
27
28    #[allow(clippy::needless_update)] // needed when `proc_macro_span` is enabled
29    pub fn new(offset: usize) -> Self {
30        Self { offset, ..Default::default() }
31    }
32}
33
34impl Default for Span {
35    fn default() -> Self {
36        Span {
37            offset: usize::MAX,
38            #[cfg(feature = "proc_macro_span")]
39            span: Default::default(),
40        }
41    }
42}
43
44impl PartialEq for Span {
45    fn eq(&self, other: &Span) -> bool {
46        self.offset == other.offset
47    }
48}
49
50#[cfg(feature = "proc_macro_span")]
51impl From<proc_macro::Span> for Span {
52    fn from(span: proc_macro::Span) -> Self {
53        Self { span: Some(span), ..Default::default() }
54    }
55}
56
57/// Returns a span.  This is implemented for tokens and nodes
58pub trait Spanned {
59    fn span(&self) -> Span;
60    fn source_file(&self) -> Option<&SourceFile>;
61    fn to_source_location(&self) -> SourceLocation {
62        SourceLocation { source_file: self.source_file().cloned(), span: self.span() }
63    }
64}
65
66#[derive(Default)]
67pub struct SourceFileInner {
68    path: PathBuf,
69
70    /// Complete source code of the path, used to map from offset to line number
71    source: Option<String>,
72
73    /// The offset of each linebreak
74    line_offsets: std::cell::OnceCell<Vec<usize>>,
75}
76
77impl std::fmt::Debug for SourceFileInner {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        write!(f, "{:?}", self.path)
80    }
81}
82
83impl SourceFileInner {
84    pub fn new(path: PathBuf, source: String) -> Self {
85        Self { path, source: Some(source), line_offsets: Default::default() }
86    }
87
88    pub fn path(&self) -> &Path {
89        &self.path
90    }
91
92    /// Create a SourceFile that has just a path, but no contents
93    pub fn from_path_only(path: PathBuf) -> Rc<Self> {
94        Rc::new(Self { path, ..Default::default() })
95    }
96
97    /// Returns a tuple with the line (starting at 1) and column number (starting at 1)
98    pub fn line_column(&self, offset: usize) -> (usize, usize) {
99        let line_offsets = self.line_offsets();
100        line_offsets.binary_search(&offset).map_or_else(
101            |line| {
102                if line == 0 {
103                    (1, offset + 1)
104                } else {
105                    (line + 1, line_offsets.get(line - 1).map_or(0, |x| offset - x + 1))
106                }
107            },
108            |line| (line + 2, 1),
109        )
110    }
111
112    pub fn text_size_to_file_line_column(
113        &self,
114        size: TextSize,
115    ) -> (String, usize, usize, usize, usize) {
116        let file_name = self.path().to_string_lossy().to_string();
117        let (start_line, start_column) = self.line_column(size.into());
118        (file_name, start_line, start_column, start_line, start_column)
119    }
120
121    /// Returns the offset that corresponds to the line/column
122    pub fn offset(&self, line: usize, column: usize) -> usize {
123        let col_offset = column.saturating_sub(1);
124        if line <= 1 {
125            // line == 0 is actually invalid!
126            return col_offset;
127        }
128        let offsets = self.line_offsets();
129        let index = std::cmp::min(line.saturating_sub(1), offsets.len());
130        offsets.get(index.saturating_sub(1)).unwrap_or(&0).saturating_add(col_offset)
131    }
132
133    fn line_offsets(&self) -> &[usize] {
134        self.line_offsets.get_or_init(|| {
135            self.source
136                .as_ref()
137                .map(|s| {
138                    s.bytes()
139                        .enumerate()
140                        // Add the offset one past the '\n' into the index: That's the first char
141                        // of the new line!
142                        .filter_map(|(i, c)| if c == b'\n' { Some(i + 1) } else { None })
143                        .collect()
144                })
145                .unwrap_or_default()
146        })
147    }
148
149    pub fn source(&self) -> Option<&str> {
150        self.source.as_deref()
151    }
152}
153
154pub type SourceFile = Rc<SourceFileInner>;
155
156pub fn load_from_path(path: &Path) -> Result<String, Diagnostic> {
157    let string = (if path == Path::new("-") {
158        let mut buffer = Vec::new();
159        let r = std::io::stdin().read_to_end(&mut buffer);
160        r.and_then(|_| {
161            String::from_utf8(buffer)
162                .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))
163        })
164    } else {
165        std::fs::read_to_string(path)
166    })
167    .map_err(|err| Diagnostic {
168        message: format!("Could not load {}: {}", path.display(), err),
169        span: SourceLocation {
170            source_file: Some(SourceFileInner::from_path_only(path.to_owned())),
171            span: Default::default(),
172        },
173        level: DiagnosticLevel::Error,
174    })?;
175
176    if path.extension().is_some_and(|e| e == "rs") {
177        return crate::lexer::extract_rust_macro(string).ok_or_else(|| Diagnostic {
178            message: "No `slint!` macro".into(),
179            span: SourceLocation {
180                source_file: Some(SourceFileInner::from_path_only(path.to_owned())),
181                span: Default::default(),
182            },
183            level: DiagnosticLevel::Error,
184        });
185    }
186
187    Ok(string)
188}
189
190#[derive(Debug, Clone, Default)]
191pub struct SourceLocation {
192    pub source_file: Option<SourceFile>,
193    pub span: Span,
194}
195
196impl Spanned for SourceLocation {
197    fn span(&self) -> Span {
198        self.span.clone()
199    }
200
201    fn source_file(&self) -> Option<&SourceFile> {
202        self.source_file.as_ref()
203    }
204}
205
206impl Spanned for Option<SourceLocation> {
207    fn span(&self) -> crate::diagnostics::Span {
208        self.as_ref().map(|n| n.span()).unwrap_or_default()
209    }
210
211    fn source_file(&self) -> Option<&SourceFile> {
212        self.as_ref().map(|n| n.source_file.as_ref()).unwrap_or_default()
213    }
214}
215
216/// This enum describes the level or severity of a diagnostic message produced by the compiler.
217#[derive(Debug, PartialEq, Copy, Clone, Default)]
218#[non_exhaustive]
219pub enum DiagnosticLevel {
220    /// The diagnostic found is an error that prevents successful compilation.
221    #[default]
222    Error,
223    /// The diagnostic found is a warning.
224    Warning,
225}
226
227#[cfg(feature = "display-diagnostics")]
228impl From<DiagnosticLevel> for codemap_diagnostic::Level {
229    fn from(l: DiagnosticLevel) -> Self {
230        match l {
231            DiagnosticLevel::Error => codemap_diagnostic::Level::Error,
232            DiagnosticLevel::Warning => codemap_diagnostic::Level::Warning,
233        }
234    }
235}
236
237/// This structure represent a diagnostic emitted while compiling .slint code.
238///
239/// It is basically a message, a level (warning or error), attached to a
240/// position in the code
241#[derive(Debug, Clone)]
242pub struct Diagnostic {
243    message: String,
244    span: SourceLocation,
245    level: DiagnosticLevel,
246}
247
248//NOTE! Diagnostic is re-exported in the public API of the interpreter
249impl Diagnostic {
250    /// Return the level for this diagnostic
251    pub fn level(&self) -> DiagnosticLevel {
252        self.level
253    }
254
255    /// Return a message for this diagnostic
256    pub fn message(&self) -> &str {
257        &self.message
258    }
259
260    /// Returns a tuple with the line (starting at 1) and column number (starting at 1)
261    ///
262    /// Can also return (0, 0) if the span is invalid
263    pub fn line_column(&self) -> (usize, usize) {
264        if !self.span.span.is_valid() {
265            return (0, 0);
266        }
267        let offset = self.span.span.offset;
268
269        match &self.span.source_file {
270            None => (0, 0),
271            Some(sl) => sl.line_column(offset),
272        }
273    }
274
275    /// return the path of the source file where this error is attached
276    pub fn source_file(&self) -> Option<&Path> {
277        self.span.source_file().map(|sf| sf.path())
278    }
279}
280
281impl std::fmt::Display for Diagnostic {
282    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283        if let Some(sf) = self.span.source_file() {
284            let (line, _) = self.line_column();
285            write!(f, "{}:{}: {}", sf.path.display(), line, self.message)
286        } else {
287            write!(f, "{}", self.message)
288        }
289    }
290}
291
292#[derive(Default)]
293pub struct BuildDiagnostics {
294    inner: Vec<Diagnostic>,
295
296    /// When false, throw error for experimental features
297    pub enable_experimental: bool,
298
299    /// This is the list of all loaded files (with or without diagnostic)
300    /// does not include the main file.
301    /// FIXME: this doesn't really belong in the diagnostics, it should be somehow returned in another way
302    /// (maybe in a compilation state that include the diagnostics?)
303    pub all_loaded_files: BTreeSet<PathBuf>,
304}
305
306impl IntoIterator for BuildDiagnostics {
307    type Item = Diagnostic;
308    type IntoIter = <Vec<Diagnostic> as IntoIterator>::IntoIter;
309    fn into_iter(self) -> Self::IntoIter {
310        self.inner.into_iter()
311    }
312}
313
314impl BuildDiagnostics {
315    pub fn push_diagnostic_with_span(
316        &mut self,
317        message: String,
318        span: SourceLocation,
319        level: DiagnosticLevel,
320    ) {
321        debug_assert!(
322            !message.as_str().ends_with('.'),
323            "Error message should not end with a period: ({message:?})"
324        );
325        self.inner.push(Diagnostic { message, span, level });
326    }
327    pub fn push_error_with_span(&mut self, message: String, span: SourceLocation) {
328        self.push_diagnostic_with_span(message, span, DiagnosticLevel::Error)
329    }
330    pub fn push_error(&mut self, message: String, source: &dyn Spanned) {
331        self.push_error_with_span(message, source.to_source_location());
332    }
333    pub fn push_warning_with_span(&mut self, message: String, span: SourceLocation) {
334        self.push_diagnostic_with_span(message, span, DiagnosticLevel::Warning)
335    }
336    pub fn push_warning(&mut self, message: String, source: &dyn Spanned) {
337        self.push_warning_with_span(message, source.to_source_location());
338    }
339    pub fn push_compiler_error(&mut self, error: Diagnostic) {
340        self.inner.push(error);
341    }
342
343    pub fn push_property_deprecation_warning(
344        &mut self,
345        old_property: &str,
346        new_property: &str,
347        source: &dyn Spanned,
348    ) {
349        self.push_diagnostic_with_span(
350            format!(
351                "The property '{old_property}' has been deprecated. Please use '{new_property}' instead"
352            ),
353            source.to_source_location(),
354            crate::diagnostics::DiagnosticLevel::Warning,
355        )
356    }
357
358    /// Return true if there is at least one compilation error for this file
359    pub fn has_errors(&self) -> bool {
360        self.inner.iter().any(|diag| diag.level == DiagnosticLevel::Error)
361    }
362
363    /// Return true if there are no diagnostics (warnings or errors); false otherwise.
364    pub fn is_empty(&self) -> bool {
365        self.inner.is_empty()
366    }
367
368    #[cfg(feature = "display-diagnostics")]
369    fn call_diagnostics<Output>(
370        self,
371        output: &mut Output,
372        mut handle_no_source: Option<&mut dyn FnMut(Diagnostic)>,
373        emitter_factory: impl for<'b> FnOnce(
374            &'b mut Output,
375            Option<&'b codemap::CodeMap>,
376        ) -> codemap_diagnostic::Emitter<'b>,
377    ) {
378        if self.inner.is_empty() {
379            return;
380        }
381
382        let mut codemap = codemap::CodeMap::new();
383        let mut codemap_files = std::collections::HashMap::new();
384
385        let diags: Vec<_> = self
386            .inner
387            .into_iter()
388            .filter_map(|d| {
389                let spans = if !d.span.span.is_valid() {
390                    vec![]
391                } else if let Some(sf) = &d.span.source_file {
392                    if let Some(ref mut handle_no_source) = handle_no_source {
393                        if sf.source.is_none() {
394                            handle_no_source(d);
395                            return None;
396                        }
397                    }
398                    let path: String = sf.path.to_string_lossy().into();
399                    let file = codemap_files.entry(path).or_insert_with(|| {
400                        codemap.add_file(
401                            sf.path.to_string_lossy().into(),
402                            sf.source.clone().unwrap_or_default(),
403                        )
404                    });
405                    let file_span = file.span;
406                    let s = codemap_diagnostic::SpanLabel {
407                        span: file_span
408                            .subspan(d.span.span.offset as u64, d.span.span.offset as u64),
409                        style: codemap_diagnostic::SpanStyle::Primary,
410                        label: None,
411                    };
412                    vec![s]
413                } else {
414                    vec![]
415                };
416                Some(codemap_diagnostic::Diagnostic {
417                    level: d.level.into(),
418                    message: d.message,
419                    code: None,
420                    spans,
421                })
422            })
423            .collect();
424
425        if !diags.is_empty() {
426            let mut emitter = emitter_factory(output, Some(&codemap));
427            emitter.emit(&diags);
428        }
429    }
430
431    #[cfg(feature = "display-diagnostics")]
432    /// Print the diagnostics on the console
433    pub fn print(self) {
434        self.call_diagnostics(&mut (), None, |_, codemap| {
435            codemap_diagnostic::Emitter::stderr(codemap_diagnostic::ColorConfig::Always, codemap)
436        });
437    }
438
439    #[cfg(feature = "display-diagnostics")]
440    /// Print into a string
441    pub fn diagnostics_as_string(self) -> String {
442        let mut output = Vec::new();
443        self.call_diagnostics(&mut output, None, |output, codemap| {
444            codemap_diagnostic::Emitter::vec(output, codemap)
445        });
446
447        String::from_utf8(output).expect(
448            "Internal error: There were errors during compilation but they did not result in valid utf-8 diagnostics!"
449        )
450    }
451
452    #[cfg(all(feature = "proc_macro_span", feature = "display-diagnostics"))]
453    /// Will convert the diagnostics that only have offsets to the actual proc_macro::Span
454    pub fn report_macro_diagnostic(
455        self,
456        span_map: &[crate::parser::Token],
457    ) -> proc_macro::TokenStream {
458        let mut result = proc_macro::TokenStream::default();
459        let mut needs_error = self.has_errors();
460        self.call_diagnostics(
461            &mut (),
462            Some(&mut |diag| {
463                let span = diag.span.span.span.or_else(|| {
464                    //let pos =
465                    //span_map.binary_search_by_key(d.span.offset, |x| x.0).unwrap_or_else(|x| x);
466                    //d.span.span = span_map.get(pos).as_ref().map(|x| x.1);
467                    let mut offset = 0;
468                    span_map.iter().find_map(|t| {
469                        if diag.span.span.offset <= offset {
470                            t.span
471                        } else {
472                            offset += t.text.len();
473                            None
474                        }
475                    })
476                });
477                let message = &diag.message;
478                match diag.level {
479                    DiagnosticLevel::Error => {
480                        needs_error = false;
481                        result.extend(proc_macro::TokenStream::from(if let Some(span) = span {
482                            quote::quote_spanned!(span.into()=> compile_error!{ #message })
483                        } else {
484                            quote::quote!(compile_error! { #message })
485                        }));
486                    }
487                    DiagnosticLevel::Warning => {
488                        result.extend(proc_macro::TokenStream::from(if let Some(span) = span {
489                            quote::quote_spanned!(span.into()=> const _ : () = { #[deprecated(note = #message)] const WARNING: () = (); WARNING };)
490                        } else {
491                            quote::quote!(const _ : () = { #[deprecated(note = #message)] const WARNING: () = (); WARNING };)
492                        }));
493                    },
494                }
495            }),
496            |_, codemap| {
497                codemap_diagnostic::Emitter::stderr(
498                    codemap_diagnostic::ColorConfig::Always,
499                    codemap,
500                )
501            },
502        );
503        if needs_error {
504            result.extend(proc_macro::TokenStream::from(quote::quote!(
505                compile_error! { "Error occurred" }
506            )))
507        }
508        result
509    }
510
511    pub fn to_string_vec(&self) -> Vec<String> {
512        self.inner.iter().map(|d| d.to_string()).collect()
513    }
514
515    pub fn push_diagnostic(
516        &mut self,
517        message: String,
518        source: &dyn Spanned,
519        level: DiagnosticLevel,
520    ) {
521        self.push_diagnostic_with_span(message, source.to_source_location(), level)
522    }
523
524    pub fn push_internal_error(&mut self, err: Diagnostic) {
525        self.inner.push(err)
526    }
527
528    pub fn iter(&self) -> impl Iterator<Item = &Diagnostic> {
529        self.inner.iter()
530    }
531
532    #[cfg(feature = "display-diagnostics")]
533    #[must_use]
534    pub fn check_and_exit_on_error(self) -> Self {
535        if self.has_errors() {
536            self.print();
537            std::process::exit(-1);
538        }
539        self
540    }
541
542    #[cfg(feature = "display-diagnostics")]
543    pub fn print_warnings_and_exit_on_error(self) {
544        let has_error = self.has_errors();
545        self.print();
546        if has_error {
547            std::process::exit(-1);
548        }
549    }
550}
551
552#[cfg(test)]
553mod tests {
554    use super::*;
555
556    #[test]
557    fn test_source_file_offset_line_column_mapping() {
558        let content = r#"import { LineEdit, Button, Slider, HorizontalBox, VerticalBox } from "std-widgets.slint";
559
560component MainWindow inherits Window {
561    property <duration> total-time: slider.value * 1s;
562
563    callback tick(duration);
564    VerticalBox {
565        HorizontalBox {
566            padding-left: 0;
567            Text { text: "Elapsed Time:"; }
568            Rectangle {
569                Rectangle {
570                    height: 100%;
571                    background: lightblue;
572                }
573            }
574        }
575    }
576
577
578}
579
580
581    "#.to_string();
582        let sf = SourceFileInner::new(PathBuf::from("foo.slint"), content.clone());
583
584        let mut line = 1;
585        let mut column = 1;
586        for offset in 0..content.len() {
587            let b = *content.as_bytes().get(offset).unwrap();
588
589            assert_eq!(sf.offset(line, column), offset);
590            assert_eq!(sf.line_column(offset), (line, column));
591
592            if b == b'\n' {
593                line += 1;
594                column = 1;
595            } else {
596                column += 1;
597            }
598        }
599    }
600}