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