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}