Skip to main content

syntaxmate/
error.rs

1use std::{fmt, sync::Arc};
2
3/// Error returned by a fallible Syntaxmate operation.
4///
5/// Match the payload's kind to select recovery without parsing display text.
6/// JSON causes remain available through [`std::error::Error::source`]. Clones
7/// share those causes; equality compares their category, position and message.
8#[derive(Debug, Clone, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum Error {
11    /// The requested language ID or alias is not present in the catalog.
12    UnknownLanguage(String),
13    /// The requested bundled theme is not present in the catalog.
14    UnknownTheme(String),
15    /// A grammar could not be parsed, validated, or prepared.
16    Grammar(GrammarError),
17    /// A theme could not be parsed or compiled.
18    Theme(ThemeError),
19    /// A bundled asset could not be decoded or validated.
20    Bundle(BundleError),
21    /// A feature-gated diagnostic operation failed.
22    Diagnostic(DiagnosticError),
23    /// Source validation or output writing failed.
24    Render(RenderError),
25    /// Incremental state belongs to another tokenizer.
26    StateMismatch,
27    /// Incremental input contained more than one logical line.
28    InvalidLine,
29}
30
31impl fmt::Display for Error {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        match self {
34            Self::UnknownLanguage(value) => write!(f, "unknown TextMate language `{value}`"),
35            Self::UnknownTheme(value) => write!(f, "unknown TextMate theme `{value}`"),
36            Self::Grammar(error) => error.fmt(f),
37            Self::Theme(error) => error.fmt(f),
38            Self::Bundle(error) => error.fmt(f),
39            Self::Diagnostic(error) => error.fmt(f),
40            Self::Render(error) => error.fmt(f),
41            Self::StateMismatch => {
42                f.write_str("tokenizer state belongs to a different Syntaxmate tokenizer")
43            }
44            Self::InvalidLine => {
45                f.write_str("tokenize_line expects one logical line without a newline terminator")
46            }
47        }
48    }
49}
50
51impl std::error::Error for Error {
52    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
53        match self {
54            Self::Grammar(error) => Some(error),
55            Self::Theme(error) => Some(error),
56            Self::Bundle(error) => Some(error),
57            Self::Diagnostic(error) => Some(error),
58            Self::Render(error) => Some(error),
59            _ => None,
60        }
61    }
62}
63
64/// Result type for fallible Syntaxmate operations.
65pub type Result<T> = std::result::Result<T, Error>;
66
67/// A JSON decoding failure with its original serde cause.
68#[derive(Debug, Clone)]
69#[non_exhaustive]
70pub struct JsonError(Arc<serde_json::Error>);
71
72impl JsonError {
73    pub(crate) fn new(error: serde_json::Error) -> Self {
74        Self(Arc::new(error))
75    }
76    /// One-based input line reported by serde.
77    pub fn line(&self) -> usize {
78        self.0.line()
79    }
80    /// One-based input column reported by serde, or zero before any input.
81    pub fn column(&self) -> usize {
82        self.0.column()
83    }
84}
85impl PartialEq for JsonError {
86    fn eq(&self, other: &Self) -> bool {
87        self.0.classify() == other.0.classify()
88            && self.line() == other.line()
89            && self.column() == other.column()
90            && self.0.to_string() == other.0.to_string()
91    }
92}
93impl Eq for JsonError {}
94impl fmt::Display for JsonError {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        self.0.fmt(f)
97    }
98}
99impl std::error::Error for JsonError {
100    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
101        Some(self.0.as_ref())
102    }
103}
104
105/// A missing local or external grammar include.
106#[derive(Debug, Clone, PartialEq, Eq)]
107#[non_exhaustive]
108pub struct MissingInclude {
109    scope: Option<String>,
110    repository: Option<String>,
111}
112impl MissingInclude {
113    pub(crate) fn new(scope: Option<String>, repository: Option<String>) -> Self {
114        Self { scope, repository }
115    }
116    /// Referenced external scope, or `None` for a local repository include.
117    pub fn scope_name(&self) -> Option<&str> {
118        self.scope.as_deref()
119    }
120    /// Referenced repository key without the `#`, if present.
121    pub fn repository_key(&self) -> Option<&str> {
122        self.repository.as_deref()
123    }
124}
125
126/// A regex parser diagnostic. Offsets count Unicode scalar values, not bytes.
127#[derive(Debug, Clone, PartialEq, Eq)]
128#[non_exhaustive]
129pub struct RegexError {
130    pattern: String,
131    position: usize,
132    message: String,
133}
134impl RegexError {
135    pub(crate) fn new(pattern: String, position: usize, message: String) -> Self {
136        Self {
137            pattern,
138            position,
139            message,
140        }
141    }
142    /// Original regex pattern.
143    pub fn pattern(&self) -> &str {
144        &self.pattern
145    }
146    /// Zero-based Unicode scalar offset, possibly at the end of the pattern.
147    pub fn position(&self) -> usize {
148        self.position
149    }
150    /// Human-readable parser diagnostic.
151    pub fn message(&self) -> &str {
152        &self.message
153    }
154}
155
156/// The custom grammar resource whose configured limit was exceeded.
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158#[non_exhaustive]
159pub enum GrammarResource {
160    /// JSON input bytes for one grammar.
161    GrammarBytes,
162    /// Number of grammars in the registry.
163    GrammarCount,
164}
165
166/// A configured grammar limit and the attempted resource usage.
167#[derive(Debug, Clone, PartialEq, Eq)]
168#[non_exhaustive]
169pub struct LimitExceeded {
170    resource: GrammarResource,
171    limit: usize,
172    actual: usize,
173}
174impl LimitExceeded {
175    pub(crate) fn new(resource: GrammarResource, limit: usize, actual: usize) -> Self {
176        Self {
177            resource,
178            limit,
179            actual,
180        }
181    }
182    /// Resource being measured; byte limits use UTF-8 bytes.
183    pub fn resource(&self) -> GrammarResource {
184        self.resource
185    }
186    /// Maximum permitted value.
187    pub fn limit(&self) -> usize {
188        self.limit
189    }
190    /// Attempted value, including the grammar being added.
191    pub fn actual(&self) -> usize {
192        self.actual
193    }
194}
195
196/// Matchable cause of a grammar failure.
197#[derive(Debug, Clone, PartialEq, Eq)]
198#[non_exhaustive]
199pub enum GrammarErrorKind {
200    /// Invalid JSON syntax or grammar JSON structure.
201    InvalidJson(JsonError),
202    /// A local repository or external grammar include was not found.
203    MissingInclude(MissingInclude),
204    /// Explicit regex validation found a parser diagnostic.
205    InvalidRegex(RegexError),
206    /// A configured registry limit was exceeded.
207    LimitExceeded(LimitExceeded),
208    /// The root grammar ID does not belong to this registry.
209    ForeignGrammarId,
210    /// Preparation exceeded an internal graph-walk bound; use a tokenizer directly.
211    PreparationLimit(String),
212    /// A compiled reference is invalid for a reason other than a missing include.
213    InvalidReference(String),
214}
215
216/// Grammar failure with the originating scope when available.
217#[derive(Debug, Clone, PartialEq, Eq)]
218#[non_exhaustive]
219pub struct GrammarError(Box<GrammarErrorInner>);
220
221#[derive(Debug, Clone, PartialEq, Eq)]
222struct GrammarErrorInner {
223    scope: Option<String>,
224    kind: GrammarErrorKind,
225}
226impl GrammarError {
227    pub(crate) fn new(scope: Option<String>, kind: GrammarErrorKind) -> Self {
228        Self(Box::new(GrammarErrorInner { scope, kind }))
229    }
230    /// Grammar scope, or `None` when parsing/limits failed before it was known.
231    pub fn scope_name(&self) -> Option<&str> {
232        self.0.scope.as_deref()
233    }
234    /// Matchable failure cause.
235    pub fn kind(&self) -> &GrammarErrorKind {
236        &self.0.kind
237    }
238}
239impl fmt::Display for GrammarError {
240    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241        if let Some(scope) = &self.0.scope {
242            write!(f, "{scope}: ")?;
243        }
244        match &self.0.kind {
245            GrammarErrorKind::InvalidJson(error) => write!(f, "JSON parse error: {error}"),
246            GrammarErrorKind::MissingInclude(include) => write!(
247                f,
248                "unknown include {}{}{}",
249                include.scope_name().unwrap_or(""),
250                if include.repository.is_some() {
251                    "#"
252                } else {
253                    ""
254                },
255                include.repository_key().unwrap_or("")
256            ),
257            GrammarErrorKind::InvalidRegex(error) => {
258                write!(f, "invalid regex `{}`: {}", error.pattern, error.message)
259            }
260            GrammarErrorKind::LimitExceeded(error) => write!(
261                f,
262                "grammar {:?} value {} exceeds limit {}",
263                error.resource, error.actual, error.limit
264            ),
265            GrammarErrorKind::ForeignGrammarId => {
266                f.write_str("root grammar does not belong to this registry")
267            }
268            GrammarErrorKind::PreparationLimit(detail) => write!(
269                f,
270                "grammar exceeds PreparedLanguage preparation bounds ({detail}); use Tokenizer directly"
271            ),
272            GrammarErrorKind::InvalidReference(message) => f.write_str(message),
273        }
274    }
275}
276impl std::error::Error for GrammarError {
277    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
278        match &self.0.kind {
279            GrammarErrorKind::InvalidJson(error) => Some(error),
280            _ => None,
281        }
282    }
283}
284
285/// Matchable cause of a theme failure.
286#[derive(Debug, Clone, PartialEq, Eq)]
287#[non_exhaustive]
288pub enum ThemeErrorKind {
289    /// Invalid JSON syntax or theme JSON structure.
290    InvalidJson(JsonError),
291    /// Unsupported color syntax or alpha/background combination; contains the input value.
292    InvalidColor(String),
293    /// Invalid selector, font style, or empty rule settings.
294    InvalidRule,
295}
296
297/// Theme parsing or rule compilation failure.
298#[derive(Debug, Clone, PartialEq, Eq)]
299#[non_exhaustive]
300pub struct ThemeError(Box<ThemeErrorInner>);
301
302#[derive(Debug, Clone, PartialEq, Eq)]
303struct ThemeErrorInner {
304    kind: ThemeErrorKind,
305    message: String,
306}
307impl ThemeError {
308    pub(crate) fn rule(message: String) -> Self {
309        Self::new(ThemeErrorKind::InvalidRule, message)
310    }
311    pub(crate) fn new(kind: ThemeErrorKind, message: String) -> Self {
312        Self(Box::new(ThemeErrorInner { kind, message }))
313    }
314    pub(crate) fn json(error: serde_json::Error) -> Self {
315        let message = format!("invalid TextMate theme JSON: {error}");
316        Self::new(ThemeErrorKind::InvalidJson(JsonError::new(error)), message)
317    }
318    pub(crate) fn color(value: &str, message: String) -> Self {
319        Self::new(ThemeErrorKind::InvalidColor(value.to_owned()), message)
320    }
321    /// Matchable failure cause, including the invalid color when applicable.
322    pub fn kind(&self) -> &ThemeErrorKind {
323        &self.0.kind
324    }
325}
326impl fmt::Display for ThemeError {
327    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328        f.write_str(&self.0.message)
329    }
330}
331impl std::error::Error for ThemeError {
332    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
333        match &self.0.kind {
334            ThemeErrorKind::InvalidJson(error) => Some(error),
335            _ => None,
336        }
337    }
338}
339
340/// Matchable cause of a bundled asset failure.
341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
342#[non_exhaustive]
343pub enum BundleErrorKind {
344    /// The embedded catalog has no languages.
345    EmptyCatalog,
346    /// A requested grammar or its closure is absent.
347    MissingGrammar,
348    /// Compiled grammar data could not be decoded.
349    Decode,
350    /// Caller-supplied bundle bytes are malformed or failed validation.
351    Invalid,
352    /// Caller-supplied bundle bytes exceed the accepted size.
353    TooLarge,
354}
355
356/// Bundled asset failure without exposing the private bundle format.
357#[derive(Debug, Clone, PartialEq, Eq)]
358#[non_exhaustive]
359pub struct BundleError(Box<BundleErrorInner>);
360
361#[derive(Debug, Clone, PartialEq, Eq)]
362struct BundleErrorInner {
363    kind: BundleErrorKind,
364    language: Option<String>,
365    message: String,
366}
367impl BundleError {
368    pub(crate) fn new(kind: BundleErrorKind, language: Option<String>, message: String) -> Self {
369        Self(Box::new(BundleErrorInner {
370            kind,
371            language,
372            message,
373        }))
374    }
375    /// Matchable failure cause.
376    pub fn kind(&self) -> BundleErrorKind {
377        self.0.kind
378    }
379    /// Affected language or scope, when known.
380    pub fn language(&self) -> Option<&str> {
381        self.0.language.as_deref()
382    }
383}
384impl fmt::Display for BundleError {
385    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386        f.write_str(&self.0.message)
387    }
388}
389impl std::error::Error for BundleError {}
390
391/// Matchable cause of a diagnostic regex operation failure.
392#[derive(Debug, Clone, Copy, PartialEq, Eq)]
393#[non_exhaustive]
394pub enum DiagnosticErrorKind {
395    /// The requested matcher cannot compile this pattern.
396    MatcherBuild,
397    /// The fallback matcher exhausted its execution budget.
398    BudgetExceeded,
399    /// The starting byte offset is outside the input or inside a UTF-8 character.
400    InvalidStart,
401}
402
403/// Diagnostic regex failure with the original pattern and execution context.
404#[derive(Debug, Clone, PartialEq, Eq)]
405#[non_exhaustive]
406pub struct DiagnosticError(Box<DiagnosticErrorInner>);
407
408#[derive(Debug, Clone, PartialEq, Eq)]
409struct DiagnosticErrorInner {
410    kind: DiagnosticErrorKind,
411    pattern: String,
412    position: Option<usize>,
413    steps: Option<usize>,
414    message: String,
415}
416impl DiagnosticError {
417    #[cfg(feature = "diagnostics")]
418    pub(crate) fn build(pattern: &str, message: String) -> Self {
419        Self(Box::new(DiagnosticErrorInner {
420            kind: DiagnosticErrorKind::MatcherBuild,
421            pattern: pattern.to_owned(),
422            position: None,
423            steps: None,
424            message,
425        }))
426    }
427    #[cfg(feature = "diagnostics")]
428    pub(crate) fn fallback(pattern: &str, error: crate::engine::regex::FallbackError) -> Self {
429        use crate::engine::regex::FallbackError;
430        let (kind, position, steps) = match error {
431            FallbackError::InvalidStart { from } => {
432                (DiagnosticErrorKind::InvalidStart, Some(from), None)
433            }
434            FallbackError::BudgetExceeded { steps } => {
435                (DiagnosticErrorKind::BudgetExceeded, None, Some(steps))
436            }
437        };
438        Self(Box::new(DiagnosticErrorInner {
439            kind,
440            pattern: pattern.to_owned(),
441            position,
442            steps,
443            message: format!("fallback error: {error:?}"),
444        }))
445    }
446    /// Matchable failure cause.
447    pub fn kind(&self) -> DiagnosticErrorKind {
448        self.0.kind
449    }
450    /// Original regex pattern.
451    pub fn pattern(&self) -> &str {
452        &self.0.pattern
453    }
454    /// Invalid starting byte offset, if applicable.
455    pub fn position(&self) -> Option<usize> {
456        self.0.position
457    }
458    /// Executed fallback steps when the budget was exhausted.
459    pub fn steps(&self) -> Option<usize> {
460        self.0.steps
461    }
462}
463impl fmt::Display for DiagnosticError {
464    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
465        f.write_str(&self.0.message)
466    }
467}
468impl std::error::Error for DiagnosticError {}
469
470/// Matchable cause of a render failure.
471#[derive(Debug, Clone, Copy, PartialEq, Eq)]
472#[non_exhaustive]
473pub enum RenderErrorKind {
474    /// Source line counts or byte ranges do not match the document.
475    SourceMismatch,
476    /// The output writer returned [`fmt::Error`]; output may be partial.
477    Writer,
478}
479
480/// Render validation or writer failure.
481#[derive(Debug, Clone, PartialEq, Eq)]
482#[non_exhaustive]
483pub struct RenderError(Box<RenderErrorInner>);
484
485#[derive(Debug, Clone, PartialEq, Eq)]
486struct RenderErrorInner {
487    kind: RenderErrorKind,
488    message: String,
489    source: Option<fmt::Error>,
490}
491impl RenderError {
492    #[cfg(any(feature = "html", feature = "ansi"))]
493    pub(crate) fn mismatch(message: String) -> Self {
494        Self(Box::new(RenderErrorInner {
495            kind: RenderErrorKind::SourceMismatch,
496            message,
497            source: None,
498        }))
499    }
500    #[cfg(any(feature = "html", feature = "ansi"))]
501    pub(crate) fn writer(error: fmt::Error) -> Self {
502        Self(Box::new(RenderErrorInner {
503            kind: RenderErrorKind::Writer,
504            message: "render output writer failed".to_owned(),
505            source: Some(error),
506        }))
507    }
508    /// Matchable failure cause.
509    pub fn kind(&self) -> RenderErrorKind {
510        self.0.kind
511    }
512}
513impl fmt::Display for RenderError {
514    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
515        f.write_str(&self.0.message)
516    }
517}
518impl std::error::Error for RenderError {
519    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
520        self.0.source.as_ref().map(|error| error as _)
521    }
522}
523
524pub(crate) fn grammar_load_error(error: crate::engine::grammar::GrammarLoadError) -> Error {
525    use crate::engine::grammar::GrammarLoadError;
526    match error {
527        GrammarLoadError::Json { source, .. } => Error::Grammar(GrammarError::new(
528            None,
529            GrammarErrorKind::InvalidJson(JsonError::new(source)),
530        )),
531        GrammarLoadError::Validation { source, .. } => grammar_validation_error(*source),
532    }
533}
534
535pub(crate) fn grammar_validation_error(
536    error: crate::engine::grammar::GrammarValidationError,
537) -> Error {
538    let message = error.to_string();
539    let kind = match error.missing_include {
540        Some(target) => {
541            let (scope, repository) = *target;
542            GrammarErrorKind::MissingInclude(MissingInclude::new(scope, repository))
543        }
544        None => GrammarErrorKind::InvalidReference(message),
545    };
546    Error::Grammar(GrammarError::new(Some(error.grammar), kind))
547}
548
549#[cfg(test)]
550mod tests;