yash_syntax/parser/lex/
arith.rs1use 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 pub async fn arithmetic_expansion(&mut self, start_index: usize) -> Result<Option<TextUnit>> {
39 let orig_index = self.index();
40
41 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 let is_delimiter = |c| c == ')';
54 let is_escapable = |c| matches!(c, '$' | '`' | '\\');
55 let content = Box::pin(self.text_with_parentheses(is_delimiter, is_escapable)).await?;
57
58 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}