1pub use compose_codespan_reporting;
2use compose_codespan_reporting::term::termcolor::WriteColor;
3use compose_codespan_reporting::{diagnostic, term};
4use compose_syntax::{
5 FileId, Fix, FixDisplay, Label, LabelType, PatchEngine, Span, SyntaxError, SyntaxErrorSeverity,
6};
7use ecow::{EcoVec, eco_vec};
8use std::fmt::{Display, Formatter};
9use std::ops::{Deref, DerefMut};
10use std::path::{Path, PathBuf};
11use std::str::Utf8Error;
12use std::string::FromUtf8Error;
13use std::{fmt, io};
14
15pub fn write_diagnostics(
16 world: &dyn World,
17 errors: &[SourceDiagnostic],
18 warnings: &[SourceDiagnostic],
19 writer: &mut dyn WriteColor,
20 config: &term::Config,
21) -> Result<(), compose_codespan_reporting::files::Error> {
22 for diag in warnings.iter().chain(errors) {
23 let mut diagnostic = match diag.severity {
24 Severity::Error => diagnostic::Diagnostic::error(),
25 Severity::Warning => diagnostic::Diagnostic::warning(),
26 }
27 .with_message(diag.message.clone())
28 .with_labels_iter(
29 diag_label(diag)
30 .into_iter()
31 .chain(diag.labels.iter().flat_map(create_label)),
32 );
33
34 for fix in &diag.fixes {
35 let Some(file_id) = fix.span.id() else {
36 continue;
37 };
38 let mut engine = PatchEngine::new();
39 let Ok(source) = world.source(file_id) else {
40 continue;
41 };
42 let Some(snippet) = source.span_text(fix.span) else {
43 continue;
44 };
45 let Some(offset) = fix.span.range().map(|r| r.start) else {
46 continue;
47 };
48
49 engine.add_patches(fix.patches.iter().cloned());
50 let Ok(patched) = engine.apply_all_with_offset(snippet, offset) else {
51 continue;
52 };
53
54 let message = format!(
55 "suggested fix: {}:\n`{}`",
56 fix.message.trim_end_matches(':'),
57 patched
58 );
59 match fix.display {
60 FixDisplay::Inline { span } => diagnostic
61 .labels
62 .extend(create_label(&Label::secondary(span, message))),
63 FixDisplay::Footer => {
64 diagnostic.notes.push(message);
65 }
66 }
67 }
68
69 diagnostic
70 .notes
71 .extend(diag.hints.iter().map(|h| format!("help: {h}")));
72 diagnostic
73 .notes
74 .extend(diag.notes.iter().map(|n| format!("note: {n}")));
75
76 if let Some(code) = &diag.code {
77 diagnostic = diagnostic.with_code(code.code).with_note(eco_format!(
78 "help: for more information about this error, try `compose explain {}`",
79 code.code
80 ))
81 }
82
83 term::emit(writer, config, &world, &diagnostic)?;
84
85 for point in &diag.trace {
86 let message = point.value.to_string();
87 let help = diagnostic::Diagnostic::help()
88 .with_message(message)
89 .with_label(diagnostic::Label::primary(
90 point.span.id().unwrap(),
91 point.span.range().unwrap(),
92 ));
93
94 term::emit(writer, &config, &world, &help)?;
95 }
96 }
97
98 Ok(())
99}
100
101fn diag_label(diag: &SourceDiagnostic) -> Option<diagnostic::Label<FileId>> {
102 let id = diag.span.id()?;
103 let range = diag.span.range()?;
104
105 let mut label = diagnostic::Label::primary(id, range);
106 if let Some(message) = diag.label_message.as_ref() {
107 label = label.with_message(message);
108 }
109
110 Some(label)
111}
112
113fn create_label(label: &Label) -> Option<diagnostic::Label<FileId>> {
114 let id = label.span.id()?;
115 let range = label.span.range()?;
116 let style = match label.ty {
117 LabelType::Primary => diagnostic::LabelStyle::Primary,
118 LabelType::Secondary => diagnostic::LabelStyle::Secondary,
119 };
120
121 Some(diagnostic::Label::new(style, id, range.clone()).with_message(&label.message))
122}
123
124#[macro_export]
146#[doc(hidden)]
147macro_rules! __bail {
148 (
150 $fmt:literal $(, $arg:expr)*
151 $(; label_message: $label_message:literal $(, $label_message_arg:expr)*)?
152 $(; label: $label:expr)*
153 $(; note: $note:literal $(, $note_arg:expr)*)*
154 $(; hint: $hint:literal $(, $hint_arg:expr)*)*
155 $(; code: $code:expr)?
156 $(,)?
157 ) => {
158 return Err($crate::diag::error!(
159 $fmt $(, $arg)*
160 $(; label_message: $label_message:literal $(, $label_message_arg:expr)*)?
161 $(; label: $label:expr)*
162 $(; note: $note:literal $(, $note_arg:expr)*)*
163 $(; hint: $hint:literal $(, $hint_arg:expr)*)*
164 $(; code: $code:expr)?
165 ))
166 };
167
168 ($error:expr) => {
170 return Err(::ecow::eco_vec![$error])
171 };
172
173 ($($tts:tt)*) => {
175 return Err(::ecow::eco_vec![$crate::diag::error!($($tts)*)])
176 };
177}
178
179#[macro_export]
182#[doc(hidden)]
183macro_rules! __error {
184 ($fmt:literal $(, $arg:expr)* $(,)?) => {
186 $crate::diag::eco_format!($fmt, $($arg),*).into()
187 };
188
189 (
191 $fmt:literal $(, $arg:expr)*
192 $(; hint: $hint:literal $(, $hint_arg:expr)*)*
193 $(,)?
194 ) => {
195 $crate::diag::HintedString::new(
196 $crate::diag::eco_format!($fmt, $($arg),*)
197 ) $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))*
198 };
199
200 (
202 $span:expr, $fmt:literal $(, $arg:expr)*
203 $(; label_message: $label_message:literal $(, $label_message_arg:expr)*)?
204 $(; label: $label:expr)*
205 $(; fix: $fix:expr)*
206 $(; note: $note:literal $(, $note_arg:expr)*)*
207 $(; hint: $hint:literal $(, $hint_arg:expr)*)*
208 $(; code: $code:expr)?
209 $(,)?
210 $(;)?
211 ) => {
212 $crate::diag::SourceDiagnostic::error(
213 $span,
214 $crate::diag::eco_format!($fmt, $($arg),*),
215 ) $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))*
216 $(.with_label_message($crate::diag::eco_format!($label_message, $($label_message_arg),*)))*
217 $(.with_note($crate::diag::eco_format!($note, $($note_arg),*)))*
218 $(.with_label($label))*
219 $(.with_fix($fix))*
220 $(.with_code($code))*
221
222 };
223}
224
225#[macro_export]
242#[doc(hidden)]
243macro_rules! __warning {
244 (
245 $span:expr,
246 $fmt:literal $(, $arg:expr)*
247 $(; label_message: $label_message:literal $(, $label_message_arg:expr)*)?
248 $(; label: $label:expr)*
249 $(; note: $note:literal $(, $note_arg:expr)*)*
250 $(; hint: $hint:literal $(, $hint_arg:expr)*)*
251 $(; code: $code:expr)?
252 $(,)?
253 $(;)?
254 ) => {
255 $crate::diag::SourceDiagnostic::warning(
256 $span,
257 $crate::diag::eco_format!($fmt, $($arg),*),
258 ) $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))*
259 $(.with_label_message($crate::diag::eco_format!($label_message, $($label_message_arg),*)))*
260 $(.with_note($crate::diag::eco_format!($note, $($note_arg),*)))*
261 $(.with_label($label))*
262 $(.with_code($code))*
263 };
264}
265
266#[rustfmt::skip]
267#[doc(inline)]
268pub use {
269 crate::__bail as bail,
270 crate::__error as error,
271 crate::__warning as warning,
272 ecow::{eco_format, EcoString},
273};
274use compose_error_codes::ErrorCode;
275use compose_library::World;
276
277pub type SourceResult<T> = Result<T, EcoVec<SourceDiagnostic>>;
278pub type StrResult<T> = Result<T, EcoString>;
279
280#[derive(Debug, Clone, PartialEq)]
281pub struct SourceDiagnostic {
282 pub severity: Severity,
283 pub span: Span,
284 pub message: EcoString,
285 pub label_message: Option<EcoString>,
286 pub trace: EcoVec<Spanned<TracePoint>>,
287 pub hints: EcoVec<EcoString>,
288 pub labels: EcoVec<Label>,
289 pub notes: EcoVec<EcoString>,
290 pub code: Option<&'static ErrorCode>,
291 pub fixes: EcoVec<Fix>,
292}
293
294impl SourceDiagnostic {
295 pub fn error<S>(span: Span, message: S) -> Self
296 where
297 S: Into<EcoString>,
298 {
299 Self {
300 severity: Severity::Error,
301 span,
302 message: message.into(),
303 label_message: None,
304 trace: eco_vec!(),
305 hints: eco_vec!(),
306 labels: eco_vec!(),
307 notes: eco_vec!(),
308 code: None,
309 fixes: eco_vec!(),
310 }
311 }
312
313 pub fn warning(span: Span, message: impl Into<EcoString>) -> Self {
314 Self {
315 severity: Severity::Warning,
316 span,
317 message: message.into(),
318 label_message: None,
319 trace: eco_vec!(),
320 hints: eco_vec!(),
321 labels: eco_vec!(),
322 notes: eco_vec!(),
323 code: None,
324 fixes: eco_vec!(),
325 }
326 }
327
328 pub fn with_label_message(mut self, message: impl Into<EcoString>) -> Self {
329 self.label_message = Some(message.into());
330 self
331 }
332
333 pub fn code(&mut self, code: &'static ErrorCode) {
334 self.code = Some(code);
335 }
336
337 pub fn with_code(mut self, code: &'static ErrorCode) -> Self {
338 self.code(code);
339 self
340 }
341
342 pub fn hint(&mut self, hint: impl Into<EcoString>) {
343 self.hints.push(hint.into());
344 }
345
346 pub fn with_hint(mut self, hint: impl Into<EcoString>) -> Self {
347 self.hint(hint);
348 self
349 }
350
351 pub fn note(&mut self, note: impl Into<EcoString>) {
352 self.notes.push(note.into());
353 }
354
355 pub fn with_note(mut self, note: impl Into<EcoString>) -> Self {
356 self.note(note);
357 self
358 }
359
360 pub fn with_label(mut self, label: Label) -> Self {
361 self.labels.push(label);
362 self
363 }
364
365 pub fn add_fix(&mut self, fix: Fix) {
366 self.fixes.push(fix);
367 }
368
369 pub fn with_fix(mut self, fix: Fix) -> Self {
370 self.add_fix(fix);
371 self
372 }
373}
374
375impl From<SyntaxError> for SourceDiagnostic {
376 fn from(error: SyntaxError) -> Self {
377 Self {
378 severity: Severity::from(error.severity),
379 span: error.span,
380 message: error.message,
381 label_message: error.label_message,
382 trace: eco_vec![],
383 hints: error.hints,
384 labels: error.labels,
385 notes: error.notes,
386 code: error.code,
387 fixes: error.fixes,
388 }
389 }
390}
391
392pub trait IntoSourceDiagnostic {
393 fn into_source_diagnostic(self, span: Span) -> SourceDiagnostic;
394}
395
396impl<S> IntoSourceDiagnostic for S
397where
398 S: Into<EcoString>,
399{
400 fn into_source_diagnostic(self, span: Span) -> SourceDiagnostic {
401 SourceDiagnostic::error(span, self.into())
402 }
403}
404
405#[derive(Debug, Clone, Copy, Eq, PartialEq)]
406pub enum Severity {
407 Error,
408 Warning,
409}
410
411impl From<SyntaxErrorSeverity> for Severity {
412 fn from(value: SyntaxErrorSeverity) -> Self {
413 match value {
414 SyntaxErrorSeverity::Error => Severity::Error,
415 SyntaxErrorSeverity::Warning => Severity::Warning,
416 }
417 }
418}
419
420#[derive(Default, Debug, Clone, PartialEq)]
421pub struct Warned<T> {
422 pub value: T,
423 pub warnings: EcoVec<SourceDiagnostic>,
424}
425
426impl<T> Warned<T> {
427 pub fn new(value: T) -> Self {
428 Self {
429 value,
430 warnings: eco_vec!(),
431 }
432 }
433
434 pub fn with_warning(mut self, warning: SourceDiagnostic) -> Self {
435 self.warnings.push(warning);
436 self
437 }
438
439 pub fn extend_warnings(&mut self, warnings: EcoVec<SourceDiagnostic>) {
440 self.warnings.extend(warnings);
441 }
442 pub fn with_warnings(mut self, warnings: EcoVec<SourceDiagnostic>) -> Warned<T> {
443 self.extend_warnings(warnings);
444 self
445 }
446}
447
448impl<T> Deref for Warned<T> {
449 type Target = T;
450
451 fn deref(&self) -> &Self::Target {
452 &self.value
453 }
454}
455
456impl<T> DerefMut for Warned<T> {
457 fn deref_mut(&mut self) -> &mut Self::Target {
458 &mut self.value
459 }
460}
461
462pub trait Trace<T> {
463 fn trace<F>(self, make_point: F, span: Span) -> Self
464 where
465 F: Fn() -> TracePoint;
466}
467
468impl<T> Trace<T> for SourceResult<T> {
469 fn trace<F>(self, make_point: F, span: Span) -> Self
470 where
471 F: Fn() -> TracePoint,
472 {
473 self.map_err(|mut errors| {
474 let Some(trace_range) = span.range() else {
475 return errors;
476 };
477
478 for error in errors.make_mut().iter_mut() {
480 match error.span.range() {
481 Some(error_range)
482 if error.span.id() == span.id()
483 && trace_range.start <= error_range.start
484 && error_range.end >= trace_range.end => {}
485 _ => error.trace.push(Spanned::new(make_point(), span)),
486 }
487 }
488 errors
489 })
490 }
491}
492
493#[derive(Debug, Clone, Eq, PartialEq)]
494pub enum TracePoint {
495 Call(Option<EcoString>),
496 Import,
497}
498
499impl Display for TracePoint {
500 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
501 match self {
502 TracePoint::Call(Some(name)) => {
503 write!(f, "error occurred in this call to `{}`", name)
504 }
505 TracePoint::Call(None) => {
506 write!(f, "error occurred in this call")
507 }
508 TracePoint::Import => {
509 write!(f, "error occurred during this import")
510 }
511 }
512 }
513}
514
515#[derive(Debug, Clone, Copy, Eq, PartialEq)]
516pub struct Spanned<T> {
517 pub value: T,
518 pub span: Span,
519}
520
521impl<T> Spanned<T> {
522 pub fn new(value: T, span: Span) -> Self {
523 Self { value, span }
524 }
525
526 pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Spanned<U> {
527 Spanned {
528 value: f(self.value),
529 span: self.span,
530 }
531 }
532
533 pub fn as_ref(&self) -> Spanned<&T> {
534 Spanned {
535 value: &self.value,
536 span: self.span,
537 }
538 }
539}
540
541pub struct UnSpanned<T> {
542 pub value: T,
543}
544
545impl<T> UnSpanned<T> {
546 pub fn new(value: T) -> Self {
547 Self { value }
548 }
549}
550
551impl<T> From<T> for UnSpanned<T> {
552 fn from(value: T) -> Self {
553 Self { value }
554 }
555}
556
557impl<T> At<T> for Result<T, UnSpanned<SourceDiagnostic>> {
558 fn at(self, span: Span) -> SourceResult<T> {
559 self.map_err(|mut err| {
560 err.value.span = span;
561 eco_vec![err.value]
562 })
563 }
564}
565
566pub trait At<T> {
567 fn at(self, span: Span) -> SourceResult<T>;
568}
569
570impl<T, E> At<T> for Result<T, E>
571where
572 E: IntoSourceDiagnostic,
573{
574 fn at(self, span: Span) -> SourceResult<T> {
575 self.map_err(|err| eco_vec![err.into_source_diagnostic(span)])
576 }
577}
578
579pub type FileResult<T> = Result<T, FileError>;
581
582#[derive(Debug, Clone, Eq, PartialEq, Hash)]
584pub enum FileError {
585 NotFound(PathBuf),
587 AccessDenied,
589 IsDirectory,
591 NotSource,
593 InvalidUtf8,
595 Other(Option<EcoString>),
599}
600
601impl FileError {
602 pub fn from_io(err: io::Error, path: &Path) -> Self {
604 match err.kind() {
605 io::ErrorKind::NotFound => Self::NotFound(path.into()),
606 io::ErrorKind::PermissionDenied => Self::AccessDenied,
607 io::ErrorKind::InvalidData
608 if err
609 .to_string()
610 .contains("stream did not contain valid UTF-8") =>
611 {
612 Self::InvalidUtf8
613 }
614 _ => Self::Other(Some(eco_format!("{err}"))),
615 }
616 }
617}
618
619impl std::error::Error for FileError {}
620
621impl Display for FileError {
622 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
623 match self {
624 Self::NotFound(path) => {
625 write!(f, "file not found (searched at {})", path.display())
626 }
627 Self::AccessDenied => f.pad("failed to load file (access denied)"),
628 Self::IsDirectory => f.pad("failed to load file (is a directory)"),
629 Self::NotSource => f.pad("not a compose source file"),
630 Self::InvalidUtf8 => f.pad("file is not valid utf-8"),
631 Self::Other(Some(err)) => write!(f, "failed to load file ({err})"),
632 Self::Other(None) => f.pad("failed to load file"),
633 }
634 }
635}
636
637impl From<Utf8Error> for FileError {
638 fn from(_: Utf8Error) -> Self {
639 Self::InvalidUtf8
640 }
641}
642
643impl From<FromUtf8Error> for FileError {
644 fn from(_: FromUtf8Error) -> Self {
645 Self::InvalidUtf8
646 }
647}
648
649impl From<FileError> for EcoString {
650 fn from(err: FileError) -> Self {
651 eco_format!("{err}")
652 }
653}