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::Utf8 {
103 col
104 } else {
105 let Some(source) = &self.source else { return col };
106 source[line_begin..][..col].encode_utf16().count()
107 }
108 };
109
110 let line_offsets = self.line_offsets();
111 line_offsets.binary_search(&offset).map_or_else(
112 |line| {
113 if line == 0 {
114 (1, adjust_utf16(0, offset) + 1)
115 } else {
116 let line_begin = *line_offsets.get(line - 1).unwrap_or(&0);
117 (line + 1, adjust_utf16(line_begin, offset - line_begin) + 1)
118 }
119 },
120 |line| (line + 2, 1),
121 )
122 }
123
124 pub fn text_size_to_file_line_column(
125 &self,
126 size: TextSize,
127 format: ByteFormat,
128 ) -> (String, usize, usize, usize, usize) {
129 let file_name = self.path().to_string_lossy().to_string();
130 let (start_line, start_column) = self.line_column(size.into(), format);
131 (file_name, start_line, start_column, start_line, start_column)
132 }
133
134 pub fn offset(&self, line: usize, column: usize, format: ByteFormat) -> usize {
136 let adjust_utf16 = |line_begin, col| {
137 if format == ByteFormat::Utf8 {
138 col
139 } else {
140 let Some(source) = &self.source else { return col };
141 let mut utf16_counter = 0;
142 for (utf8_index, c) in source[line_begin..].char_indices() {
143 if utf16_counter >= col {
144 return utf8_index;
145 }
146 utf16_counter += c.len_utf16();
147 }
148 col
149 }
150 };
151
152 let col_offset = column.saturating_sub(1);
153 if line <= 1 {
154 return adjust_utf16(0, col_offset);
156 }
157 let offsets = self.line_offsets();
158 let index = std::cmp::min(line.saturating_sub(1), offsets.len());
159 let line_offset = *offsets.get(index.saturating_sub(1)).unwrap_or(&0);
160 line_offset.saturating_add(adjust_utf16(line_offset, col_offset))
161 }
162
163 fn line_offsets(&self) -> &[usize] {
164 self.line_offsets.get_or_init(|| {
165 self.source
166 .as_ref()
167 .map(|s| {
168 s.bytes()
169 .enumerate()
170 .filter_map(|(i, c)| if c == b'\n' { Some(i + 1) } else { None })
173 .collect()
174 })
175 .unwrap_or_default()
176 })
177 }
178
179 pub fn source(&self) -> Option<&str> {
180 self.source.as_deref()
181 }
182}
183
184#[derive(Copy, Clone, Eq, PartialEq, Debug)]
185pub enum ByteFormat {
187 Utf8,
188 Utf16,
189}
190
191pub type SourceFile = Rc<SourceFileInner>;
192
193pub fn load_from_path(path: &Path) -> Result<String, Diagnostic> {
194 let string = (if path == Path::new("-") {
195 let mut buffer = Vec::new();
196 let r = std::io::stdin().read_to_end(&mut buffer);
197 r.and_then(|_| {
198 String::from_utf8(buffer)
199 .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))
200 })
201 } else {
202 std::fs::read_to_string(path)
203 })
204 .map_err(|err| Diagnostic {
205 message: format!("Could not load {}: {}", path.display(), err),
206 span: SourceLocation {
207 source_file: Some(SourceFileInner::from_path_only(path.to_owned())),
208 span: Default::default(),
209 },
210 level: DiagnosticLevel::Error,
211 })?;
212
213 if path.extension().is_some_and(|e| e == "rs") {
214 return crate::lexer::extract_rust_macro(string).ok_or_else(|| Diagnostic {
215 message: "No `slint!` macro".into(),
216 span: SourceLocation {
217 source_file: Some(SourceFileInner::from_path_only(path.to_owned())),
218 span: Default::default(),
219 },
220 level: DiagnosticLevel::Error,
221 });
222 }
223
224 Ok(string)
225}
226
227#[derive(Debug, Clone, Default)]
228pub struct SourceLocation {
229 pub source_file: Option<SourceFile>,
230 pub span: Span,
231}
232
233impl Spanned for SourceLocation {
234 fn span(&self) -> Span {
235 self.span.clone()
236 }
237
238 fn source_file(&self) -> Option<&SourceFile> {
239 self.source_file.as_ref()
240 }
241}
242
243impl Spanned for Option<SourceLocation> {
244 fn span(&self) -> crate::diagnostics::Span {
245 self.as_ref().map(|n| n.span()).unwrap_or_default()
246 }
247
248 fn source_file(&self) -> Option<&SourceFile> {
249 self.as_ref().map(|n| n.source_file.as_ref()).unwrap_or_default()
250 }
251}
252
253#[derive(Debug, PartialEq, Copy, Clone, Default)]
255#[non_exhaustive]
256pub enum DiagnosticLevel {
257 #[default]
259 Error,
260 Warning,
262 Note,
264}
265
266#[derive(Debug, Clone)]
271pub struct Diagnostic {
272 message: String,
273 span: SourceLocation,
274 level: DiagnosticLevel,
275}
276
277impl Diagnostic {
279 pub fn level(&self) -> DiagnosticLevel {
281 self.level
282 }
283
284 pub fn message(&self) -> &str {
286 &self.message
287 }
288
289 pub fn line_column(&self) -> (usize, usize) {
293 if !self.span.span.is_valid() {
294 return (0, 0);
295 }
296 let offset = self.span.span.offset;
297
298 match &self.span.source_file {
299 None => (0, 0),
300 Some(sl) => sl.line_column(offset, ByteFormat::Utf8),
301 }
302 }
303
304 pub fn length(&self) -> usize {
306 self.span.span.length
307 }
308
309 pub fn source_file(&self) -> Option<&Path> {
314 self.span.source_file().map(|sf| sf.path())
315 }
316}
317
318impl std::fmt::Display for Diagnostic {
319 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320 if let Some(sf) = self.span.source_file() {
321 let (line, _) = self.line_column();
322 write!(f, "{}:{}: {}", sf.path.display(), line, self.message)
323 } else {
324 write!(f, "{}", self.message)
325 }
326 }
327}
328
329impl std::fmt::Display for SourceLocation {
330 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
331 if let Some(sf) = &self.source_file {
332 let (line, col) = sf.line_column(self.span.offset, ByteFormat::Utf8);
333 write!(f, "{}:{line}:{col}", sf.path.display())
334 } else {
335 write!(f, "<unknown>")
336 }
337 }
338}
339
340pub fn diagnostic_line_column_with_format(
341 diagnostic: &Diagnostic,
342 format: ByteFormat,
343) -> (usize, usize) {
344 let Some(sf) = &diagnostic.span.source_file else { return (0, 0) };
345 sf.line_column(diagnostic.span.span.offset, format)
346}
347
348pub fn diagnostic_end_line_column_with_format(
349 diagnostic: &Diagnostic,
350 format: ByteFormat,
351) -> (usize, usize) {
352 let Some(sf) = &diagnostic.span.source_file else { return (0, 0) };
353 let offset = diagnostic.span.span.offset + diagnostic.length();
358 sf.line_column(offset, format)
359}
360
361#[derive(Default)]
362pub struct BuildDiagnostics {
363 inner: Vec<Diagnostic>,
364
365 pub enable_experimental: bool,
367
368 pub all_loaded_files: BTreeSet<PathBuf>,
373}
374
375impl IntoIterator for BuildDiagnostics {
376 type Item = Diagnostic;
377 type IntoIter = <Vec<Diagnostic> as IntoIterator>::IntoIter;
378 fn into_iter(self) -> Self::IntoIter {
379 self.inner.into_iter()
380 }
381}
382
383impl BuildDiagnostics {
384 pub fn push_diagnostic_with_span(
385 &mut self,
386 message: String,
387 span: SourceLocation,
388 level: DiagnosticLevel,
389 ) {
390 debug_assert!(
391 !message.as_str().ends_with('.'),
392 "Error message should not end with a period: ({message:?})"
393 );
394 self.inner.push(Diagnostic { message, span, level });
395 }
396 pub fn push_error_with_span(&mut self, message: String, span: SourceLocation) {
397 self.push_diagnostic_with_span(message, span, DiagnosticLevel::Error)
398 }
399 pub fn push_error(&mut self, message: String, source: &dyn Spanned) {
400 self.push_error_with_span(message, source.to_source_location());
401 }
402 pub fn push_warning_with_span(&mut self, message: String, span: SourceLocation) {
403 self.push_diagnostic_with_span(message, span, DiagnosticLevel::Warning)
404 }
405 pub fn push_warning(&mut self, message: String, source: &dyn Spanned) {
406 self.push_warning_with_span(message, source.to_source_location());
407 }
408 pub fn push_note_with_span(&mut self, message: String, span: SourceLocation) {
409 self.push_diagnostic_with_span(message, span, DiagnosticLevel::Note)
410 }
411 pub fn push_note(&mut self, message: String, source: &dyn Spanned) {
412 self.push_note_with_span(message, source.to_source_location());
413 }
414 pub fn push_compiler_error(&mut self, error: Diagnostic) {
415 self.inner.push(error);
416 }
417
418 pub fn push_property_deprecation_warning(
419 &mut self,
420 old_property: &str,
421 new_property: &str,
422 source: &dyn Spanned,
423 ) {
424 self.push_diagnostic_with_span(
425 format!(
426 "The property '{old_property}' has been deprecated. Please use '{new_property}' instead"
427 ),
428 source.to_source_location(),
429 crate::diagnostics::DiagnosticLevel::Warning,
430 )
431 }
432
433 pub fn has_errors(&self) -> bool {
435 self.inner.iter().any(|diag| diag.level == DiagnosticLevel::Error)
436 }
437
438 pub fn is_empty(&self) -> bool {
440 self.inner.is_empty()
441 }
442
443 #[cfg(feature = "display-diagnostics")]
444 fn call_diagnostics(
445 &self,
446 mut handle_no_source: Option<&mut dyn FnMut(&Diagnostic)>,
447 ) -> String {
448 if self.inner.is_empty() {
449 return Default::default();
450 }
451
452 let report: Vec<_> = self
453 .inner
454 .iter()
455 .filter_map(|d| {
456 let annotate_snippets_level = match d.level {
457 DiagnosticLevel::Error => annotate_snippets::Level::ERROR,
458 DiagnosticLevel::Warning => annotate_snippets::Level::WARNING,
459 DiagnosticLevel::Note => annotate_snippets::Level::NOTE,
460 };
461 let message = annotate_snippets_level.primary_title(d.message());
462
463 let group = if !d.span.span.is_valid() {
464 annotate_snippets::Group::with_title(message)
465 } else if let Some(sf) = &d.span.source_file {
466 if let Some(source) = &sf.source {
467 let start_offset = d.span.span.offset;
468 let end_offset = d.span.span.offset + d.length();
469 message.element(
470 annotate_snippets::Snippet::source(source)
471 .path(sf.path.to_string_lossy())
472 .annotation(
473 annotate_snippets::AnnotationKind::Primary
474 .span(start_offset..end_offset),
475 ),
476 )
477 } else {
478 if let Some(ref mut handle_no_source) = handle_no_source {
479 drop(message);
480 handle_no_source(d);
481 return None;
482 }
483 message.element(annotate_snippets::Origin::path(sf.path.to_string_lossy()))
484 }
485 } else {
486 annotate_snippets::Group::with_title(message)
487 };
488 Some(group)
489 })
490 .collect();
491
492 annotate_snippets::Renderer::styled().render(&report)
493 }
494
495 #[cfg(feature = "display-diagnostics")]
496 pub fn print(self) {
498 let to_print = self.call_diagnostics(None);
499 if !to_print.is_empty() {
500 std::eprintln!("{to_print}");
501 }
502 }
503
504 #[cfg(feature = "display-diagnostics")]
505 pub fn diagnostics_as_string(self) -> String {
507 self.call_diagnostics(None)
508 }
509
510 #[cfg(all(feature = "proc_macro_span", feature = "display-diagnostics"))]
511 pub fn report_macro_diagnostic(
513 self,
514 span_map: &[crate::parser::Token],
515 ) -> proc_macro::TokenStream {
516 let mut result = proc_macro::TokenStream::default();
517 let mut needs_error = self.has_errors();
518 let output = self.call_diagnostics(
519 Some(&mut |diag| {
520 let span = diag.span.span.span.or_else(|| {
521 let mut offset = 0;
525 span_map.iter().find_map(|t| {
526 if diag.span.span.offset <= offset {
527 t.span
528 } else {
529 offset += t.text.len();
530 None
531 }
532 })
533 });
534 let message = &diag.message;
535
536 let span: proc_macro2::Span = if let Some(span) = span {
537 span.into()
538 } else {
539 proc_macro2::Span::call_site()
540 };
541 match diag.level {
542 DiagnosticLevel::Error => {
543 needs_error = false;
544 result.extend(proc_macro::TokenStream::from(
545 quote::quote_spanned!(span => compile_error!{ #message })
546 ));
547 }
548 DiagnosticLevel::Warning => {
549 result.extend(proc_macro::TokenStream::from(
550 quote::quote_spanned!(span => const _ : () = { #[deprecated(note = #message)] const WARNING: () = (); WARNING };)
551 ));
552 },
553 DiagnosticLevel::Note => {
554 let message = format!("note: {message}");
557 result.extend(proc_macro::TokenStream::from(
558 quote::quote_spanned!(span => const _ : () = { #[deprecated(note = #message)] const NOTE: () = (); NOTE };)
559 ));
560 },
561 }
562 }),
563 );
564 if !output.is_empty() {
565 eprintln!("{output}");
566 }
567
568 if needs_error {
569 result.extend(proc_macro::TokenStream::from(quote::quote!(
570 compile_error! { "Error occurred" }
571 )))
572 }
573 result
574 }
575
576 pub fn to_string_vec(&self) -> Vec<String> {
577 self.inner.iter().map(|d| d.to_string()).collect()
578 }
579
580 pub fn push_diagnostic(
581 &mut self,
582 message: String,
583 source: &dyn Spanned,
584 level: DiagnosticLevel,
585 ) {
586 self.push_diagnostic_with_span(message, source.to_source_location(), level)
587 }
588
589 pub fn push_internal_error(&mut self, err: Diagnostic) {
590 self.inner.push(err)
591 }
592
593 pub fn iter(&self) -> impl Iterator<Item = &Diagnostic> {
594 self.inner.iter()
595 }
596
597 #[cfg(feature = "display-diagnostics")]
598 #[must_use]
599 pub fn check_and_exit_on_error(self) -> Self {
600 if self.has_errors() {
601 self.print();
602 std::process::exit(-1);
603 }
604 self
605 }
606
607 #[cfg(feature = "display-diagnostics")]
608 pub fn print_warnings_and_exit_on_error(self) {
609 let has_error = self.has_errors();
610 self.print();
611 if has_error {
612 std::process::exit(-1);
613 }
614 }
615}
616
617#[cfg(test)]
618mod tests {
619 use super::*;
620
621 #[test]
622 fn test_source_file_offset_line_column_mapping() {
623 let content = r#"import { LineEdit, Button, Slider, HorizontalBox, VerticalBox } from "std-widgets.slint";
624
625component MainWindow inherits Window {
626 property <duration> total-time: slider.value * 1s;
627
628 callback tick(duration);
629 VerticalBox {
630 HorizontalBox {
631 padding-left: 0;
632 Text { text: "Elapsed Time:"; }
633 Rectangle {
634 Rectangle {
635 height: 100%;
636 background: lightblue;
637 }
638 }
639 }
640 }
641
642
643}
644
645
646 "#.to_string();
647 let sf = SourceFileInner::new(PathBuf::from("foo.slint"), content.clone());
648
649 let mut line = 1;
650 let mut column = 1;
651 for offset in 0..content.len() {
652 let b = *content.as_bytes().get(offset).unwrap();
653
654 assert_eq!(sf.offset(line, column, ByteFormat::Utf8), offset);
655 assert_eq!(sf.line_column(offset, ByteFormat::Utf8), (line, column));
656
657 if b == b'\n' {
658 line += 1;
659 column = 1;
660 } else {
661 column += 1;
662 }
663 }
664 }
665}