yash_syntax/parser/
case.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2020 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Syntax parser for case command
18
19use super::core::Parser;
20use super::core::Rec;
21use super::core::Result;
22use super::error::Error;
23use super::error::SyntaxError;
24use super::lex::Keyword::{Case, Esac, In};
25use super::lex::Operator::{Bar, CloseParen, Newline, OpenParen};
26use super::lex::TokenId::{self, EndOfInput, Operator, Token};
27use crate::syntax::CaseItem;
28use crate::syntax::CompoundCommand;
29
30impl Parser<'_, '_> {
31    /// Parses a case item.
32    ///
33    /// If the next token is `esac`, returns `None` without consuming it.
34    /// Otherwise, returns a case item and whether there may be a next item.
35    pub async fn case_item(&mut self) -> Result<Option<(CaseItem, bool)>> {
36        fn pattern_error_cause(token_id: TokenId) -> SyntaxError {
37            match token_id {
38                Token(_) => unreachable!(),
39                Operator(CloseParen) | Operator(Bar) | Operator(Newline) | EndOfInput => {
40                    SyntaxError::MissingPattern
41                }
42                _ => SyntaxError::InvalidPattern,
43            }
44        }
45
46        let first_token = loop {
47            while self.newline_and_here_doc_contents().await? {}
48
49            if self.peek_token().await?.id == Token(Some(Esac)) {
50                return Ok(None);
51            }
52
53            match self.take_token_manual(false).await? {
54                Rec::AliasSubstituted => (),
55                Rec::Parsed(token) => break token,
56            }
57        };
58
59        let first_pattern = match first_token.id {
60            Token(_) => first_token.word,
61            Operator(OpenParen) => {
62                let next_token = self.take_token_auto(&[Esac]).await?;
63                match next_token.id {
64                    Token(_) => next_token.word,
65                    _ => {
66                        let cause = pattern_error_cause(next_token.id).into();
67                        let location = next_token.word.location;
68                        return Err(Error { cause, location });
69                    }
70                }
71            }
72            _ => {
73                let cause = pattern_error_cause(first_token.id).into();
74                let location = first_token.word.location;
75                return Err(Error { cause, location });
76            }
77        };
78
79        let mut patterns = vec![first_pattern];
80        loop {
81            let separator = self.take_token_auto(&[]).await?;
82            match separator.id {
83                Operator(CloseParen) => break,
84                Operator(Bar) => {
85                    let pattern = self.take_token_auto(&[]).await?;
86                    match pattern.id {
87                        Token(_) => patterns.push(pattern.word),
88                        _ => {
89                            let cause = pattern_error_cause(pattern.id).into();
90                            let location = pattern.word.location;
91                            return Err(Error { cause, location });
92                        }
93                    }
94                }
95                _ => {
96                    let cause = SyntaxError::UnclosedPatternList.into();
97                    let location = separator.word.location;
98                    return Err(Error { cause, location });
99                }
100            }
101        }
102
103        let body = self.maybe_compound_list_boxed().await?;
104
105        let continuation = match self.peek_token().await?.id {
106            Operator(op) => op.try_into().ok(),
107            _ => None,
108        };
109        let continued = continuation.is_some();
110        let continuation = continuation.unwrap_or_default();
111        if continued {
112            self.take_token_raw().await?;
113            // TODO Reject Continue in strict POSIX mode
114        }
115
116        Ok(Some((
117            CaseItem {
118                patterns,
119                body,
120                continuation,
121            },
122            continued,
123        )))
124    }
125
126    /// Parses a case conditional construct.
127    ///
128    /// The next token must be the `case` reserved word.
129    ///
130    /// # Panics
131    ///
132    /// If the first token is not `case`.
133    pub async fn case_command(&mut self) -> Result<CompoundCommand> {
134        let open = self.take_token_raw().await?;
135        assert_eq!(open.id, Token(Some(Case)));
136
137        let subject = self.take_token_auto(&[]).await?;
138        match subject.id {
139            Token(_) => (),
140            Operator(Newline) | EndOfInput => {
141                let cause = SyntaxError::MissingCaseSubject.into();
142                let location = subject.word.location;
143                return Err(Error { cause, location });
144            }
145            _ => {
146                let cause = SyntaxError::InvalidCaseSubject.into();
147                let location = subject.word.location;
148                return Err(Error { cause, location });
149            }
150        }
151        let subject = subject.word;
152
153        loop {
154            while self.newline_and_here_doc_contents().await? {}
155
156            let next_token = self.take_token_auto(&[In]).await?;
157            match next_token.id {
158                Token(Some(In)) => break,
159                Operator(Newline) => (),
160                _ => {
161                    let opening_location = open.word.location;
162                    let cause = SyntaxError::MissingIn { opening_location }.into();
163                    let location = next_token.word.location;
164                    return Err(Error { cause, location });
165                }
166            }
167        }
168
169        let mut items = Vec::new();
170        while let Some((item, continued)) = self.case_item().await? {
171            items.push(item);
172            if !continued {
173                break;
174            }
175        }
176
177        let close = self.take_token_raw().await?;
178        if close.id != Token(Some(Esac)) {
179            let opening_location = open.word.location;
180            let cause = SyntaxError::UnclosedCase { opening_location }.into();
181            let location = close.word.location;
182            return Err(Error { cause, location });
183        }
184
185        Ok(CompoundCommand::Case { subject, items })
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::super::error::ErrorCause;
192    use super::super::lex::Lexer;
193    use super::*;
194    use crate::alias::{AliasSet, HashEntry};
195    use crate::source::Location;
196    use crate::source::Source;
197    use crate::syntax::CaseContinuation;
198    use assert_matches::assert_matches;
199    use futures_util::FutureExt;
200
201    #[test]
202    fn parser_case_item_esac() {
203        let mut lexer = Lexer::with_code("\nESAC");
204        #[allow(clippy::mutable_key_type)]
205        let mut aliases = AliasSet::new();
206        let origin = Location::dummy("");
207        aliases.insert(HashEntry::new(
208            "ESAC".to_string(),
209            "\n\nesac".to_string(),
210            true,
211            origin.clone(),
212        ));
213        aliases.insert(HashEntry::new(
214            "esac".to_string(),
215            "&&".to_string(),
216            true,
217            origin,
218        ));
219        let mut parser = Parser::config().aliases(&aliases).input(&mut lexer);
220
221        let option = parser.case_item().now_or_never().unwrap().unwrap();
222        assert_eq!(option, None);
223
224        let next = parser.peek_token().now_or_never().unwrap().unwrap();
225        assert_eq!(next.id, Token(Some(Esac)));
226    }
227
228    #[test]
229    fn parser_case_item_minimum() {
230        let mut lexer = Lexer::with_code("foo)");
231        let mut parser = Parser::new(&mut lexer);
232
233        let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap();
234        assert_eq!(item.patterns.len(), 1);
235        assert_eq!(item.patterns[0].to_string(), "foo");
236        assert_eq!(item.body.0, []);
237        assert_eq!(item.continuation, CaseContinuation::Break);
238        assert!(!continued);
239
240        let next = parser.peek_token().now_or_never().unwrap().unwrap();
241        assert_eq!(next.id, EndOfInput);
242    }
243
244    #[test]
245    fn parser_case_item_with_open_paren() {
246        let mut lexer = Lexer::with_code("(foo)");
247        let mut parser = Parser::new(&mut lexer);
248
249        let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap();
250        assert_eq!(item.patterns.len(), 1);
251        assert_eq!(item.patterns[0].to_string(), "foo");
252        assert_eq!(item.body.0, []);
253        assert_eq!(item.continuation, CaseContinuation::Break);
254        assert!(!continued);
255
256        let next = parser.peek_token().now_or_never().unwrap().unwrap();
257        assert_eq!(next.id, EndOfInput);
258    }
259
260    #[test]
261    fn parser_case_item_many_patterns() {
262        let mut lexer = Lexer::with_code("1 | esac | $three)");
263        let mut parser = Parser::new(&mut lexer);
264
265        let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap();
266        assert_eq!(item.patterns.len(), 3);
267        assert_eq!(item.patterns[0].to_string(), "1");
268        assert_eq!(item.patterns[1].to_string(), "esac");
269        assert_eq!(item.patterns[2].to_string(), "$three");
270        assert_eq!(item.body.0, []);
271        assert_eq!(item.continuation, CaseContinuation::Break);
272        assert!(!continued);
273
274        let next = parser.peek_token().now_or_never().unwrap().unwrap();
275        assert_eq!(next.id, EndOfInput);
276    }
277
278    #[test]
279    fn parser_case_item_non_empty_body() {
280        let mut lexer = Lexer::with_code("foo)\necho ok\n:&\n");
281        let mut parser = Parser::new(&mut lexer);
282
283        let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap();
284        assert_eq!(item.patterns.len(), 1);
285        assert_eq!(item.patterns[0].to_string(), "foo");
286        assert_eq!(item.body.0.len(), 2);
287        assert_eq!(item.body.0[0].to_string(), "echo ok");
288        assert_eq!(item.body.0[1].to_string(), ":&");
289        assert_eq!(item.continuation, CaseContinuation::Break);
290        assert!(!continued);
291
292        let next = parser.peek_token().now_or_never().unwrap().unwrap();
293        assert_eq!(next.id, EndOfInput);
294    }
295
296    #[test]
297    fn parser_case_item_with_double_semicolon() {
298        let mut lexer = Lexer::with_code("foo);;");
299        let mut parser = Parser::new(&mut lexer);
300
301        let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap();
302        assert_eq!(item.patterns.len(), 1);
303        assert_eq!(item.patterns[0].to_string(), "foo");
304        assert_eq!(item.body.0, []);
305        assert_eq!(item.continuation, CaseContinuation::Break);
306        assert!(continued);
307
308        let next = parser.peek_token().now_or_never().unwrap().unwrap();
309        assert_eq!(next.id, EndOfInput);
310    }
311
312    #[test]
313    fn parser_case_item_with_non_empty_body_and_double_semicolon() {
314        let mut lexer = Lexer::with_code("foo):;\n;;");
315        let mut parser = Parser::new(&mut lexer);
316
317        let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap();
318        assert_eq!(item.patterns.len(), 1);
319        assert_eq!(item.patterns[0].to_string(), "foo");
320        assert_eq!(item.body.0.len(), 1);
321        assert_eq!(item.body.0[0].to_string(), ":");
322        assert_eq!(item.continuation, CaseContinuation::Break);
323        assert!(continued);
324
325        let next = parser.peek_token().now_or_never().unwrap().unwrap();
326        assert_eq!(next.id, EndOfInput);
327    }
328
329    #[test]
330    fn parser_case_item_with_semicolon_and() {
331        let mut lexer = Lexer::with_code("foo);&");
332        let mut parser = Parser::new(&mut lexer);
333
334        let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap();
335        assert_eq!(item.patterns.len(), 1);
336        assert_eq!(item.patterns[0].to_string(), "foo");
337        assert_eq!(item.body.0, []);
338        assert_eq!(item.continuation, CaseContinuation::FallThrough);
339        assert!(continued);
340
341        let next = parser.peek_token().now_or_never().unwrap().unwrap();
342        assert_eq!(next.id, EndOfInput);
343    }
344
345    #[test]
346    fn parser_case_item_missing_pattern_without_open_paren() {
347        let mut lexer = Lexer::with_code(")");
348        let mut parser = Parser::new(&mut lexer);
349
350        let e = parser.case_item().now_or_never().unwrap().unwrap_err();
351        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::MissingPattern));
352        assert_eq!(*e.location.code.value.borrow(), ")");
353        assert_eq!(e.location.code.start_line_number.get(), 1);
354        assert_eq!(*e.location.code.source, Source::Unknown);
355        assert_eq!(e.location.range, 0..1);
356    }
357
358    #[test]
359    fn parser_case_item_esac_after_paren() {
360        let mut lexer = Lexer::with_code("(esac)");
361        let mut parser = Parser::new(&mut lexer);
362
363        let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap();
364        assert_eq!(item.patterns.len(), 1);
365        assert_eq!(item.patterns[0].to_string(), "esac");
366        assert_eq!(item.body.0, []);
367        assert_eq!(item.continuation, CaseContinuation::Break);
368        assert!(!continued);
369    }
370
371    #[test]
372    fn parser_case_item_first_pattern_not_word_after_open_paren() {
373        let mut lexer = Lexer::with_code("(&");
374        let mut parser = Parser::new(&mut lexer);
375
376        let e = parser.case_item().now_or_never().unwrap().unwrap_err();
377        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::InvalidPattern));
378        assert_eq!(*e.location.code.value.borrow(), "(&");
379        assert_eq!(e.location.code.start_line_number.get(), 1);
380        assert_eq!(*e.location.code.source, Source::Unknown);
381        assert_eq!(e.location.range, 1..2);
382    }
383
384    #[test]
385    fn parser_case_item_missing_pattern_after_bar() {
386        let mut lexer = Lexer::with_code("(foo| |");
387        let mut parser = Parser::new(&mut lexer);
388
389        let e = parser.case_item().now_or_never().unwrap().unwrap_err();
390        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::MissingPattern));
391        assert_eq!(*e.location.code.value.borrow(), "(foo| |");
392        assert_eq!(e.location.code.start_line_number.get(), 1);
393        assert_eq!(*e.location.code.source, Source::Unknown);
394        assert_eq!(e.location.range, 6..7);
395    }
396
397    #[test]
398    fn parser_case_item_missing_close_paren() {
399        let mut lexer = Lexer::with_code("(foo bar");
400        let mut parser = Parser::new(&mut lexer);
401
402        let e = parser.case_item().now_or_never().unwrap().unwrap_err();
403        assert_eq!(
404            e.cause,
405            ErrorCause::Syntax(SyntaxError::UnclosedPatternList)
406        );
407        assert_eq!(*e.location.code.value.borrow(), "(foo bar");
408        assert_eq!(e.location.code.start_line_number.get(), 1);
409        assert_eq!(*e.location.code.source, Source::Unknown);
410        assert_eq!(e.location.range, 5..8);
411    }
412
413    #[test]
414    fn parser_case_command_minimum() {
415        let mut lexer = Lexer::with_code("case foo in esac");
416        let mut parser = Parser::new(&mut lexer);
417
418        let result = parser.compound_command().now_or_never().unwrap();
419        let compound_command = result.unwrap().unwrap();
420        assert_matches!(compound_command, CompoundCommand::Case { subject, items } => {
421            assert_eq!(subject.to_string(), "foo");
422            assert_eq!(items, []);
423        });
424
425        let next = parser.peek_token().now_or_never().unwrap().unwrap();
426        assert_eq!(next.id, EndOfInput);
427    }
428
429    #[test]
430    fn parser_case_command_newline_before_in() {
431        // Alias substitution results in "case x \n\n \nin esac"
432        let mut lexer = Lexer::with_code("CASE_X IN_ESAC");
433        #[allow(clippy::mutable_key_type)]
434        let mut aliases = AliasSet::new();
435        let origin = Location::dummy("");
436        aliases.insert(HashEntry::new(
437            "CASE_X".to_string(),
438            " case x \n\n ".to_string(),
439            false,
440            origin.clone(),
441        ));
442        aliases.insert(HashEntry::new(
443            "IN_ESAC".to_string(),
444            "\nin esac".to_string(),
445            false,
446            origin,
447        ));
448        let mut parser = Parser::config().aliases(&aliases).input(&mut lexer);
449
450        let result = parser.take_token_manual(true).now_or_never().unwrap();
451        assert_matches!(result, Ok(Rec::AliasSubstituted));
452
453        let result = parser.compound_command().now_or_never().unwrap();
454        let compound_command = result.unwrap().unwrap();
455        assert_matches!(compound_command, CompoundCommand::Case { subject, items } => {
456            assert_eq!(subject.to_string(), "x");
457            assert_eq!(items, []);
458        });
459
460        let next = parser.peek_token().now_or_never().unwrap().unwrap();
461        assert_eq!(next.id, EndOfInput);
462    }
463
464    #[test]
465    fn parser_case_command_alias_on_subject() {
466        // Alias substitution results in " case   in in  a|b) esac"
467        let mut lexer = Lexer::with_code("CASE in a|b) esac");
468        #[allow(clippy::mutable_key_type)]
469        let mut aliases = AliasSet::new();
470        let origin = Location::dummy("");
471        aliases.insert(HashEntry::new(
472            "CASE".to_string(),
473            " case ".to_string(),
474            false,
475            origin.clone(),
476        ));
477        aliases.insert(HashEntry::new(
478            "in".to_string(),
479            " in in ".to_string(),
480            false,
481            origin,
482        ));
483        let mut parser = Parser::config().aliases(&aliases).input(&mut lexer);
484
485        let result = parser.take_token_manual(true).now_or_never().unwrap();
486        assert_matches!(result, Ok(Rec::AliasSubstituted));
487
488        let result = parser.compound_command().now_or_never().unwrap();
489        let compound_command = result.unwrap().unwrap();
490        assert_matches!(compound_command, CompoundCommand::Case { subject, items } => {
491            assert_eq!(subject.to_string(), "in");
492            assert_eq!(items.len(), 1);
493            assert_eq!(items[0].to_string(), "(a | b) ;;");
494        });
495
496        let next = parser.peek_token().now_or_never().unwrap().unwrap();
497        assert_eq!(next.id, EndOfInput);
498    }
499
500    #[test]
501    fn parser_case_command_alias_on_in() {
502        // Alias substitution results in "case x  in esac"
503        let mut lexer = Lexer::with_code("CASE_X in esac");
504        #[allow(clippy::mutable_key_type)]
505        let mut aliases = AliasSet::new();
506        let origin = Location::dummy("");
507        aliases.insert(HashEntry::new(
508            "CASE_X".to_string(),
509            "case x ".to_string(),
510            false,
511            origin.clone(),
512        ));
513        aliases.insert(HashEntry::new(
514            "in".to_string(),
515            "in a)".to_string(),
516            false,
517            origin,
518        ));
519        let mut parser = Parser::config().aliases(&aliases).input(&mut lexer);
520
521        let result = parser.take_token_manual(true).now_or_never().unwrap();
522        assert_matches!(result, Ok(Rec::AliasSubstituted));
523
524        let result = parser.compound_command().now_or_never().unwrap();
525        let compound_command = result.unwrap().unwrap();
526        assert_matches!(compound_command, CompoundCommand::Case { subject, items } => {
527            assert_eq!(subject.to_string(), "x");
528            assert_eq!(items, []);
529        });
530
531        let next = parser.peek_token().now_or_never().unwrap().unwrap();
532        assert_eq!(next.id, EndOfInput);
533    }
534
535    #[test]
536    fn parser_case_command_one_item() {
537        let mut lexer = Lexer::with_code("case foo in bar) esac");
538        let mut parser = Parser::new(&mut lexer);
539
540        let result = parser.compound_command().now_or_never().unwrap();
541        let compound_command = result.unwrap().unwrap();
542        assert_matches!(compound_command, CompoundCommand::Case { subject, items } => {
543            assert_eq!(subject.to_string(), "foo");
544            assert_eq!(items.len(), 1);
545            assert_eq!(items[0].to_string(), "(bar) ;;");
546        });
547
548        let next = parser.peek_token().now_or_never().unwrap().unwrap();
549        assert_eq!(next.id, EndOfInput);
550    }
551
552    #[test]
553    fn parser_case_command_many_items_without_final_double_semicolon() {
554        let mut lexer = Lexer::with_code("case x in\n\na) ;; (b|c):&:; ;;\n d)echo\nesac");
555        let mut parser = Parser::new(&mut lexer);
556
557        let result = parser.compound_command().now_or_never().unwrap();
558        let compound_command = result.unwrap().unwrap();
559        assert_matches!(compound_command, CompoundCommand::Case { subject, items } => {
560            assert_eq!(subject.to_string(), "x");
561            assert_eq!(items.len(), 3);
562            assert_eq!(items[0].to_string(), "(a) ;;");
563            assert_eq!(items[1].to_string(), "(b | c) :& :;;");
564            assert_eq!(items[2].to_string(), "(d) echo;;");
565        });
566
567        let next = parser.peek_token().now_or_never().unwrap().unwrap();
568        assert_eq!(next.id, EndOfInput);
569    }
570
571    #[test]
572    fn parser_case_command_many_items_with_final_double_semicolon() {
573        let mut lexer = Lexer::with_code("case x in(1);; 2)echo\n\n;;\n\nesac");
574        let mut parser = Parser::new(&mut lexer);
575
576        let result = parser.compound_command().now_or_never().unwrap();
577        let compound_command = result.unwrap().unwrap();
578        assert_matches!(compound_command, CompoundCommand::Case { subject, items } => {
579            assert_eq!(subject.to_string(), "x");
580            assert_eq!(items.len(), 2);
581            assert_eq!(items[0].to_string(), "(1) ;;");
582            assert_eq!(items[1].to_string(), "(2) echo;;");
583        });
584
585        let next = parser.peek_token().now_or_never().unwrap().unwrap();
586        assert_eq!(next.id, EndOfInput);
587    }
588
589    #[test]
590    fn parser_case_command_missing_subject() {
591        let mut lexer = Lexer::with_code(" case  ");
592        let mut parser = Parser::new(&mut lexer);
593
594        let result = parser.compound_command().now_or_never().unwrap();
595        let e = result.unwrap_err();
596        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::MissingCaseSubject));
597        assert_eq!(*e.location.code.value.borrow(), " case  ");
598        assert_eq!(e.location.code.start_line_number.get(), 1);
599        assert_eq!(*e.location.code.source, Source::Unknown);
600        assert_eq!(e.location.range, 7..7);
601    }
602
603    #[test]
604    fn parser_case_command_invalid_subject() {
605        let mut lexer = Lexer::with_code(" case ; ");
606        let mut parser = Parser::new(&mut lexer);
607
608        let result = parser.compound_command().now_or_never().unwrap();
609        let e = result.unwrap_err();
610        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::InvalidCaseSubject));
611        assert_eq!(*e.location.code.value.borrow(), " case ; ");
612        assert_eq!(e.location.code.start_line_number.get(), 1);
613        assert_eq!(*e.location.code.source, Source::Unknown);
614        assert_eq!(e.location.range, 6..7);
615    }
616
617    #[test]
618    fn parser_case_command_missing_in() {
619        let mut lexer = Lexer::with_code(" case x esac");
620        let mut parser = Parser::new(&mut lexer);
621
622        let result = parser.compound_command().now_or_never().unwrap();
623        let e = result.unwrap_err();
624        assert_matches!(e.cause,
625            ErrorCause::Syntax(SyntaxError::MissingIn { opening_location }) => {
626            assert_eq!(*opening_location.code.value.borrow(), " case x esac");
627            assert_eq!(opening_location.code.start_line_number.get(), 1);
628            assert_eq!(*opening_location.code.source, Source::Unknown);
629            assert_eq!(opening_location.range, 1..5);
630        });
631        assert_eq!(*e.location.code.value.borrow(), " case x esac");
632        assert_eq!(e.location.code.start_line_number.get(), 1);
633        assert_eq!(*e.location.code.source, Source::Unknown);
634        assert_eq!(e.location.range, 8..12);
635    }
636
637    #[test]
638    fn parser_case_command_missing_esac() {
639        let mut lexer = Lexer::with_code("case x in a) }");
640        let mut parser = Parser::new(&mut lexer);
641
642        let result = parser.compound_command().now_or_never().unwrap();
643        let e = result.unwrap_err();
644        assert_matches!(e.cause,
645            ErrorCause::Syntax(SyntaxError::UnclosedCase { opening_location }) => {
646            assert_eq!(*opening_location.code.value.borrow(), "case x in a) }");
647            assert_eq!(opening_location.code.start_line_number.get(), 1);
648            assert_eq!(*opening_location.code.source, Source::Unknown);
649            assert_eq!(opening_location.range, 0..4);
650        });
651        assert_eq!(*e.location.code.value.borrow(), "case x in a) }");
652        assert_eq!(e.location.code.start_line_number.get(), 1);
653        assert_eq!(*e.location.code.source, Source::Unknown);
654        assert_eq!(e.location.range, 13..14);
655    }
656}