compose_library/
diag.rs

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/// Early-return with a [`StrResult`] or [`SourceResult`].
125///
126/// If called with just a string and format args, returns with a
127/// `StrResult`. If called with a span, a string and format args, returns
128/// a `SourceResult`.
129///
130/// You can also emit hints with the `; hint: "..."` syntax.
131///
132/// ```ignore
133/// bail!("bailing with a {}", "string result");
134/// bail!(span, "bailing with a {}", "source result");
135/// bail!(
136///     span, "bailing with a {}", "source result";
137///     hint: "hint 1"
138/// );
139/// bail!(
140///     span, "bailing with a {}", "source result";
141///     hint: "hint 1";
142///     hint: "hint 2";
143/// );
144/// ```
145#[macro_export]
146#[doc(hidden)]
147macro_rules! __bail {
148    // For bail!("just a {}", "string")
149    (
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    // For bail!(error!(..))
169    ($error:expr) => {
170        return Err(::ecow::eco_vec![$error])
171    };
172
173    // For bail(span, ...)
174    ($($tts:tt)*) => {
175        return Err(::ecow::eco_vec![$crate::diag::error!($($tts)*)])
176    };
177}
178
179/// Construct an [`EcoString`], [`HintedString`] or [`SourceDiagnostic`] with
180/// severity `Error`.
181#[macro_export]
182#[doc(hidden)]
183macro_rules! __error {
184    // For bail!("just a {}", "string").
185    ($fmt:literal $(, $arg:expr)* $(,)?) => {
186        $crate::diag::eco_format!($fmt, $($arg),*).into()
187    };
188
189    // For bail!("a hinted {}", "string"; hint: "some hint"; hint: "...")
190    (
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    // For bail!(span, ...)
201    (
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/// Construct a [`SourceDiagnostic`] with severity `Warning`.
226///
227/// You can also emit hints with the `; hint: "..."` syntax.
228///
229/// ```ignore
230/// warning!(span, "warning with a {}", "source result");
231/// warning!(
232///     span, "warning with a {}", "source result";
233///     hint: "hint 1"
234/// );
235/// warning!(
236///     span, "warning with a {}", "source result";
237///     hint: "hint 1";
238///     hint: "hint 2";
239/// );
240/// ```
241#[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            // Skip traces that are fully contained within the given span.
479            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
579/// A result type with a file-related error.
580pub type FileResult<T> = Result<T, FileError>;
581
582/// An error that occurred while trying to load of a file.
583#[derive(Debug, Clone, Eq, PartialEq, Hash)]
584pub enum FileError {
585    /// A file was not found at this path.
586    NotFound(PathBuf),
587    /// A file could not be accessed.
588    AccessDenied,
589    /// A directory was found, but a file was expected.
590    IsDirectory,
591    /// The file is not a Compose source file, but should have been.
592    NotSource,
593    /// The file was not valid UTF-8, but should have been.
594    InvalidUtf8,
595    /// Another error.
596    ///
597    /// The optional string can give more details, if available.
598    Other(Option<EcoString>),
599}
600
601impl FileError {
602    /// Create a file error from an I/O error.
603    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}