Skip to main content

textmate_lang/
error.rs

1//! Why a grammar could not be emitted.
2
3use alloc::{string::String, vec::Vec};
4use core::fmt;
5
6use crate::RegexError;
7
8/// Why [`Grammar`](crate::Grammar) refused to emit a grammar.
9///
10/// Emitting validates the whole grammar first and reports the first problem.
11/// Every variant that has a `path` locates the problem with a
12/// [JSON Pointer](https://www.rfc-editor.org/rfc/rfc6901) into the JSON that
13/// would have been emitted: `/patterns/2/begin`, `/repository/string/captures/1/name`.
14/// Repository and injection entries are addressed by name, so a path is the
15/// same whatever order the entries were added in.
16///
17/// The checks run in this order: the grammar's `scopeName`, `fileTypes`,
18/// `firstLineMatch`, and `injectionSelector`; repository names and injection
19/// selectors (and their uniqueness); then every rule — the top-level
20/// patterns, the repository in name order, the injections in selector order
21/// — each checked before the rules nested in it; finally include cycles.
22/// Within a rule, its regular expressions are checked first, then its scope
23/// names, then its captures.
24///
25/// # Examples
26///
27/// ```
28/// use textmate_lang::{Error, Grammar, Rule};
29///
30/// let grammar = Grammar::new("source.demo").pattern(Rule::include("strings"));
31/// match grammar.to_json() {
32///     Err(Error::UndefinedInclude { path, name }) => {
33///         assert_eq!(path, "/patterns/0/include");
34///         assert_eq!(name, "strings");
35///     }
36///     other => panic!("unexpected {other:?}"),
37/// }
38/// ```
39#[derive(Clone, Debug, PartialEq, Eq)]
40#[non_exhaustive]
41pub enum Error {
42    /// A regular expression is outside the supported Oniguruma subset; see
43    /// [`validate_regex`](crate::validate_regex) and
44    /// [`RegexErrorKind`](crate::RegexErrorKind) for the fix.
45    InvalidRegex {
46        /// Where the pattern is.
47        path: String,
48        /// The byte offset in the pattern and what is wrong there.
49        error: RegexError,
50    },
51
52    /// A `match`, `begin`, `end`, `while`, or `firstLineMatch` pattern is
53    /// empty. An empty pattern matches everywhere without consuming text,
54    /// which tokenizers either loop on or detect and skip.
55    ///
56    /// Give the pattern something to match; to end a region at once, use a
57    /// look-ahead such as `(?=.)` or `$`.
58    EmptyRegex {
59        /// Where the pattern is.
60        path: String,
61    },
62
63    /// A scope name is malformed: empty, with an empty segment, with a
64    /// character outside ASCII letters, digits, `_`, `+`, and `-`, with a
65    /// malformed `$n` substitution, or with a substitution where none is
66    /// allowed (`scopeName`, external includes).
67    ///
68    /// Scopes are dot-separated segments such as `keyword.control.demo`; a
69    /// rule's name may list several separated by single spaces.
70    InvalidScope {
71        /// Where the scope is.
72        path: String,
73        /// The scope as given.
74        scope: String,
75        /// Byte offset of the problem in `scope`.
76        offset: usize,
77        /// What is wrong.
78        reason: &'static str,
79    },
80
81    /// A scope selector (`injectionSelector` or an `injections` key) is
82    /// malformed, in the syntax VS Code's `vscode-textmate` reads: scopes
83    /// combined with `,`, `|` (inside parentheses), `-`, and `(...)`, each
84    /// top-level entry optionally prefixed with `L:` or `R:`.
85    InvalidSelector {
86        /// Where the selector is.
87        path: String,
88        /// The selector as given.
89        selector: String,
90        /// Byte offset of the problem in `selector`.
91        offset: usize,
92        /// What is wrong.
93        reason: &'static str,
94    },
95
96    /// A repository name (defined or included) or a file type is
97    /// malformed. Repository names are non-empty ASCII letters, digits, `_`,
98    /// `-`, and `.`; file types are non-empty.
99    InvalidName {
100        /// Where the name is.
101        path: String,
102        /// The name as given.
103        name: String,
104        /// What is wrong.
105        reason: &'static str,
106    },
107
108    /// Two repository entries share a name. The JSON object could hold only
109    /// one, so the second would silently replace the first.
110    DuplicateRepository {
111        /// The name defined more than once.
112        name: String,
113    },
114
115    /// Two injections share a selector.
116    DuplicateInjection {
117        /// The selector given more than once.
118        selector: String,
119    },
120
121    /// A captures map sets the same group twice.
122    DuplicateCapture {
123        /// The captures map.
124        path: String,
125        /// The repeated group number.
126        group: u32,
127    },
128
129    /// A capture or a `$n` substitution names a group the pattern does not
130    /// have. For `captures` on a region, the group must exist in every
131    /// pattern the captures apply to (`begin`, and `end` or `while`, unless
132    /// those have their own captures); for a region's `name` and
133    /// `contentName`, in `begin`.
134    CaptureOutOfRange {
135        /// Where the capture or the substitution is.
136        path: String,
137        /// The group named.
138        group: u32,
139        /// How many groups the pattern has.
140        groups: u32,
141    },
142
143    /// A region's `captures` can never apply: the region has both
144    /// `beginCaptures` and `endCaptures` (or `whileCaptures`), which take
145    /// precedence. Move the captures into those, or remove them.
146    ShadowedCaptures {
147        /// Where the region is.
148        path: String,
149    },
150
151    /// An include names a repository entry this grammar does not define.
152    UndefinedInclude {
153        /// Where the include is.
154        path: String,
155        /// The entry it names.
156        name: String,
157    },
158
159    /// Includes form a cycle in which no rule consumes text: an entry that
160    /// is only a list of patterns (or one include) includes itself through
161    /// other such entries, or through `$self` / `$base` from the top-level
162    /// patterns. Tokenizers expand these lists when building their scanners
163    /// and recurse forever: `vscode-textmate` collects them with a recursion
164    /// that keeps no record of lists already visited.
165    ///
166    /// Recursion through a region is fine — a `begin`/`end` rule whose
167    /// patterns include the entry that contains it is how nested
168    /// constructs are written — and so is recursion through captures.
169    ///
170    /// `cycle` lists the entries on the cycle, `$self` for the top-level
171    /// patterns and `#name` for repository entries, starting and ending with
172    /// the same one.
173    IncludeCycle {
174        /// The entries on the cycle.
175        cycle: Vec<String>,
176    },
177
178    /// Rules nest more than [`MAX_RULE_DEPTH`](crate::MAX_RULE_DEPTH) deep
179    /// (regions and captures inside regions and captures). Editors build
180    /// their rule tables recursively, so a deeper grammar can crash them;
181    /// use repository entries and includes instead.
182    TooDeep {
183        /// Where the limit was crossed.
184        path: String,
185    },
186}
187
188impl fmt::Display for Error {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        match self {
191            Self::InvalidRegex { path, error } => {
192                write!(f, "invalid regular expression at `{path}`, {error}")
193            }
194            Self::EmptyRegex { path } => write!(f, "empty regular expression at `{path}`"),
195            Self::InvalidScope {
196                path,
197                scope,
198                offset,
199                reason,
200            } => write!(
201                f,
202                "invalid scope name {scope:?} at `{path}`, byte {offset}: {reason}"
203            ),
204            Self::InvalidSelector {
205                path,
206                selector,
207                offset,
208                reason,
209            } => write!(
210                f,
211                "invalid scope selector {selector:?} at `{path}`, byte {offset}: {reason}"
212            ),
213            Self::InvalidName { path, name, reason } => {
214                write!(f, "invalid name {name:?} at `{path}`: {reason}")
215            }
216            Self::DuplicateRepository { name } => {
217                write!(f, "repository entry {name:?} is defined more than once")
218            }
219            Self::DuplicateInjection { selector } => {
220                write!(f, "injection selector {selector:?} is given more than once")
221            }
222            Self::DuplicateCapture { path, group } => {
223                write!(f, "capture group {group} is set more than once at `{path}`")
224            }
225            Self::CaptureOutOfRange {
226                path,
227                group,
228                groups,
229            } => write!(
230                f,
231                "`{path}` refers to capture group {group}, but the highest group in the pattern is {groups}"
232            ),
233            Self::ShadowedCaptures { path } => write!(
234                f,
235                "`{path}/captures` never applies: both sides of the region have their own captures"
236            ),
237            Self::UndefinedInclude { path, name } => {
238                write!(
239                    f,
240                    "`{path}` includes repository entry {name:?}, which is not defined"
241                )
242            }
243            Self::IncludeCycle { cycle } => {
244                f.write_str("includes form a cycle that consumes no text: ")?;
245                for (i, entry) in cycle.iter().enumerate() {
246                    if i > 0 {
247                        f.write_str(" -> ")?;
248                    }
249                    f.write_str(entry)?;
250                }
251                Ok(())
252            }
253            Self::TooDeep { path } => write!(
254                f,
255                "rules nest more than {} deep at `{path}`",
256                crate::MAX_RULE_DEPTH
257            ),
258        }
259    }
260}
261
262impl core::error::Error for Error {
263    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
264        match self {
265            Self::InvalidRegex { error, .. } => Some(error),
266            _ => None,
267        }
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use alloc::{string::ToString, vec};
274
275    use super::*;
276    use crate::RegexErrorKind;
277
278    #[test]
279    fn test_every_variant_displays_its_details() {
280        let p = || String::from("/p");
281        let all = [
282            Error::InvalidRegex {
283                path: p(),
284                error: RegexError {
285                    offset: 3,
286                    kind: RegexErrorKind::UnclosedGroup,
287                },
288            },
289            Error::EmptyRegex { path: p() },
290            Error::InvalidScope {
291                path: p(),
292                scope: "a..b".into(),
293                offset: 2,
294                reason: "r",
295            },
296            Error::InvalidSelector {
297                path: p(),
298                selector: "(".into(),
299                offset: 0,
300                reason: "r",
301            },
302            Error::InvalidName {
303                path: p(),
304                name: "a b".into(),
305                reason: "r",
306            },
307            Error::DuplicateCapture {
308                path: p(),
309                group: 1,
310            },
311            Error::CaptureOutOfRange {
312                path: p(),
313                group: 3,
314                groups: 2,
315            },
316            Error::ShadowedCaptures { path: p() },
317            Error::UndefinedInclude {
318                path: p(),
319                name: "x".into(),
320            },
321            Error::TooDeep { path: p() },
322        ];
323        for e in all {
324            assert!(e.to_string().contains("/p"), "{e}");
325        }
326        assert!(
327            Error::DuplicateRepository { name: "s".into() }
328                .to_string()
329                .contains("\"s\"")
330        );
331        assert!(
332            Error::DuplicateInjection {
333                selector: "L:x".into()
334            }
335            .to_string()
336            .contains("L:x")
337        );
338        let cycle = Error::IncludeCycle {
339            cycle: vec!["#a".into(), "#b".into(), "#a".into()],
340        };
341        assert!(cycle.to_string().ends_with("#a -> #b -> #a"));
342    }
343
344    #[test]
345    fn test_regex_error_is_the_source() {
346        use core::error::Error as _;
347        let e = Error::InvalidRegex {
348            path: "/x".into(),
349            error: RegexError {
350                offset: 0,
351                kind: RegexErrorKind::TrailingBackslash,
352            },
353        };
354        assert!(e.source().is_some());
355        assert!(Error::EmptyRegex { path: "/x".into() }.source().is_none());
356        assert_eq!(
357            e.to_string(),
358            "invalid regular expression at `/x`, at byte 0: the pattern ends with a lone `\\`"
359        );
360    }
361}