typst_library/
diag.rs

1//! Diagnostics.
2
3use std::fmt::{self, Display, Formatter};
4use std::io;
5use std::path::{Path, PathBuf};
6use std::str::Utf8Error;
7use std::string::FromUtf8Error;
8
9use comemo::Tracked;
10use ecow::{eco_vec, EcoVec};
11use typst_syntax::package::{PackageSpec, PackageVersion};
12use typst_syntax::{Span, Spanned, SyntaxError};
13
14use crate::engine::Engine;
15use crate::{World, WorldExt};
16
17/// Early-return with a [`StrResult`] or [`SourceResult`].
18///
19/// If called with just a string and format args, returns with a
20/// `StrResult`. If called with a span, a string and format args, returns
21/// a `SourceResult`.
22///
23/// You can also emit hints with the `; hint: "..."` syntax.
24///
25/// ```ignore
26/// bail!("bailing with a {}", "string result");
27/// bail!(span, "bailing with a {}", "source result");
28/// bail!(
29///     span, "bailing with a {}", "source result";
30///     hint: "hint 1"
31/// );
32/// bail!(
33///     span, "bailing with a {}", "source result";
34///     hint: "hint 1";
35///     hint: "hint 2";
36/// );
37/// ```
38#[macro_export]
39#[doc(hidden)]
40macro_rules! __bail {
41    // For bail!("just a {}", "string")
42    (
43        $fmt:literal $(, $arg:expr)*
44        $(; hint: $hint:literal $(, $hint_arg:expr)*)*
45        $(,)?
46    ) => {
47        return Err($crate::diag::error!(
48            $fmt $(, $arg)*
49            $(; hint: $hint $(, $hint_arg)*)*
50        ))
51    };
52
53    // For bail!(error!(..))
54    ($error:expr) => {
55        return Err(::ecow::eco_vec![$error])
56    };
57
58    // For bail(span, ...)
59    ($($tts:tt)*) => {
60        return Err(::ecow::eco_vec![$crate::diag::error!($($tts)*)])
61    };
62}
63
64/// Construct an [`EcoString`], [`HintedString`] or [`SourceDiagnostic`] with
65/// severity `Error`.
66#[macro_export]
67#[doc(hidden)]
68macro_rules! __error {
69    // For bail!("just a {}", "string").
70    ($fmt:literal $(, $arg:expr)* $(,)?) => {
71        $crate::diag::eco_format!($fmt, $($arg),*).into()
72    };
73
74    // For bail!("a hinted {}", "string"; hint: "some hint"; hint: "...")
75    (
76        $fmt:literal $(, $arg:expr)*
77        $(; hint: $hint:literal $(, $hint_arg:expr)*)*
78        $(,)?
79    ) => {
80        $crate::diag::HintedString::new(
81            $crate::diag::eco_format!($fmt, $($arg),*)
82        ) $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))*
83    };
84
85    // For bail!(span, ...)
86    (
87        $span:expr, $fmt:literal $(, $arg:expr)*
88        $(; hint: $hint:literal $(, $hint_arg:expr)*)*
89        $(,)?
90    ) => {
91        $crate::diag::SourceDiagnostic::error(
92            $span,
93            $crate::diag::eco_format!($fmt, $($arg),*),
94        )  $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))*
95    };
96}
97
98/// Construct a [`SourceDiagnostic`] with severity `Warning`.
99///
100/// You can also emit hints with the `; hint: "..."` syntax.
101///
102/// ```ignore
103/// warning!(span, "warning with a {}", "source result");
104/// warning!(
105///     span, "warning with a {}", "source result";
106///     hint: "hint 1"
107/// );
108/// warning!(
109///     span, "warning with a {}", "source result";
110///     hint: "hint 1";
111///     hint: "hint 2";
112/// );
113/// ```
114#[macro_export]
115#[doc(hidden)]
116macro_rules! __warning {
117    (
118        $span:expr,
119        $fmt:literal $(, $arg:expr)*
120        $(; hint: $hint:literal $(, $hint_arg:expr)*)*
121        $(,)?
122    ) => {
123        $crate::diag::SourceDiagnostic::warning(
124            $span,
125            $crate::diag::eco_format!($fmt, $($arg),*),
126        ) $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))*
127    };
128}
129
130#[rustfmt::skip]
131#[doc(inline)]
132pub use {
133    crate::__bail as bail,
134    crate::__error as error,
135    crate::__warning as warning,
136    ecow::{eco_format, EcoString},
137};
138
139/// A result that can carry multiple source errors.
140pub type SourceResult<T> = Result<T, EcoVec<SourceDiagnostic>>;
141
142/// An output alongside warnings generated while producing it.
143#[derive(Debug, Clone, Eq, PartialEq, Hash)]
144pub struct Warned<T> {
145    /// The produced output.
146    pub output: T,
147    /// Warnings generated while producing the output.
148    pub warnings: EcoVec<SourceDiagnostic>,
149}
150
151/// An error or warning in a source file.
152///
153/// The contained spans will only be detached if any of the input source files
154/// were detached.
155#[derive(Debug, Clone, Eq, PartialEq, Hash)]
156pub struct SourceDiagnostic {
157    /// Whether the diagnostic is an error or a warning.
158    pub severity: Severity,
159    /// The span of the relevant node in the source code.
160    pub span: Span,
161    /// A diagnostic message describing the problem.
162    pub message: EcoString,
163    /// The trace of function calls leading to the problem.
164    pub trace: EcoVec<Spanned<Tracepoint>>,
165    /// Additional hints to the user, indicating how this problem could be avoided
166    /// or worked around.
167    pub hints: EcoVec<EcoString>,
168}
169
170/// The severity of a [`SourceDiagnostic`].
171#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
172pub enum Severity {
173    /// A fatal error.
174    Error,
175    /// A non-fatal warning.
176    Warning,
177}
178
179impl SourceDiagnostic {
180    /// Create a new, bare error.
181    pub fn error(span: Span, message: impl Into<EcoString>) -> Self {
182        Self {
183            severity: Severity::Error,
184            span,
185            trace: eco_vec![],
186            message: message.into(),
187            hints: eco_vec![],
188        }
189    }
190
191    /// Create a new, bare warning.
192    pub fn warning(span: Span, message: impl Into<EcoString>) -> Self {
193        Self {
194            severity: Severity::Warning,
195            span,
196            trace: eco_vec![],
197            message: message.into(),
198            hints: eco_vec![],
199        }
200    }
201
202    /// Adds a single hint to the diagnostic.
203    pub fn hint(&mut self, hint: impl Into<EcoString>) {
204        self.hints.push(hint.into());
205    }
206
207    /// Adds a single hint to the diagnostic.
208    pub fn with_hint(mut self, hint: impl Into<EcoString>) -> Self {
209        self.hint(hint);
210        self
211    }
212
213    /// Adds user-facing hints to the diagnostic.
214    pub fn with_hints(mut self, hints: impl IntoIterator<Item = EcoString>) -> Self {
215        self.hints.extend(hints);
216        self
217    }
218}
219
220impl From<SyntaxError> for SourceDiagnostic {
221    fn from(error: SyntaxError) -> Self {
222        Self {
223            severity: Severity::Error,
224            span: error.span,
225            message: error.message,
226            trace: eco_vec![],
227            hints: error.hints,
228        }
229    }
230}
231
232/// Destination for a deprecation message when accessing a deprecated value.
233pub trait DeprecationSink {
234    /// Emits the given deprecation message into this sink.
235    fn emit(&mut self, message: &str);
236
237    /// Emits the given deprecation message into this sink, with the given
238    /// hints.
239    fn emit_with_hints(&mut self, message: &str, hints: &[&str]);
240}
241
242impl DeprecationSink for () {
243    fn emit(&mut self, _: &str) {}
244    fn emit_with_hints(&mut self, _: &str, _: &[&str]) {}
245}
246
247impl DeprecationSink for (&mut Vec<SourceDiagnostic>, Span) {
248    fn emit(&mut self, message: &str) {
249        self.0.push(SourceDiagnostic::warning(self.1, message));
250    }
251
252    fn emit_with_hints(&mut self, message: &str, hints: &[&str]) {
253        self.0.push(
254            SourceDiagnostic::warning(self.1, message)
255                .with_hints(hints.iter().copied().map(Into::into)),
256        );
257    }
258}
259
260impl DeprecationSink for (&mut Engine<'_>, Span) {
261    fn emit(&mut self, message: &str) {
262        self.0.sink.warn(SourceDiagnostic::warning(self.1, message));
263    }
264
265    fn emit_with_hints(&mut self, message: &str, hints: &[&str]) {
266        self.0.sink.warn(
267            SourceDiagnostic::warning(self.1, message)
268                .with_hints(hints.iter().copied().map(Into::into)),
269        );
270    }
271}
272
273/// A part of a diagnostic's [trace](SourceDiagnostic::trace).
274#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
275pub enum Tracepoint {
276    /// A function call.
277    Call(Option<EcoString>),
278    /// A show rule application.
279    Show(EcoString),
280    /// A module import.
281    Import,
282}
283
284impl Display for Tracepoint {
285    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
286        match self {
287            Tracepoint::Call(Some(name)) => {
288                write!(f, "error occurred in this call of function `{name}`")
289            }
290            Tracepoint::Call(None) => {
291                write!(f, "error occurred in this function call")
292            }
293            Tracepoint::Show(name) => {
294                write!(f, "error occurred while applying show rule to this {name}")
295            }
296            Tracepoint::Import => {
297                write!(f, "error occurred while importing this module")
298            }
299        }
300    }
301}
302
303/// Enrich a [`SourceResult`] with a tracepoint.
304pub trait Trace<T> {
305    /// Add the tracepoint to all errors that lie outside the `span`.
306    fn trace<F>(self, world: Tracked<dyn World + '_>, make_point: F, span: Span) -> Self
307    where
308        F: Fn() -> Tracepoint;
309}
310
311impl<T> Trace<T> for SourceResult<T> {
312    fn trace<F>(self, world: Tracked<dyn World + '_>, make_point: F, span: Span) -> Self
313    where
314        F: Fn() -> Tracepoint,
315    {
316        self.map_err(|mut errors| {
317            let Some(trace_range) = world.range(span) else { return errors };
318            for error in errors.make_mut().iter_mut() {
319                // Skip traces that surround the error.
320                if let Some(error_range) = world.range(error.span) {
321                    if error.span.id() == span.id()
322                        && trace_range.start <= error_range.start
323                        && trace_range.end >= error_range.end
324                    {
325                        continue;
326                    }
327                }
328
329                error.trace.push(Spanned::new(make_point(), span));
330            }
331            errors
332        })
333    }
334}
335
336/// A result type with a string error message.
337pub type StrResult<T> = Result<T, EcoString>;
338
339/// Convert a [`StrResult`] or [`HintedStrResult`] to a [`SourceResult`] by
340/// adding span information.
341pub trait At<T> {
342    /// Add the span information.
343    fn at(self, span: Span) -> SourceResult<T>;
344}
345
346impl<T, S> At<T> for Result<T, S>
347where
348    S: Into<EcoString>,
349{
350    fn at(self, span: Span) -> SourceResult<T> {
351        self.map_err(|message| {
352            let mut diagnostic = SourceDiagnostic::error(span, message);
353            if diagnostic.message.contains("(access denied)") {
354                diagnostic.hint("cannot read file outside of project root");
355                diagnostic
356                    .hint("you can adjust the project root with the --root argument");
357            }
358            eco_vec![diagnostic]
359        })
360    }
361}
362
363/// A result type with a string error message and hints.
364pub type HintedStrResult<T> = Result<T, HintedString>;
365
366/// A string message with hints.
367///
368/// This is internally represented by a vector of strings.
369/// The first element of the vector contains the message.
370/// The remaining elements are the hints.
371/// This is done to reduce the size of a HintedString.
372/// The vector is guaranteed to not be empty.
373#[derive(Debug, Clone, Eq, PartialEq, Hash)]
374pub struct HintedString(EcoVec<EcoString>);
375
376impl HintedString {
377    /// Creates a new hinted string with the given message.
378    pub fn new(message: EcoString) -> Self {
379        Self(eco_vec![message])
380    }
381
382    /// A diagnostic message describing the problem.
383    pub fn message(&self) -> &EcoString {
384        self.0.first().unwrap()
385    }
386
387    /// Additional hints to the user, indicating how this error could be avoided
388    /// or worked around.
389    pub fn hints(&self) -> &[EcoString] {
390        self.0.get(1..).unwrap_or(&[])
391    }
392
393    /// Adds a single hint to the hinted string.
394    pub fn hint(&mut self, hint: impl Into<EcoString>) {
395        self.0.push(hint.into());
396    }
397
398    /// Adds a single hint to the hinted string.
399    pub fn with_hint(mut self, hint: impl Into<EcoString>) -> Self {
400        self.hint(hint);
401        self
402    }
403
404    /// Adds user-facing hints to the hinted string.
405    pub fn with_hints(mut self, hints: impl IntoIterator<Item = EcoString>) -> Self {
406        self.0.extend(hints);
407        self
408    }
409}
410
411impl<S> From<S> for HintedString
412where
413    S: Into<EcoString>,
414{
415    fn from(value: S) -> Self {
416        Self::new(value.into())
417    }
418}
419
420impl<T> At<T> for HintedStrResult<T> {
421    fn at(self, span: Span) -> SourceResult<T> {
422        self.map_err(|err| {
423            let mut components = err.0.into_iter();
424            let message = components.next().unwrap();
425            let diag = SourceDiagnostic::error(span, message).with_hints(components);
426            eco_vec![diag]
427        })
428    }
429}
430
431/// Enrich a [`StrResult`] or [`HintedStrResult`] with a hint.
432pub trait Hint<T> {
433    /// Add the hint.
434    fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T>;
435}
436
437impl<T, S> Hint<T> for Result<T, S>
438where
439    S: Into<EcoString>,
440{
441    fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T> {
442        self.map_err(|message| HintedString::new(message.into()).with_hint(hint))
443    }
444}
445
446impl<T> Hint<T> for HintedStrResult<T> {
447    fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T> {
448        self.map_err(|mut error| {
449            error.hint(hint.into());
450            error
451        })
452    }
453}
454
455/// A result type with a file-related error.
456pub type FileResult<T> = Result<T, FileError>;
457
458/// An error that occurred while trying to load of a file.
459#[derive(Debug, Clone, Eq, PartialEq, Hash)]
460pub enum FileError {
461    /// A file was not found at this path.
462    NotFound(PathBuf),
463    /// A file could not be accessed.
464    AccessDenied,
465    /// A directory was found, but a file was expected.
466    IsDirectory,
467    /// The file is not a Typst source file, but should have been.
468    NotSource,
469    /// The file was not valid UTF-8, but should have been.
470    InvalidUtf8,
471    /// The package the file is part of could not be loaded.
472    Package(PackageError),
473    /// Another error.
474    ///
475    /// The optional string can give more details, if available.
476    Other(Option<EcoString>),
477}
478
479impl FileError {
480    /// Create a file error from an I/O error.
481    pub fn from_io(err: io::Error, path: &Path) -> Self {
482        match err.kind() {
483            io::ErrorKind::NotFound => Self::NotFound(path.into()),
484            io::ErrorKind::PermissionDenied => Self::AccessDenied,
485            io::ErrorKind::InvalidData
486                if err.to_string().contains("stream did not contain valid UTF-8") =>
487            {
488                Self::InvalidUtf8
489            }
490            _ => Self::Other(Some(eco_format!("{err}"))),
491        }
492    }
493}
494
495impl std::error::Error for FileError {}
496
497impl Display for FileError {
498    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
499        match self {
500            Self::NotFound(path) => {
501                write!(f, "file not found (searched at {})", path.display())
502            }
503            Self::AccessDenied => f.pad("failed to load file (access denied)"),
504            Self::IsDirectory => f.pad("failed to load file (is a directory)"),
505            Self::NotSource => f.pad("not a typst source file"),
506            Self::InvalidUtf8 => f.pad("file is not valid utf-8"),
507            Self::Package(error) => error.fmt(f),
508            Self::Other(Some(err)) => write!(f, "failed to load file ({err})"),
509            Self::Other(None) => f.pad("failed to load file"),
510        }
511    }
512}
513
514impl From<Utf8Error> for FileError {
515    fn from(_: Utf8Error) -> Self {
516        Self::InvalidUtf8
517    }
518}
519
520impl From<FromUtf8Error> for FileError {
521    fn from(_: FromUtf8Error) -> Self {
522        Self::InvalidUtf8
523    }
524}
525
526impl From<PackageError> for FileError {
527    fn from(err: PackageError) -> Self {
528        Self::Package(err)
529    }
530}
531
532impl From<FileError> for EcoString {
533    fn from(err: FileError) -> Self {
534        eco_format!("{err}")
535    }
536}
537
538/// A result type with a package-related error.
539pub type PackageResult<T> = Result<T, PackageError>;
540
541/// An error that occurred while trying to load a package.
542///
543/// Some variants have an optional string can give more details, if available.
544#[derive(Debug, Clone, Eq, PartialEq, Hash)]
545pub enum PackageError {
546    /// The specified package does not exist.
547    NotFound(PackageSpec),
548    /// The specified package found, but the version does not exist.
549    VersionNotFound(PackageSpec, PackageVersion),
550    /// Failed to retrieve the package through the network.
551    NetworkFailed(Option<EcoString>),
552    /// The package archive was malformed.
553    MalformedArchive(Option<EcoString>),
554    /// Another error.
555    Other(Option<EcoString>),
556}
557
558impl std::error::Error for PackageError {}
559
560impl Display for PackageError {
561    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
562        match self {
563            Self::NotFound(spec) => {
564                write!(f, "package not found (searched for {spec})",)
565            }
566            Self::VersionNotFound(spec, latest) => {
567                write!(
568                    f,
569                    "package found, but version {} does not exist (latest is {})",
570                    spec.version, latest,
571                )
572            }
573            Self::NetworkFailed(Some(err)) => {
574                write!(f, "failed to download package ({err})")
575            }
576            Self::NetworkFailed(None) => f.pad("failed to download package"),
577            Self::MalformedArchive(Some(err)) => {
578                write!(f, "failed to decompress package ({err})")
579            }
580            Self::MalformedArchive(None) => {
581                f.pad("failed to decompress package (archive malformed)")
582            }
583            Self::Other(Some(err)) => write!(f, "failed to load package ({err})"),
584            Self::Other(None) => f.pad("failed to load package"),
585        }
586    }
587}
588
589impl From<PackageError> for EcoString {
590    fn from(err: PackageError) -> Self {
591        eco_format!("{err}")
592    }
593}
594
595/// Format a user-facing error message for an XML-like file format.
596pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> EcoString {
597    match error {
598        roxmltree::Error::UnexpectedCloseTag(expected, actual, pos) => {
599            eco_format!(
600                "failed to parse {format} (found closing tag '{actual}' \
601                 instead of '{expected}' in line {})",
602                pos.row
603            )
604        }
605        roxmltree::Error::UnknownEntityReference(entity, pos) => {
606            eco_format!(
607                "failed to parse {format} (unknown entity '{entity}' in line {})",
608                pos.row
609            )
610        }
611        roxmltree::Error::DuplicatedAttribute(attr, pos) => {
612            eco_format!(
613                "failed to parse {format} (duplicate attribute '{attr}' in line {})",
614                pos.row
615            )
616        }
617        roxmltree::Error::NoRootNode => {
618            eco_format!("failed to parse {format} (missing root node)")
619        }
620        err => eco_format!("failed to parse {format} ({err})"),
621    }
622}