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 | |
4 | use std::io::Read; |
5 | use std::path::{Path, PathBuf}; |
6 | use std::rc::Rc; |
7 | |
8 | use 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)] |
16 | pub struct Span { |
17 | pub offset: usize, |
18 | #[cfg (feature = "proc_macro_span" )] |
19 | pub span: Option<proc_macro::Span>, |
20 | } |
21 | |
22 | impl 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 | |
33 | impl 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 | |
43 | impl PartialEq for Span { |
44 | fn eq(&self, other: &Span) -> bool { |
45 | self.offset == other.offset |
46 | } |
47 | } |
48 | |
49 | #[cfg (feature = "proc_macro_span" )] |
50 | impl 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 |
57 | pub 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 | |
65 | pub type SourceFileVersion = Option<i32>; |
66 | |
67 | #[derive (Default)] |
68 | pub 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 | |
81 | impl 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 | |
88 | impl 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 | |
163 | pub type SourceFile = Rc<SourceFileInner>; |
164 | |
165 | pub 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)] |
200 | pub struct SourceLocation { |
201 | pub source_file: Option<SourceFile>, |
202 | pub span: Span, |
203 | } |
204 | |
205 | impl 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 | |
215 | impl 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 ] |
228 | pub 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" )] |
237 | impl 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)] |
251 | pub 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 |
258 | impl 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 | |
290 | impl 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)] |
302 | pub 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 | |
312 | impl 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 | |
320 | impl 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)] |
561 | mod 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 | |
568 | component 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 | |