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 #[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)] 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
57pub 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 source: Option<String>,
72
73 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 pub fn from_path_only(path: PathBuf) -> Rc<Self> {
94 Rc::new(Self { path, ..Default::default() })
95 }
96
97 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 pub fn offset(&self, line: usize, column: usize) -> usize {
123 let col_offset = column.saturating_sub(1);
124 if line <= 1 {
125 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 .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#[derive(Debug, PartialEq, Copy, Clone, Default)]
218#[non_exhaustive]
219pub enum DiagnosticLevel {
220 #[default]
222 Error,
223 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#[derive(Debug, Clone)]
242pub struct Diagnostic {
243 message: String,
244 span: SourceLocation,
245 level: DiagnosticLevel,
246}
247
248impl Diagnostic {
250 pub fn level(&self) -> DiagnosticLevel {
252 self.level
253 }
254
255 pub fn message(&self) -> &str {
257 &self.message
258 }
259
260 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 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 pub enable_experimental: bool,
298
299 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 pub fn has_errors(&self) -> bool {
360 self.inner.iter().any(|diag| diag.level == DiagnosticLevel::Error)
361 }
362
363 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 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 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 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 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}