Skip to content

Commit d6e317b

Browse files
Move f-string parser into own module and clean up a bit
1 parent 098675d commit d6e317b

File tree

4 files changed

+207
-223
lines changed

4 files changed

+207
-223
lines changed

parser/src/fstring.rs

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
use std::iter;
2+
use std::mem;
3+
use std::str;
4+
5+
use lalrpop_util::ParseError as LalrpopError;
6+
7+
use crate::ast::StringGroup;
8+
use crate::lexer::{LexicalError, Location, Tok};
9+
use crate::parser::parse_expression;
10+
11+
use self::StringGroup::*;
12+
13+
// TODO: consolidate these with ParseError
14+
#[derive(Debug, PartialEq)]
15+
pub enum FStringError {
16+
UnclosedLbrace,
17+
UnopenedRbrace,
18+
InvalidExpression,
19+
}
20+
21+
impl From<FStringError> for LalrpopError<Location, Tok, LexicalError> {
22+
fn from(_err: FStringError) -> Self {
23+
lalrpop_util::ParseError::User {
24+
error: LexicalError::StringError,
25+
}
26+
}
27+
}
28+
29+
struct FStringParser<'a> {
30+
chars: iter::Peekable<str::Chars<'a>>,
31+
}
32+
33+
impl<'a> FStringParser<'a> {
34+
fn new(source: &'a str) -> Self {
35+
Self {
36+
chars: source.chars().peekable(),
37+
}
38+
}
39+
40+
fn parse_formatted_value(&mut self) -> Result<StringGroup, FStringError> {
41+
let mut expression = String::new();
42+
let mut spec = String::new();
43+
let mut depth = 0;
44+
45+
while let Some(ch) = self.chars.next() {
46+
match ch {
47+
':' if depth == 0 => {
48+
while let Some(&next) = self.chars.peek() {
49+
if next != '}' {
50+
spec.push(next);
51+
self.chars.next();
52+
} else {
53+
break;
54+
}
55+
}
56+
}
57+
'{' => {
58+
if let Some('{') = self.chars.peek() {
59+
expression.push_str("{{");
60+
self.chars.next();
61+
} else {
62+
expression.push('{');
63+
depth += 1;
64+
}
65+
}
66+
'}' => {
67+
if let Some('}') = self.chars.peek() {
68+
expression.push_str("}}");
69+
self.chars.next();
70+
} else if depth > 0 {
71+
expression.push('}');
72+
depth -= 1;
73+
} else {
74+
return Ok(FormattedValue {
75+
value: Box::new(
76+
parse_expression(expression.trim())
77+
.map_err(|_| FStringError::InvalidExpression)?,
78+
),
79+
spec,
80+
});
81+
}
82+
}
83+
_ => {
84+
expression.push(ch);
85+
}
86+
}
87+
}
88+
89+
return Err(FStringError::UnclosedLbrace);
90+
}
91+
92+
fn parse(mut self) -> Result<StringGroup, FStringError> {
93+
let mut content = String::new();
94+
let mut values = vec![];
95+
96+
while let Some(ch) = self.chars.next() {
97+
match ch {
98+
'{' => {
99+
if let Some('{') = self.chars.peek() {
100+
self.chars.next();
101+
content.push('{');
102+
} else {
103+
if !content.is_empty() {
104+
values.push(Constant {
105+
value: mem::replace(&mut content, String::new()),
106+
});
107+
}
108+
109+
values.push(self.parse_formatted_value()?);
110+
}
111+
}
112+
'}' => {
113+
if let Some('}') = self.chars.peek() {
114+
self.chars.next();
115+
content.push('}');
116+
} else {
117+
return Err(FStringError::UnopenedRbrace);
118+
}
119+
}
120+
_ => {
121+
content.push(ch);
122+
}
123+
}
124+
}
125+
126+
if !content.is_empty() {
127+
values.push(Constant { value: content })
128+
}
129+
130+
Ok(match values.len() {
131+
0 => Constant {
132+
value: String::new(),
133+
},
134+
1 => values.into_iter().next().unwrap(),
135+
_ => Joined { values },
136+
})
137+
}
138+
}
139+
140+
pub fn parse_fstring(source: &str) -> Result<StringGroup, FStringError> {
141+
FStringParser::new(source).parse()
142+
}
143+
144+
#[cfg(test)]
145+
mod tests {
146+
use crate::ast;
147+
148+
use super::*;
149+
150+
fn mk_ident(name: &str) -> ast::Expression {
151+
ast::Expression::Identifier {
152+
name: name.to_owned(),
153+
}
154+
}
155+
156+
#[test]
157+
fn test_parse_fstring() {
158+
let source = String::from("{a}{ b }{{foo}}");
159+
let parse_ast = parse_fstring(&source).unwrap();
160+
161+
assert_eq!(
162+
parse_ast,
163+
Joined {
164+
values: vec![
165+
FormattedValue {
166+
value: Box::new(mk_ident("a")),
167+
spec: String::new(),
168+
},
169+
FormattedValue {
170+
value: Box::new(mk_ident("b")),
171+
spec: String::new(),
172+
},
173+
Constant {
174+
value: "{foo}".to_owned()
175+
}
176+
]
177+
}
178+
);
179+
}
180+
181+
#[test]
182+
fn test_parse_empty_fstring() {
183+
assert_eq!(
184+
parse_fstring(""),
185+
Ok(Constant {
186+
value: String::new(),
187+
}),
188+
);
189+
}
190+
191+
#[test]
192+
fn test_parse_invalid_fstring() {
193+
assert_eq!(parse_fstring("{"), Err(FStringError::UnclosedLbrace));
194+
assert_eq!(parse_fstring("}"), Err(FStringError::UnopenedRbrace));
195+
assert_eq!(
196+
parse_fstring("{class}"),
197+
Err(FStringError::InvalidExpression)
198+
);
199+
}
200+
}

parser/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ extern crate log;
33

44
pub mod ast;
55
pub mod error;
6+
mod fstring;
67
pub mod lexer;
78
pub mod parser;
89
#[cfg_attr(rustfmt, rustfmt_skip)]

0 commit comments

Comments
 (0)