1use std::io::Read;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use crate::parser::TextSize;
9use std::collections::BTreeSet;
10
11#[derive(Debug, Clone, PartialEq)]
15pub struct Span {
16 pub offset: usize,
17 pub length: usize,
18}
19
20impl Span {
21 pub fn is_valid(&self) -> bool {
22 self.offset != usize::MAX
23 }
24
25 pub fn new(offset: usize, length: usize) -> Self {
26 Self { offset, length }
27 }
28}
29
30impl Default for Span {
31 fn default() -> Self {
32 Span { offset: usize::MAX, length: 0 }
33 }
34}
35
36pub trait Spanned {
38 fn span(&self) -> Span;
39 fn source_file(&self) -> Option<&SourceFile>;
40 fn to_source_location(&self) -> SourceLocation {
41 SourceLocation { source_file: self.source_file().cloned(), span: self.span() }
42 }
43}
44
45#[derive(Default)]
46pub struct SourceFileInner {
47 path: PathBuf,
48
49 source: Option<String>,
51
52 line_offsets: std::sync::OnceLock<Vec<usize>>,
54}
55
56impl std::fmt::Debug for SourceFileInner {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 write!(f, "{:?}", self.path)
59 }
60}
61
62impl SourceFileInner {
63 pub fn new(path: PathBuf, source: String) -> Self {
64 Self { path, source: Some(source), line_offsets: Default::default() }
65 }
66
67 pub fn path(&self) -> &Path {
68 &self.path
69 }
70
71 pub fn from_path_only(path: PathBuf) -> Arc<Self> {
73 Arc::new(Self { path, ..Default::default() })
74 }
75
76 pub fn line_column(&self, offset: usize, format: ByteFormat) -> (usize, usize) {
78 let adjust_utf16 = |line_begin, col| {
79 if format == ByteFormat::Utf16
80 && let Some(source) = &self.source
81 {
82 return i_slint_common::unicode_utils::byte_offset_to_utf16_offset(
83 &source[line_begin..],
84 col,
85 );
86 }
87 col
88 };
89
90 let line_offsets = self.line_offsets();
91 line_offsets.binary_search(&offset).map_or_else(
92 |line| {
93 if line == 0 {
94 (1, adjust_utf16(0, offset) + 1)
95 } else {
96 let line_begin = *line_offsets.get(line - 1).unwrap_or(&0);
97 (line + 1, adjust_utf16(line_begin, offset - line_begin) + 1)
98 }
99 },
100 |line| (line + 2, 1),
101 )
102 }
103
104 pub fn text_size_to_file_line_column(
105 &self,
106 size: TextSize,
107 format: ByteFormat,
108 ) -> (String, usize, usize, usize, usize) {
109 let file_name = self.path().to_string_lossy().to_string();
110 let (start_line, start_column) = self.line_column(size.into(), format);
111 (file_name, start_line, start_column, start_line, start_column)
112 }
113
114 pub fn offset(&self, line: usize, column: usize, format: ByteFormat) -> usize {
116 let adjust_utf16 = |line_begin, col| {
117 if format == ByteFormat::Utf16
118 && let Some(source) = &self.source
119 {
120 return i_slint_common::unicode_utils::utf16_offset_to_byte_offset_clamped(
121 &source[line_begin..],
122 col,
123 );
124 }
125 col
126 };
127
128 let col_offset = column.saturating_sub(1);
129 if line <= 1 {
130 return adjust_utf16(0, col_offset);
132 }
133 let offsets = self.line_offsets();
134 let index = std::cmp::min(line.saturating_sub(1), offsets.len());
135 let line_offset = *offsets.get(index.saturating_sub(1)).unwrap_or(&0);
136 line_offset.saturating_add(adjust_utf16(line_offset, col_offset))
137 }
138
139 fn line_offsets(&self) -> &[usize] {
140 self.line_offsets.get_or_init(|| {
141 self.source
142 .as_ref()
143 .map(|s| {
144 s.bytes()
145 .enumerate()
146 .filter_map(|(i, c)| if c == b'\n' { Some(i + 1) } else { None })
149 .collect()
150 })
151 .unwrap_or_default()
152 })
153 }
154
155 pub fn source(&self) -> Option<&str> {
156 self.source.as_deref()
157 }
158}
159
160#[derive(Copy, Clone, Eq, PartialEq, Debug)]
161pub enum ByteFormat {
163 Utf8,
164 Utf16,
165}
166
167pub type SourceFile = Arc<SourceFileInner>;
168
169pub fn load_from_path(path: &Path) -> Result<String, Diagnostic> {
170 let string = (if path == Path::new("-") {
171 let mut buffer = Vec::new();
172 let r = std::io::stdin().read_to_end(&mut buffer);
173 r.and_then(|_| {
174 String::from_utf8(buffer)
175 .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))
176 })
177 } else {
178 std::fs::read_to_string(path)
179 })
180 .map_err(|err| Diagnostic {
181 message: format!("Could not load {}: {}", path.display(), err),
182 span: SourceLocation {
183 source_file: Some(SourceFileInner::from_path_only(path.to_owned())),
184 span: Default::default(),
185 },
186 level: DiagnosticLevel::Error,
187 })?;
188
189 if path.extension().is_some_and(|e| e == "rs") {
190 return crate::lexer::extract_rust_macro(string).ok_or_else(|| Diagnostic {
191 message: "No `slint!` macro".into(),
192 span: SourceLocation {
193 source_file: Some(SourceFileInner::from_path_only(path.to_owned())),
194 span: Default::default(),
195 },
196 level: DiagnosticLevel::Error,
197 });
198 }
199
200 Ok(string)
201}
202
203#[derive(Debug, Clone, Default)]
204pub struct SourceLocation {
205 pub source_file: Option<SourceFile>,
206 pub span: Span,
207}
208
209impl Spanned for SourceLocation {
210 fn span(&self) -> Span {
211 self.span.clone()
212 }
213
214 fn source_file(&self) -> Option<&SourceFile> {
215 self.source_file.as_ref()
216 }
217}
218
219impl Spanned for Option<SourceLocation> {
220 fn span(&self) -> crate::diagnostics::Span {
221 self.as_ref().map(|n| n.span()).unwrap_or_default()
222 }
223
224 fn source_file(&self) -> Option<&SourceFile> {
225 self.as_ref().map(|n| n.source_file.as_ref()).unwrap_or_default()
226 }
227}
228
229#[derive(Debug, PartialEq, Copy, Clone, Default)]
231#[non_exhaustive]
232pub enum DiagnosticLevel {
233 #[default]
235 Error,
236 Warning,
238 Note,
240}
241
242#[derive(Debug, Clone)]
247pub struct Diagnostic {
248 message: String,
249 span: SourceLocation,
250 level: DiagnosticLevel,
251}
252
253impl Diagnostic {
255 pub fn level(&self) -> DiagnosticLevel {
257 self.level
258 }
259
260 pub fn message(&self) -> &str {
262 &self.message
263 }
264
265 pub fn line_column(&self) -> (usize, usize) {
269 if !self.span.span.is_valid() {
270 return (0, 0);
271 }
272 let offset = self.span.span.offset;
273
274 match &self.span.source_file {
275 None => (0, 0),
276 Some(sl) => sl.line_column(offset, ByteFormat::Utf8),
277 }
278 }
279
280 pub fn length(&self) -> usize {
282 self.span.span.length
283 }
284
285 pub fn source_file(&self) -> Option<&Path> {
290 self.span.source_file().map(|sf| sf.path())
291 }
292}
293
294impl std::fmt::Display for Diagnostic {
295 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296 if let Some(sf) = self.span.source_file() {
297 let (line, _) = self.line_column();
298 write!(f, "{}:{}: {}", sf.path.display(), line, self.message)
299 } else {
300 write!(f, "{}", self.message)
301 }
302 }
303}
304
305impl std::fmt::Display for SourceLocation {
306 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307 if let Some(sf) = &self.source_file {
308 let (line, col) = sf.line_column(self.span.offset, ByteFormat::Utf8);
309 write!(f, "{}:{line}:{col}", sf.path.display())
310 } else {
311 write!(f, "<unknown>")
312 }
313 }
314}
315
316pub fn diagnostic_line_column_with_format(
317 diagnostic: &Diagnostic,
318 format: ByteFormat,
319) -> (usize, usize) {
320 let Some(sf) = &diagnostic.span.source_file else { return (0, 0) };
321 sf.line_column(diagnostic.span.span.offset, format)
322}
323
324pub fn diagnostic_end_line_column_with_format(
325 diagnostic: &Diagnostic,
326 format: ByteFormat,
327) -> (usize, usize) {
328 let Some(sf) = &diagnostic.span.source_file else { return (0, 0) };
329 let offset = diagnostic.span.span.offset + diagnostic.length();
334 sf.line_column(offset, format)
335}
336
337#[derive(Default)]
338pub struct BuildDiagnostics {
339 inner: Vec<Diagnostic>,
340
341 pub enable_experimental: bool,
343
344 #[cfg(feature = "slint-sc")]
346 pub slint_sc: bool,
347
348 pub all_loaded_files: BTreeSet<PathBuf>,
353}
354
355impl IntoIterator for BuildDiagnostics {
356 type Item = Diagnostic;
357 type IntoIter = <Vec<Diagnostic> as IntoIterator>::IntoIter;
358 fn into_iter(self) -> Self::IntoIter {
359 self.inner.into_iter()
360 }
361}
362
363impl BuildDiagnostics {
364 pub fn discarded() -> Self {
371 let mut diag = Self::default();
372 diag.push_error_with_span(
373 "Dummy error because some of the code asserts there was an error".into(),
374 Default::default(),
375 );
376 diag
377 }
378
379 pub fn push_diagnostic_with_span(
380 &mut self,
381 message: String,
382 span: SourceLocation,
383 level: DiagnosticLevel,
384 ) {
385 debug_assert!(
386 !message.as_str().ends_with('.'),
387 "Error message should not end with a period: ({message:?})"
388 );
389 self.inner.push(Diagnostic { message, span, level });
390 }
391 pub fn push_error_with_span(&mut self, message: String, span: SourceLocation) {
392 self.push_diagnostic_with_span(message, span, DiagnosticLevel::Error)
393 }
394 pub fn push_error(&mut self, message: String, source: &dyn Spanned) {
395 self.push_error_with_span(message, source.to_source_location());
396 }
397 pub fn push_warning_with_span(&mut self, message: String, span: SourceLocation) {
398 self.push_diagnostic_with_span(message, span, DiagnosticLevel::Warning)
399 }
400 pub fn push_warning(&mut self, message: String, source: &dyn Spanned) {
401 self.push_warning_with_span(message, source.to_source_location());
402 }
403 pub fn push_note_with_span(&mut self, message: String, span: SourceLocation) {
404 self.push_diagnostic_with_span(message, span, DiagnosticLevel::Note)
405 }
406 pub fn push_note(&mut self, message: String, source: &dyn Spanned) {
407 self.push_note_with_span(message, source.to_source_location());
408 }
409 pub fn push_compiler_error(&mut self, error: Diagnostic) {
410 self.inner.push(error);
411 }
412
413 pub fn is_slint_sc(&self) -> bool {
417 #[cfg(feature = "slint-sc")]
418 return self.slint_sc;
419 #[cfg(not(feature = "slint-sc"))]
420 false
421 }
422
423 #[cfg(feature = "slint-sc")]
429 pub fn slint_sc_error(&mut self, feature: &str, source: &dyn Spanned) {
430 if self.slint_sc
431 && !source
432 .source_file()
433 .is_some_and(|sf| sf.path().to_string_lossy().starts_with("builtin:"))
434 {
435 self.push_error(format!("{feature} not supported in Slint SC"), source);
436 }
437 }
438
439 pub fn push_property_deprecation_warning(
440 &mut self,
441 old_property: &str,
442 new_property: &str,
443 source: &dyn Spanned,
444 ) {
445 self.push_property_deprecation_warning_with_message(
446 old_property,
447 &format!("Please use '{new_property}' instead"),
448 source,
449 )
450 }
451
452 pub fn push_property_deprecation_warning_with_message(
455 &mut self,
456 old_property: &str,
457 message: &str,
458 source: &dyn Spanned,
459 ) {
460 self.push_diagnostic_with_span(
461 format!("The property '{old_property}' has been deprecated. {message}"),
462 source.to_source_location(),
463 crate::diagnostics::DiagnosticLevel::Warning,
464 )
465 }
466
467 pub fn has_errors(&self) -> bool {
469 self.inner.iter().any(|diag| diag.level == DiagnosticLevel::Error)
470 }
471
472 pub fn is_empty(&self) -> bool {
474 self.inner.is_empty()
475 }
476
477 #[cfg(feature = "display-diagnostics")]
478 fn call_diagnostics(
479 &self,
480 mut handle_no_source: Option<&mut dyn FnMut(&Diagnostic)>,
481 ) -> String {
482 if self.inner.is_empty() {
483 return Default::default();
484 }
485
486 let report: Vec<_> = self
487 .inner
488 .iter()
489 .filter_map(|d| {
490 let annotate_snippets_level = match d.level {
491 DiagnosticLevel::Error => annotate_snippets::Level::ERROR,
492 DiagnosticLevel::Warning => annotate_snippets::Level::WARNING,
493 DiagnosticLevel::Note => annotate_snippets::Level::NOTE,
494 };
495 let message = annotate_snippets_level.primary_title(d.message());
496
497 let group = if !d.span.span.is_valid() {
498 annotate_snippets::Group::with_title(message)
499 } else if let Some(sf) = &d.span.source_file {
500 if let Some(source) = &sf.source {
501 let start_offset = d.span.span.offset;
502 let end_offset = d.span.span.offset + d.length();
503 message.element(
504 annotate_snippets::Snippet::source(source)
505 .path(sf.path.to_string_lossy())
506 .annotation(
507 annotate_snippets::AnnotationKind::Primary
508 .span(start_offset..end_offset),
509 ),
510 )
511 } else {
512 if let Some(ref mut handle_no_source) = handle_no_source {
513 drop(message);
514 handle_no_source(d);
515 return None;
516 }
517 message.element(annotate_snippets::Origin::path(sf.path.to_string_lossy()))
518 }
519 } else {
520 annotate_snippets::Group::with_title(message)
521 };
522 Some(group)
523 })
524 .collect();
525
526 annotate_snippets::Renderer::styled().render(&report)
527 }
528
529 #[cfg(feature = "display-diagnostics")]
530 pub fn print(self) {
532 use std::io::Write;
533 let to_print = self.call_diagnostics(None);
534 if !to_print.is_empty() {
535 let _ = writeln!(std::io::stderr(), "{to_print}");
536 }
537 }
538
539 #[cfg(feature = "display-diagnostics")]
540 pub fn diagnostics_as_string(self) -> String {
542 self.call_diagnostics(None)
543 }
544
545 #[cfg(all(feature = "proc_macro_span", feature = "display-diagnostics"))]
546 pub fn report_macro_diagnostic(
550 self,
551 tokens: &[crate::parser::Token],
552 ) -> proc_macro::TokenStream {
553 let mut result = proc_macro::TokenStream::default();
554 let mut needs_error = self.has_errors();
555 let output = self.call_diagnostics(
556 Some(&mut |diag| {
557 let span = if diag.span.span.is_valid() {
560 let index = tokens
561 .binary_search_by_key(&diag.span.span.offset, |t| t.offset)
562 .unwrap_or_else(|i| i.saturating_sub(1));
563 tokens.get(index).and_then(|t| t.span)
564 } else {
565 None
566 };
567 let message = &diag.message;
568
569 let span: proc_macro2::Span = if let Some(span) = span {
570 span.into()
571 } else {
572 proc_macro2::Span::call_site()
573 };
574 match diag.level {
575 DiagnosticLevel::Error => {
576 needs_error = false;
577 result.extend(proc_macro::TokenStream::from(
578 quote::quote_spanned!(span => compile_error!{ #message })
579 ));
580 }
581 DiagnosticLevel::Warning => {
582 result.extend(proc_macro::TokenStream::from(
583 quote::quote_spanned!(span => const _ : () = { #[deprecated(note = #message)] const WARNING: () = (); WARNING };)
584 ));
585 },
586 DiagnosticLevel::Note => {
587 let message = format!("note: {message}");
590 result.extend(proc_macro::TokenStream::from(
591 quote::quote_spanned!(span => const _ : () = { #[deprecated(note = #message)] const NOTE: () = (); NOTE };)
592 ));
593 },
594 }
595 }),
596 );
597 if !output.is_empty() {
598 eprintln!("{output}");
599 }
600
601 if needs_error {
602 result.extend(proc_macro::TokenStream::from(quote::quote!(
603 compile_error! { "Error occurred" }
604 )))
605 }
606 result
607 }
608
609 pub fn to_string_vec(&self) -> Vec<String> {
610 self.inner.iter().map(|d| d.to_string()).collect()
611 }
612
613 pub fn push_diagnostic(
614 &mut self,
615 message: String,
616 source: &dyn Spanned,
617 level: DiagnosticLevel,
618 ) {
619 self.push_diagnostic_with_span(message, source.to_source_location(), level)
620 }
621
622 pub fn push_internal_error(&mut self, err: Diagnostic) {
623 self.inner.push(err)
624 }
625
626 pub fn iter(&self) -> impl Iterator<Item = &Diagnostic> {
627 self.inner.iter()
628 }
629
630 #[cfg(feature = "display-diagnostics")]
631 #[must_use]
632 pub fn check_and_exit_on_error(self) -> Self {
633 if self.has_errors() {
634 self.print();
635 std::process::exit(-1);
636 }
637 self
638 }
639
640 #[cfg(feature = "display-diagnostics")]
641 pub fn print_warnings_and_exit_on_error(self) {
642 let has_error = self.has_errors();
643 self.print();
644 if has_error {
645 std::process::exit(-1);
646 }
647 }
648}
649
650#[cfg(test)]
651mod tests {
652 use super::*;
653
654 #[test]
655 fn test_source_file_offset_line_column_mapping() {
656 let content = r#"import { LineEdit, Button, Slider, HorizontalBox, VerticalBox } from "std-widgets.slint";
657
658component MainWindow inherits Window {
659 property <duration> total-time: slider.value * 1s;
660
661 callback tick(duration);
662 VerticalBox {
663 HorizontalBox {
664 padding-left: 0;
665 Text { text: "Elapsed Time:"; }
666 Rectangle {
667 Rectangle {
668 height: 100%;
669 background: lightblue;
670 }
671 }
672 }
673 }
674
675
676}
677
678
679 "#.to_string();
680 let sf = SourceFileInner::new(PathBuf::from("foo.slint"), content.clone());
681
682 let mut line = 1;
683 let mut column = 1;
684 for offset in 0..content.len() {
685 let b = *content.as_bytes().get(offset).unwrap();
686
687 assert_eq!(sf.offset(line, column, ByteFormat::Utf8), offset);
688 assert_eq!(sf.line_column(offset, ByteFormat::Utf8), (line, column));
689
690 if b == b'\n' {
691 line += 1;
692 column = 1;
693 } else {
694 column += 1;
695 }
696 }
697 }
698}