1use std::fmt::{self, Display, Formatter, Write as _};
4use std::io;
5use std::path::{Path, PathBuf};
6use std::str::Utf8Error;
7use std::string::FromUtf8Error;
8
9use az::SaturatingAs;
10use comemo::Tracked;
11use ecow::{EcoVec, eco_vec};
12use typst_syntax::package::{PackageSpec, PackageVersion};
13use typst_syntax::{Lines, Span, Spanned, SyntaxError};
14use utf8_iter::ErrorReportingUtf8Chars;
15
16use crate::engine::Engine;
17use crate::loading::{LoadSource, Loaded};
18use crate::{World, WorldExt};
19
20#[macro_export]
42#[doc(hidden)]
43macro_rules! __bail {
44 (
46 $fmt:literal $(, $arg:expr)*
47 $(; hint: $hint:literal $(, $hint_arg:expr)*)*
48 $(,)?
49 ) => {
50 return Err($crate::diag::error!(
51 $fmt $(, $arg)*
52 $(; hint: $hint $(, $hint_arg)*)*
53 ))
54 };
55
56 ($error:expr) => {
58 return Err(::ecow::eco_vec![$error])
59 };
60
61 ($($tts:tt)*) => {
63 return Err(::ecow::eco_vec![$crate::diag::error!($($tts)*)])
64 };
65}
66
67#[macro_export]
70#[doc(hidden)]
71macro_rules! __error {
72 ($fmt:literal $(, $arg:expr)* $(,)?) => {
74 $crate::diag::eco_format!($fmt, $($arg),*).into()
75 };
76
77 (
79 $fmt:literal $(, $arg:expr)*
80 $(; hint: $hint:literal $(, $hint_arg:expr)*)*
81 $(,)?
82 ) => {
83 $crate::diag::HintedString::new(
84 $crate::diag::eco_format!($fmt, $($arg),*)
85 ) $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))*
86 };
87
88 (
90 $span:expr, $fmt:literal $(, $arg:expr)*
91 $(; hint: $hint:literal $(, $hint_arg:expr)*)*
92 $(,)?
93 ) => {
94 $crate::diag::SourceDiagnostic::error(
95 $span,
96 $crate::diag::eco_format!($fmt, $($arg),*),
97 ) $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))*
98 };
99}
100
101#[macro_export]
118#[doc(hidden)]
119macro_rules! __warning {
120 (
121 $span:expr,
122 $fmt:literal $(, $arg:expr)*
123 $(; hint: $hint:literal $(, $hint_arg:expr)*)*
124 $(,)?
125 ) => {
126 $crate::diag::SourceDiagnostic::warning(
127 $span,
128 $crate::diag::eco_format!($fmt, $($arg),*),
129 ) $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))*
130 };
131}
132
133#[rustfmt::skip]
134#[doc(inline)]
135pub use {
136 crate::__bail as bail,
137 crate::__error as error,
138 crate::__warning as warning,
139 ecow::{eco_format, EcoString},
140};
141
142pub type SourceResult<T> = Result<T, EcoVec<SourceDiagnostic>>;
144
145#[derive(Debug, Clone, Eq, PartialEq, Hash)]
147pub struct Warned<T> {
148 pub output: T,
150 pub warnings: EcoVec<SourceDiagnostic>,
152}
153
154impl<T> Warned<T> {
155 pub fn map<R, F: FnOnce(T) -> R>(self, f: F) -> Warned<R> {
157 Warned { output: f(self.output), warnings: self.warnings }
158 }
159}
160
161#[derive(Debug, Clone, Eq, PartialEq, Hash)]
166pub struct SourceDiagnostic {
167 pub severity: Severity,
169 pub span: Span,
171 pub message: EcoString,
173 pub trace: EcoVec<Spanned<Tracepoint>>,
175 pub hints: EcoVec<EcoString>,
178}
179
180#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
182pub enum Severity {
183 Error,
185 Warning,
187}
188
189impl SourceDiagnostic {
190 pub fn error(span: Span, message: impl Into<EcoString>) -> Self {
192 Self {
193 severity: Severity::Error,
194 span,
195 trace: eco_vec![],
196 message: message.into(),
197 hints: eco_vec![],
198 }
199 }
200
201 pub fn warning(span: Span, message: impl Into<EcoString>) -> Self {
203 Self {
204 severity: Severity::Warning,
205 span,
206 trace: eco_vec![],
207 message: message.into(),
208 hints: eco_vec![],
209 }
210 }
211
212 pub fn hint(&mut self, hint: impl Into<EcoString>) {
214 self.hints.push(hint.into());
215 }
216
217 pub fn with_hint(mut self, hint: impl Into<EcoString>) -> Self {
219 self.hint(hint);
220 self
221 }
222
223 pub fn with_hints(mut self, hints: impl IntoIterator<Item = EcoString>) -> Self {
225 self.hints.extend(hints);
226 self
227 }
228}
229
230impl From<SyntaxError> for SourceDiagnostic {
231 fn from(error: SyntaxError) -> Self {
232 Self {
233 severity: Severity::Error,
234 span: error.span,
235 message: error.message,
236 trace: eco_vec![],
237 hints: error.hints,
238 }
239 }
240}
241
242pub trait DeprecationSink {
244 fn emit(self, message: &str, until: Option<&str>);
247}
248
249impl DeprecationSink for () {
250 fn emit(self, _: &str, _: Option<&str>) {}
251}
252
253impl DeprecationSink for (&mut Engine<'_>, Span) {
254 fn emit(self, message: &str, version: Option<&str>) {
256 self.0
257 .sink
258 .warn(SourceDiagnostic::warning(self.1, message).with_hints(
259 version.map(|v| eco_format!("it will be removed in Typst {}", v)),
260 ));
261 }
262}
263
264#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
266pub enum Tracepoint {
267 Call(Option<EcoString>),
269 Show(EcoString),
271 Import,
273}
274
275impl Display for Tracepoint {
276 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
277 match self {
278 Tracepoint::Call(Some(name)) => {
279 write!(f, "error occurred in this call of function `{name}`")
280 }
281 Tracepoint::Call(None) => {
282 write!(f, "error occurred in this function call")
283 }
284 Tracepoint::Show(name) => {
285 write!(f, "error occurred while applying show rule to this {name}")
286 }
287 Tracepoint::Import => {
288 write!(f, "error occurred while importing this module")
289 }
290 }
291 }
292}
293
294pub trait Trace<T> {
296 fn trace<F>(self, world: Tracked<dyn World + '_>, make_point: F, span: Span) -> Self
298 where
299 F: Fn() -> Tracepoint;
300}
301
302impl<T> Trace<T> for SourceResult<T> {
303 fn trace<F>(self, world: Tracked<dyn World + '_>, make_point: F, span: Span) -> Self
304 where
305 F: Fn() -> Tracepoint,
306 {
307 self.map_err(|mut errors| {
308 let Some(trace_range) = world.range(span) else { return errors };
309 for error in errors.make_mut().iter_mut() {
310 if let Some(error_range) = world.range(error.span)
312 && error.span.id() == span.id()
313 && trace_range.start <= error_range.start
314 && trace_range.end >= error_range.end
315 {
316 continue;
317 }
318
319 error.trace.push(Spanned::new(make_point(), span));
320 }
321 errors
322 })
323 }
324}
325
326pub type StrResult<T> = Result<T, EcoString>;
328
329pub trait At<T> {
332 fn at(self, span: Span) -> SourceResult<T>;
334}
335
336impl<T, S> At<T> for Result<T, S>
337where
338 S: Into<EcoString>,
339{
340 fn at(self, span: Span) -> SourceResult<T> {
341 self.map_err(|message| {
342 let mut diagnostic = SourceDiagnostic::error(span, message);
343 if diagnostic.message.contains("(access denied)") {
344 diagnostic.hint("cannot read file outside of project root");
345 diagnostic
346 .hint("you can adjust the project root with the --root argument");
347 }
348 eco_vec![diagnostic]
349 })
350 }
351}
352
353pub type HintedStrResult<T> = Result<T, HintedString>;
355
356#[derive(Debug, Clone, Eq, PartialEq, Hash)]
364pub struct HintedString(EcoVec<EcoString>);
365
366impl HintedString {
367 pub fn new(message: EcoString) -> Self {
369 Self(eco_vec![message])
370 }
371
372 pub fn message(&self) -> &EcoString {
374 self.0.first().unwrap()
375 }
376
377 pub fn hints(&self) -> &[EcoString] {
380 self.0.get(1..).unwrap_or(&[])
381 }
382
383 pub fn hint(&mut self, hint: impl Into<EcoString>) {
385 self.0.push(hint.into());
386 }
387
388 pub fn with_hint(mut self, hint: impl Into<EcoString>) -> Self {
390 self.hint(hint);
391 self
392 }
393
394 pub fn with_hints(mut self, hints: impl IntoIterator<Item = EcoString>) -> Self {
396 self.0.extend(hints);
397 self
398 }
399}
400
401impl<S> From<S> for HintedString
402where
403 S: Into<EcoString>,
404{
405 fn from(value: S) -> Self {
406 Self::new(value.into())
407 }
408}
409
410impl<T> At<T> for HintedStrResult<T> {
411 fn at(self, span: Span) -> SourceResult<T> {
412 self.map_err(|err| {
413 let mut components = err.0.into_iter();
414 let message = components.next().unwrap();
415 let diag = SourceDiagnostic::error(span, message).with_hints(components);
416 eco_vec![diag]
417 })
418 }
419}
420
421pub trait Hint<T> {
423 fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T>;
425}
426
427impl<T, S> Hint<T> for Result<T, S>
428where
429 S: Into<EcoString>,
430{
431 fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T> {
432 self.map_err(|message| HintedString::new(message.into()).with_hint(hint))
433 }
434}
435
436impl<T> Hint<T> for HintedStrResult<T> {
437 fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T> {
438 self.map_err(|mut error| {
439 error.hint(hint.into());
440 error
441 })
442 }
443}
444
445pub type FileResult<T> = Result<T, FileError>;
447
448#[derive(Debug, Clone, Eq, PartialEq, Hash)]
450pub enum FileError {
451 NotFound(PathBuf),
453 AccessDenied,
455 IsDirectory,
457 NotSource,
459 InvalidUtf8,
461 Package(PackageError),
463 Other(Option<EcoString>),
467}
468
469impl FileError {
470 pub fn from_io(err: io::Error, path: &Path) -> Self {
472 match err.kind() {
473 io::ErrorKind::NotFound => Self::NotFound(path.into()),
474 io::ErrorKind::PermissionDenied => Self::AccessDenied,
475 io::ErrorKind::InvalidData
476 if err.to_string().contains("stream did not contain valid UTF-8") =>
477 {
478 Self::InvalidUtf8
479 }
480 _ => Self::Other(Some(eco_format!("{err}"))),
481 }
482 }
483}
484
485impl std::error::Error for FileError {}
486
487impl Display for FileError {
488 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
489 match self {
490 Self::NotFound(path) => {
491 write!(f, "file not found (searched at {})", path.display())
492 }
493 Self::AccessDenied => f.pad("failed to load file (access denied)"),
494 Self::IsDirectory => f.pad("failed to load file (is a directory)"),
495 Self::NotSource => f.pad("not a Typst source file"),
496 Self::InvalidUtf8 => f.pad("file is not valid utf-8"),
497 Self::Package(error) => error.fmt(f),
498 Self::Other(Some(err)) => write!(f, "failed to load file ({err})"),
499 Self::Other(None) => f.pad("failed to load file"),
500 }
501 }
502}
503
504impl From<Utf8Error> for FileError {
505 fn from(_: Utf8Error) -> Self {
506 Self::InvalidUtf8
507 }
508}
509
510impl From<FromUtf8Error> for FileError {
511 fn from(_: FromUtf8Error) -> Self {
512 Self::InvalidUtf8
513 }
514}
515
516impl From<PackageError> for FileError {
517 fn from(err: PackageError) -> Self {
518 Self::Package(err)
519 }
520}
521
522impl From<FileError> for EcoString {
523 fn from(err: FileError) -> Self {
524 eco_format!("{err}")
525 }
526}
527
528pub type PackageResult<T> = Result<T, PackageError>;
530
531#[derive(Debug, Clone, Eq, PartialEq, Hash)]
535pub enum PackageError {
536 NotFound(PackageSpec),
538 VersionNotFound(PackageSpec, PackageVersion),
540 NetworkFailed(Option<EcoString>),
542 MalformedArchive(Option<EcoString>),
544 Other(Option<EcoString>),
546}
547
548impl std::error::Error for PackageError {}
549
550impl Display for PackageError {
551 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
552 match self {
553 Self::NotFound(spec) => {
554 write!(f, "package not found (searched for {spec})",)
555 }
556 Self::VersionNotFound(spec, latest) => {
557 write!(
558 f,
559 "package found, but version {} does not exist (latest is {})",
560 spec.version, latest,
561 )
562 }
563 Self::NetworkFailed(Some(err)) => {
564 write!(f, "failed to download package ({err})")
565 }
566 Self::NetworkFailed(None) => f.pad("failed to download package"),
567 Self::MalformedArchive(Some(err)) => {
568 write!(f, "failed to decompress package ({err})")
569 }
570 Self::MalformedArchive(None) => {
571 f.pad("failed to decompress package (archive malformed)")
572 }
573 Self::Other(Some(err)) => write!(f, "failed to load package ({err})"),
574 Self::Other(None) => f.pad("failed to load package"),
575 }
576 }
577}
578
579impl From<PackageError> for EcoString {
580 fn from(err: PackageError) -> Self {
581 eco_format!("{err}")
582 }
583}
584
585pub type LoadResult<T> = Result<T, LoadError>;
587
588#[derive(Debug, Clone, Eq, PartialEq, Hash)]
595pub struct LoadError {
596 pos: ReportPos,
598 message: EcoString,
600}
601
602impl LoadError {
603 pub fn new(
607 pos: impl Into<ReportPos>,
608 message: impl std::fmt::Display,
609 error: impl std::fmt::Display,
610 ) -> Self {
611 Self {
612 pos: pos.into(),
613 message: eco_format!("{message} ({error})"),
614 }
615 }
616}
617
618impl From<Utf8Error> for LoadError {
619 fn from(err: Utf8Error) -> Self {
620 let start = err.valid_up_to();
621 let end = start + err.error_len().unwrap_or(0);
622 LoadError::new(
623 start..end,
624 "failed to convert to string",
625 "file is not valid utf-8",
626 )
627 }
628}
629
630pub trait LoadedWithin<T> {
633 fn within(self, loaded: &Loaded) -> SourceResult<T>;
635}
636
637impl<T, E> LoadedWithin<T> for Result<T, E>
638where
639 E: Into<LoadError>,
640{
641 fn within(self, loaded: &Loaded) -> SourceResult<T> {
642 self.map_err(|err| {
643 let LoadError { pos, message } = err.into();
644 load_err_in_text(loaded, pos, message)
645 })
646 }
647}
648
649fn load_err_in_text(
652 loaded: &Loaded,
653 pos: impl Into<ReportPos>,
654 mut message: EcoString,
655) -> EcoVec<SourceDiagnostic> {
656 let pos = pos.into();
657 let lines = Lines::try_from(&loaded.data);
661 match (loaded.source.v, lines) {
662 (LoadSource::Path(file_id), Ok(lines)) => {
663 if let Some(range) = pos.range(&lines) {
664 let span = Span::from_range(file_id, range);
665 return eco_vec![SourceDiagnostic::error(span, message)];
666 }
667
668 let span = Span::from_range(file_id, 0..loaded.data.len());
672 if let Some(pair) = pos.line_col(&lines) {
673 message.pop();
674 let (line, col) = pair.numbers();
675 write!(&mut message, " at {line}:{col})").ok();
676 }
677 eco_vec![SourceDiagnostic::error(span, message)]
678 }
679 (LoadSource::Bytes, Ok(lines)) => {
680 if let Some(pair) = pos.line_col(&lines) {
681 message.pop();
682 let (line, col) = pair.numbers();
683 write!(&mut message, " at {line}:{col})").ok();
684 }
685 eco_vec![SourceDiagnostic::error(loaded.source.span, message)]
686 }
687 _ => load_err_in_invalid_text(loaded, pos, message),
688 }
689}
690
691fn load_err_in_invalid_text(
693 loaded: &Loaded,
694 pos: impl Into<ReportPos>,
695 mut message: EcoString,
696) -> EcoVec<SourceDiagnostic> {
697 let line_col = pos.into().try_line_col(&loaded.data).map(|p| p.numbers());
698 match (loaded.source.v, line_col) {
699 (LoadSource::Path(file), _) => {
700 message.pop();
701 if let Some(package) = file.package() {
702 write!(
703 &mut message,
704 " in {package}{}",
705 file.vpath().as_rooted_path().display()
706 )
707 .ok();
708 } else {
709 write!(&mut message, " in {}", file.vpath().as_rootless_path().display())
710 .ok();
711 };
712 if let Some((line, col)) = line_col {
713 write!(&mut message, ":{line}:{col}").ok();
714 }
715 message.push(')');
716 }
717 (LoadSource::Bytes, Some((line, col))) => {
718 message.pop();
719 write!(&mut message, " at {line}:{col})").ok();
720 }
721 (LoadSource::Bytes, None) => (),
722 }
723 eco_vec![SourceDiagnostic::error(loaded.source.span, message)]
724}
725
726#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
728pub enum ReportPos {
729 Full(std::ops::Range<u32>, LineCol),
731 Range(std::ops::Range<u32>),
733 LineCol(LineCol),
735 #[default]
736 None,
737}
738
739impl From<std::ops::Range<usize>> for ReportPos {
740 fn from(value: std::ops::Range<usize>) -> Self {
741 Self::Range(value.start.saturating_as()..value.end.saturating_as())
742 }
743}
744
745impl From<LineCol> for ReportPos {
746 fn from(value: LineCol) -> Self {
747 Self::LineCol(value)
748 }
749}
750
751impl ReportPos {
752 pub fn full(range: std::ops::Range<usize>, pair: LineCol) -> Self {
754 let range = range.start.saturating_as()..range.end.saturating_as();
755 Self::Full(range, pair)
756 }
757
758 fn range(&self, lines: &Lines<String>) -> Option<std::ops::Range<usize>> {
760 match self {
761 ReportPos::Full(range, _) => Some(range.start as usize..range.end as usize),
762 ReportPos::Range(range) => Some(range.start as usize..range.end as usize),
763 &ReportPos::LineCol(pair) => {
764 let i =
765 lines.line_column_to_byte(pair.line as usize, pair.col as usize)?;
766 Some(i..i)
767 }
768 ReportPos::None => None,
769 }
770 }
771
772 fn line_col(&self, lines: &Lines<String>) -> Option<LineCol> {
774 match self {
775 &ReportPos::Full(_, pair) => Some(pair),
776 ReportPos::Range(range) => {
777 let (line, col) = lines.byte_to_line_column(range.start as usize)?;
778 Some(LineCol::zero_based(line, col))
779 }
780 &ReportPos::LineCol(pair) => Some(pair),
781 ReportPos::None => None,
782 }
783 }
784
785 fn try_line_col(&self, bytes: &[u8]) -> Option<LineCol> {
788 match self {
789 &ReportPos::Full(_, pair) => Some(pair),
790 ReportPos::Range(range) => {
791 LineCol::try_from_byte_pos(range.start as usize, bytes)
792 }
793 &ReportPos::LineCol(pair) => Some(pair),
794 ReportPos::None => None,
795 }
796 }
797}
798
799#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
801pub struct LineCol {
802 line: u32,
804 col: u32,
806}
807
808impl LineCol {
809 pub fn zero_based(line: usize, col: usize) -> Self {
811 Self {
812 line: line.saturating_as(),
813 col: col.saturating_as(),
814 }
815 }
816
817 pub fn one_based(line: usize, col: usize) -> Self {
819 Self::zero_based(line.saturating_sub(1), col.saturating_sub(1))
820 }
821
822 pub fn try_from_byte_pos(pos: usize, bytes: &[u8]) -> Option<Self> {
824 let bytes = &bytes[..pos];
825 let mut line = 0;
826 #[allow(clippy::double_ended_iterator_last)]
827 let line_start = memchr::memchr_iter(b'\n', bytes)
828 .inspect(|_| line += 1)
829 .last()
830 .map(|i| i + 1)
831 .unwrap_or(bytes.len());
832
833 let col = ErrorReportingUtf8Chars::new(&bytes[line_start..]).count();
834 Some(LineCol::zero_based(line, col))
835 }
836
837 pub fn indices(&self) -> (usize, usize) {
839 (self.line as usize, self.col as usize)
840 }
841
842 pub fn numbers(&self) -> (usize, usize) {
844 (self.line as usize + 1, self.col as usize + 1)
845 }
846}
847
848pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> LoadError {
850 let pos = LineCol::one_based(error.pos().row as usize, error.pos().col as usize);
851 let message = match error {
852 roxmltree::Error::UnexpectedCloseTag(expected, actual, _) => {
853 eco_format!(
854 "failed to parse {format} (found closing tag '{actual}' instead of '{expected}')"
855 )
856 }
857 roxmltree::Error::UnknownEntityReference(entity, _) => {
858 eco_format!("failed to parse {format} (unknown entity '{entity}')")
859 }
860 roxmltree::Error::DuplicatedAttribute(attr, _) => {
861 eco_format!("failed to parse {format} (duplicate attribute '{attr}')")
862 }
863 roxmltree::Error::NoRootNode => {
864 eco_format!("failed to parse {format} (missing root node)")
865 }
866 err => eco_format!("failed to parse {format} ({err})"),
867 };
868
869 LoadError { pos: pos.into(), message }
870}