1use std::io::Read;
5use std::path::{Path, PathBuf};
6use std::rc::Rc;
7
8#[derive(Debug, Clone)]
14pub struct Span {
15 pub offset: usize,
16 #[cfg(feature = "proc_macro_span")]
17 pub span: Option<proc_macro::Span>,
18}
19
20impl Span {
21 pub fn is_valid(&self) -> bool {
22 self.offset != usize::MAX
23 }
24
25 #[allow(clippy::needless_update)] pub fn new(offset: usize) -> Self {
27 Self { offset, ..Default::default() }
28 }
29}
30
31impl Default for Span {
32 fn default() -> Self {
33 Span {
34 offset: usize::MAX,
35 #[cfg(feature = "proc_macro_span")]
36 span: Default::default(),
37 }
38 }
39}
40
41impl PartialEq for Span {
42 fn eq(&self, other: &Span) -> bool {
43 self.offset == other.offset
44 }
45}
46
47#[cfg(feature = "proc_macro_span")]
48impl From<proc_macro::Span> for Span {
49 fn from(span: proc_macro::Span) -> Self {
50 Self { span: Some(span), ..Default::default() }
51 }
52}
53
54pub trait Spanned {
56 fn span(&self) -> Span;
57 fn source_file(&self) -> Option<&SourceFile>;
58 fn to_source_location(&self) -> SourceLocation {
59 SourceLocation { source_file: self.source_file().cloned(), span: self.span() }
60 }
61}
62
63#[derive(Default)]
64pub struct SourceFileInner {
65 path: PathBuf,
66
67 source: Option<String>,
69
70 line_offsets: once_cell::unsync::OnceCell<Vec<usize>>,
72}
73
74impl std::fmt::Debug for SourceFileInner {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 write!(f, "{:?}", self.path)
77 }
78}
79
80impl SourceFileInner {
81 pub fn new(path: PathBuf, source: String) -> Self {
82 Self { path, source: Some(source), line_offsets: Default::default() }
83 }
84
85 pub fn path(&self) -> &Path {
86 &self.path
87 }
88
89 pub fn from_path_only(path: PathBuf) -> Rc<Self> {
91 Rc::new(Self { path, ..Default::default() })
92 }
93
94 fn line_offsets(&self) -> &[usize] {
95 self.line_offsets.get_or_init(|| {
96 self.source
97 .as_ref()
98 .map(|s| {
99 s.bytes()
100 .enumerate()
101 .filter_map(|(i, c)| if c == b'\n' { Some(i) } else { None })
102 .collect()
103 })
104 .unwrap_or_default()
105 })
106 }
107}
108
109pub type SourceFile = Rc<SourceFileInner>;
110
111pub fn load_from_path(path: &Path) -> Result<String, Diagnostic> {
112 (if path == Path::new("-") {
113 let mut buffer = Vec::new();
114 let r = std::io::stdin().read_to_end(&mut buffer);
115 r.and_then(|_| {
116 String::from_utf8(buffer)
117 .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))
118 })
119 } else {
120 std::fs::read_to_string(path)
121 })
122 .map_err(|err| Diagnostic {
123 message: format!("Could not load {}: {}", path.display(), err),
124 span: SourceLocation {
125 source_file: Some(SourceFileInner::from_path_only(path.to_owned())),
126 span: Default::default(),
127 },
128 level: DiagnosticLevel::Error,
129 })
130}
131
132#[derive(Debug, Clone, Default)]
133pub struct SourceLocation {
134 pub source_file: Option<SourceFile>,
135 pub span: Span,
136}
137
138impl Spanned for SourceLocation {
139 fn span(&self) -> Span {
140 self.span.clone()
141 }
142
143 fn source_file(&self) -> Option<&SourceFile> {
144 self.source_file.as_ref()
145 }
146}
147
148impl Spanned for Option<SourceLocation> {
149 fn span(&self) -> crate::diagnostics::Span {
150 self.as_ref().map(|n| n.span()).unwrap_or_default()
151 }
152
153 fn source_file(&self) -> Option<&SourceFile> {
154 self.as_ref().map(|n| n.source_file.as_ref()).unwrap_or_default()
155 }
156}
157
158#[derive(Debug, PartialEq, Copy, Clone)]
160pub enum DiagnosticLevel {
161 Error,
162 Warning,
163}
164
165impl Default for DiagnosticLevel {
166 fn default() -> Self {
167 Self::Error
168 }
169}
170
171#[cfg(feature = "display-diagnostics")]
172impl From<DiagnosticLevel> for codemap_diagnostic::Level {
173 fn from(l: DiagnosticLevel) -> Self {
174 match l {
175 DiagnosticLevel::Error => codemap_diagnostic::Level::Error,
176 DiagnosticLevel::Warning => codemap_diagnostic::Level::Warning,
177 }
178 }
179}
180
181#[derive(Debug, Clone)]
186pub struct Diagnostic {
187 message: String,
188 span: SourceLocation,
189 level: DiagnosticLevel,
190}
191
192impl Diagnostic {
193 pub fn level(&self) -> DiagnosticLevel {
195 self.level
196 }
197
198 pub fn message(&self) -> &str {
200 &self.message
201 }
202
203 pub fn line_column(&self) -> (usize, usize) {
205 let offset = self.span.span.offset;
206 let line_offsets = match &self.span.source_file {
207 None => return (0, 0),
208 Some(sl) => sl.line_offsets(),
209 };
210 line_offsets.binary_search(&offset).map_or_else(
211 |line| {
212 if line == 0 {
213 (line + 1, offset)
214 } else {
215 (line + 1, line_offsets.get(line - 1).map_or(0, |x| offset - x))
216 }
217 },
218 |line| (line + 1, 0),
219 )
220 }
221
222 pub fn source_file(&self) -> Option<&Path> {
224 self.span.source_file().map(|sf| sf.path())
225 }
226}
227
228impl std::fmt::Display for Diagnostic {
229 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230 if let Some(sf) = self.span.source_file() {
231 let (line, _) = self.line_column();
232 write!(f, "{}:{}: {}", sf.path.display(), line, self.message)
233 } else {
234 write!(f, "{}", self.message)
235 }
236 }
237}
238
239#[derive(Default)]
240pub struct BuildDiagnostics {
241 inner: Vec<Diagnostic>,
242
243 pub all_loaded_files: Vec<PathBuf>,
248}
249
250impl IntoIterator for BuildDiagnostics {
251 type Item = Diagnostic;
252 type IntoIter = <Vec<Diagnostic> as IntoIterator>::IntoIter;
253 fn into_iter(self) -> Self::IntoIter {
254 self.inner.into_iter()
255 }
256}
257
258impl BuildDiagnostics {
259 pub fn push_diagnostic_with_span(
260 &mut self,
261 message: String,
262 span: SourceLocation,
263 level: DiagnosticLevel,
264 ) {
265 debug_assert!(
266 !message.as_str().ends_with('.'),
267 "Error message should not end with a period: ({:?})",
268 message
269 );
270 self.inner.push(Diagnostic { message, span, level });
271 }
272 pub fn push_error_with_span(&mut self, message: String, span: SourceLocation) {
273 self.push_diagnostic_with_span(message, span, DiagnosticLevel::Error)
274 }
275 pub fn push_error(&mut self, message: String, source: &dyn Spanned) {
276 self.push_error_with_span(message, source.to_source_location());
277 }
278 pub fn push_warning_with_span(&mut self, message: String, span: SourceLocation) {
279 self.push_diagnostic_with_span(message, span, DiagnosticLevel::Warning)
280 }
281 pub fn push_warning(&mut self, message: String, source: &dyn Spanned) {
282 self.push_warning_with_span(message, source.to_source_location());
283 }
284 pub fn push_compiler_error(&mut self, error: Diagnostic) {
285 self.inner.push(error);
286 }
287
288 pub fn push_property_deprecation_warning(
289 &mut self,
290 old_property: &str,
291 new_property: &str,
292 source: &dyn Spanned,
293 ) {
294 self.push_diagnostic_with_span(
295 format!(
296 "The property '{}' has been deprecated. Please use '{}' instead",
297 old_property, new_property
298 ),
299 source.to_source_location(),
300 crate::diagnostics::DiagnosticLevel::Warning,
301 )
302 }
303
304 pub fn has_error(&self) -> bool {
306 self.inner.iter().any(|diag| diag.level == DiagnosticLevel::Error)
307 }
308
309 pub fn is_empty(&self) -> bool {
311 self.inner.is_empty()
312 }
313
314 #[cfg(feature = "display-diagnostics")]
315 fn call_diagnostics<Output>(
316 self,
317 output: &mut Output,
318 mut handle_no_source: Option<&mut dyn FnMut(Diagnostic)>,
319 emitter_factory: impl for<'b> FnOnce(
320 &'b mut Output,
321 Option<&'b codemap::CodeMap>,
322 ) -> codemap_diagnostic::Emitter<'b>,
323 ) {
324 if self.inner.is_empty() {
325 return;
326 }
327
328 let mut codemap = codemap::CodeMap::new();
329 let mut codemap_files = std::collections::HashMap::new();
330
331 let diags: Vec<_> = self
332 .inner
333 .into_iter()
334 .filter_map(|d| {
335 let spans = if !d.span.span.is_valid() {
336 vec![]
337 } else if let Some(sf) = &d.span.source_file {
338 if let Some(ref mut handle_no_source) = handle_no_source {
339 if sf.source.is_none() {
340 handle_no_source(d);
341 return None;
342 }
343 }
344 let path: String = sf.path.to_string_lossy().into();
345 let file = codemap_files.entry(path).or_insert_with(|| {
346 codemap.add_file(
347 sf.path.to_string_lossy().into(),
348 sf.source.clone().unwrap_or_default(),
349 )
350 });
351 let file_span = file.span;
352 let s = codemap_diagnostic::SpanLabel {
353 span: file_span
354 .subspan(d.span.span.offset as u64, d.span.span.offset as u64),
355 style: codemap_diagnostic::SpanStyle::Primary,
356 label: None,
357 };
358 vec![s]
359 } else {
360 vec![]
361 };
362 Some(codemap_diagnostic::Diagnostic {
363 level: d.level.into(),
364 message: d.message,
365 code: None,
366 spans,
367 })
368 })
369 .collect();
370
371 let mut emitter = emitter_factory(output, Some(&codemap));
372 emitter.emit(&diags);
373 }
374
375 #[cfg(feature = "display-diagnostics")]
376 pub fn print(self) {
378 self.call_diagnostics(&mut (), None, |_, codemap| {
379 codemap_diagnostic::Emitter::stderr(codemap_diagnostic::ColorConfig::Always, codemap)
380 });
381 }
382
383 #[cfg(feature = "display-diagnostics")]
384 pub fn diagnostics_as_string(self) -> String {
386 let mut output = Vec::new();
387 self.call_diagnostics(&mut output, None, |output, codemap| {
388 codemap_diagnostic::Emitter::vec(output, codemap)
389 });
390
391 String::from_utf8(output).expect(
392 "Internal error: There were errors during compilation but they did not result in valid utf-8 diagnostics!"
393 )
394 }
395
396 #[cfg(all(feature = "proc_macro_span", feature = "display-diagnostics"))]
397 pub fn report_macro_diagnostic(
399 self,
400 span_map: &[crate::parser::Token],
401 ) -> proc_macro::TokenStream {
402 let mut result = proc_macro::TokenStream::default();
403 let mut needs_error = self.has_error();
404 self.call_diagnostics(
405 &mut (),
406 Some(&mut |diag| {
407 let span = diag.span.span.span.or_else(|| {
408 let mut offset = 0;
412 span_map.iter().find_map(|t| {
413 if diag.span.span.offset <= offset {
414 t.span
415 } else {
416 offset += t.text.len();
417 None
418 }
419 })
420 });
421 let message = &diag.message;
422 match diag.level {
423 DiagnosticLevel::Error => {
424 needs_error = false;
425 result.extend(proc_macro::TokenStream::from(if let Some(span) = span {
426 quote::quote_spanned!(span.into() => compile_error!{ #message })
427 } else {
428 quote::quote!(compile_error! { #message })
429 }));
430 }
431 DiagnosticLevel::Warning => (),
433 }
434 }),
435 |_, codemap| {
436 codemap_diagnostic::Emitter::stderr(
437 codemap_diagnostic::ColorConfig::Always,
438 codemap,
439 )
440 },
441 );
442 if needs_error {
443 result.extend(proc_macro::TokenStream::from(quote::quote!(
444 compile_error! { "Error occurred" }
445 )))
446 }
447 result
448 }
449
450 pub fn to_string_vec(&self) -> Vec<String> {
451 self.inner.iter().map(|d| d.to_string()).collect()
452 }
453
454 pub fn push_diagnostic(
455 &mut self,
456 message: String,
457 source: &dyn Spanned,
458 level: DiagnosticLevel,
459 ) {
460 self.push_diagnostic_with_span(message, source.to_source_location(), level)
461 }
462
463 pub fn push_internal_error(&mut self, err: Diagnostic) {
464 self.inner.push(err)
465 }
466
467 pub fn iter(&self) -> impl Iterator<Item = &Diagnostic> {
468 self.inner.iter()
469 }
470
471 #[cfg(feature = "display-diagnostics")]
472 #[must_use]
473 pub fn check_and_exit_on_error(self) -> Self {
474 if self.has_error() {
475 self.print();
476 std::process::exit(-1);
477 }
478 self
479 }
480
481 #[cfg(feature = "display-diagnostics")]
482 pub fn print_warnings_and_exit_on_error(self) {
483 let has_error = self.has_error();
484 self.print();
485 if has_error {
486 std::process::exit(-1);
487 }
488 }
489}