yash_syntax/parser/lex/
arith.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//! Part of the lexer that parses arithmetic expansions
18
19use super::core::Lexer;
20use crate::parser::core::Result;
21use crate::parser::error::Error;
22use crate::parser::error::SyntaxError;
23use crate::syntax::TextUnit;
24
25impl Lexer<'_> {
26    /// Parses an arithmetic expansion.
27    ///
28    /// The initial `$` must have been consumed before calling this function.
29    /// In this function, the next two characters are examined to see if they
30    /// begin an arithmetic expansion. If the characters are `((`, then the
31    /// arithmetic expansion is parsed, in which case this function consumes up
32    /// to the closing `))` (inclusive). Otherwise, no characters are consumed
33    /// and the return value is `Ok(None)`.
34    ///
35    /// The `start_index` parameter should be the index for the initial `$`. It is
36    /// used to construct the result, but this function does not check if it
37    /// actually points to the `$`.
38    pub async fn arithmetic_expansion(&mut self, start_index: usize) -> Result<Option<TextUnit>> {
39        let orig_index = self.index();
40
41        // Part 1: Parse `((`
42        if !self.skip_if(|c| c == '(').await? {
43            return Ok(None);
44        }
45        if !self.skip_if(|c| c == '(').await? {
46            self.rewind(orig_index);
47            return Ok(None);
48        }
49
50        let opening_location = self.location_range(start_index..self.index());
51
52        // Part 2: Parse the content
53        let is_delimiter = |c| c == ')';
54        let is_escapable = |c| matches!(c, '$' | '`' | '\\');
55        // Boxing needed for recursion
56        let content = Box::pin(self.text_with_parentheses(is_delimiter, is_escapable)).await?;
57
58        // Part 3: Parse `))`
59        match self.peek_char().await? {
60            Some(')') => self.consume_char(),
61            Some(_) => unreachable!(),
62            None => {
63                let cause = SyntaxError::UnclosedArith { opening_location }.into();
64                let location = self.location().await?.clone();
65                return Err(Error { cause, location });
66            }
67        }
68        match self.peek_char().await? {
69            Some(')') => self.consume_char(),
70            Some(_) => {
71                self.rewind(orig_index);
72                return Ok(None);
73            }
74            None => {
75                let cause = SyntaxError::UnclosedArith { opening_location }.into();
76                let location = self.location().await?.clone();
77                return Err(Error { cause, location });
78            }
79        }
80
81        let location = self.location_range(start_index..self.index());
82        Ok(Some(TextUnit::Arith { content, location }))
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use crate::parser::error::ErrorCause;
90    use crate::source::Source;
91    use crate::syntax::Backslashed;
92    use crate::syntax::Literal;
93    use assert_matches::assert_matches;
94    use futures_util::FutureExt;
95
96    #[test]
97    fn lexer_arithmetic_expansion_empty() {
98        let mut lexer = Lexer::with_code("$(());");
99        lexer.peek_char().now_or_never().unwrap().unwrap();
100        lexer.consume_char();
101
102        let result = lexer.arithmetic_expansion(0).now_or_never().unwrap();
103        let text_unit = result.unwrap().unwrap();
104        assert_matches!(text_unit, TextUnit::Arith { content, location } => {
105            assert_eq!(content.0, []);
106            assert_eq!(*location.code.value.borrow(), "$(());");
107            assert_eq!(location.code.start_line_number.get(), 1);
108            assert_eq!(*location.code.source, Source::Unknown);
109            assert_eq!(location.range, 0..5);
110        });
111
112        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(Some(';')));
113    }
114
115    #[test]
116    fn lexer_arithmetic_expansion_none() {
117        let mut lexer = Lexer::with_code("$( foo bar )baz");
118        lexer.peek_char().now_or_never().unwrap().unwrap();
119        lexer.consume_char();
120        assert_eq!(
121            lexer.arithmetic_expansion(0).now_or_never().unwrap(),
122            Ok(None)
123        );
124        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(Some('(')));
125    }
126
127    #[test]
128    fn lexer_arithmetic_expansion_line_continuations() {
129        let mut lexer = Lexer::with_code("$(\\\n\\\n(\\\n)\\\n\\\n);");
130        lexer.peek_char().now_or_never().unwrap().unwrap();
131        lexer.consume_char();
132
133        let result = lexer.arithmetic_expansion(0).now_or_never().unwrap();
134        let text_unit = result.unwrap().unwrap();
135        assert_matches!(text_unit, TextUnit::Arith { content, location } => {
136            assert_eq!(content.0, []);
137            assert_eq!(*location.code.value.borrow(), "$(\\\n\\\n(\\\n)\\\n\\\n);");
138            assert_eq!(location.code.start_line_number.get(), 1);
139            assert_eq!(*location.code.source, Source::Unknown);
140            assert_eq!(location.range, 0..15);
141        });
142
143        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(Some(';')));
144    }
145
146    #[test]
147    fn lexer_arithmetic_expansion_escapes() {
148        let mut lexer = Lexer::with_code(r#".$((\\\"\`\$));"#);
149        lexer.peek_char().now_or_never().unwrap().unwrap();
150        lexer.consume_char();
151        lexer.peek_char().now_or_never().unwrap().unwrap();
152        lexer.consume_char();
153
154        let result = lexer.arithmetic_expansion(1).now_or_never().unwrap();
155        let text_unit = result.unwrap().unwrap();
156        assert_matches!(text_unit, TextUnit::Arith { content, location } => {
157            assert_eq!(
158                content.0,
159                [
160                    Backslashed('\\'),
161                    Literal('\\'),
162                    Literal('"'),
163                    Backslashed('`'),
164                    Backslashed('$')
165                ]
166            );
167            assert_eq!(*location.code.value.borrow(), r#".$((\\\"\`\$));"#);
168            assert_eq!(location.code.start_line_number.get(), 1);
169            assert_eq!(*location.code.source, Source::Unknown);
170            assert_eq!(location.range, 1..14);
171        });
172
173        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(Some(';')));
174    }
175
176    #[test]
177    fn lexer_arithmetic_expansion_unclosed_first() {
178        let mut lexer = Lexer::with_code("$((1");
179        lexer.peek_char().now_or_never().unwrap().unwrap();
180        lexer.consume_char();
181
182        let result = lexer.arithmetic_expansion(0).now_or_never().unwrap();
183        let e = result.unwrap_err();
184        assert_matches!(e.cause,
185            ErrorCause::Syntax(SyntaxError::UnclosedArith { opening_location }) => {
186            assert_eq!(*opening_location.code.value.borrow(), "$((1");
187            assert_eq!(opening_location.code.start_line_number.get(), 1);
188            assert_eq!(*opening_location.code.source, Source::Unknown);
189            assert_eq!(opening_location.range, 0..3);
190        });
191        assert_eq!(*e.location.code.value.borrow(), "$((1");
192        assert_eq!(e.location.code.start_line_number.get(), 1);
193        assert_eq!(*e.location.code.source, Source::Unknown);
194        assert_eq!(e.location.range, 4..4);
195    }
196
197    #[test]
198    fn lexer_arithmetic_expansion_unclosed_second() {
199        let mut lexer = Lexer::with_code("$((1)");
200        lexer.peek_char().now_or_never().unwrap().unwrap();
201        lexer.consume_char();
202
203        let result = lexer.arithmetic_expansion(0).now_or_never().unwrap();
204        let e = result.unwrap_err();
205        assert_matches!(e.cause,
206            ErrorCause::Syntax(SyntaxError::UnclosedArith { opening_location }) => {
207            assert_eq!(*opening_location.code.value.borrow(), "$((1)");
208            assert_eq!(opening_location.code.start_line_number.get(), 1);
209            assert_eq!(*opening_location.code.source, Source::Unknown);
210            assert_eq!(opening_location.range, 0..3);
211        });
212        assert_eq!(*e.location.code.value.borrow(), "$((1)");
213        assert_eq!(e.location.code.start_line_number.get(), 1);
214        assert_eq!(*e.location.code.source, Source::Unknown);
215        assert_eq!(e.location.range, 5..5);
216    }
217
218    #[test]
219    fn lexer_arithmetic_expansion_unclosed_but_maybe_command_substitution() {
220        let mut lexer = Lexer::with_code("$((1) ");
221        lexer.peek_char().now_or_never().unwrap().unwrap();
222        lexer.consume_char();
223        assert_eq!(
224            lexer.arithmetic_expansion(0).now_or_never().unwrap(),
225            Ok(None)
226        );
227        assert_eq!(lexer.index(), 1);
228    }
229}