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