rnix/ast/
str_util.rs

1use crate::kinds::SyntaxKind::*;
2use rowan::{ast::AstNode as OtherAstNode, NodeOrToken};
3
4use crate::ast;
5
6use super::{support::children_tokens_u, AstToken, InterpolPart, StrContent};
7
8impl ast::Str {
9    pub fn parts(&self) -> impl Iterator<Item = InterpolPart<StrContent>> {
10        self.syntax().children_with_tokens().filter_map(|child| match child {
11            NodeOrToken::Token(token) if token.kind() == TOKEN_STRING_CONTENT => {
12                Some(InterpolPart::Literal(StrContent::cast(token).unwrap()))
13            }
14            NodeOrToken::Token(token) => {
15                assert!(token.kind() == TOKEN_STRING_START || token.kind() == TOKEN_STRING_END);
16                None
17            }
18            NodeOrToken::Node(node) => {
19                assert_eq!(node.kind(), NODE_INTERPOL);
20                Some(InterpolPart::Interpolation(ast::Interpol::cast(node.clone()).unwrap()))
21            }
22        })
23    }
24
25    pub fn normalized_parts(&self) -> Vec<InterpolPart<String>> {
26        let multiline = children_tokens_u(self).next().map_or(false, |t| t.text() == "''");
27        let mut is_first_literal = true;
28        let mut at_start_of_line = true;
29        let mut min_indent = 1000000;
30        let mut cur_indent = 0;
31        let mut n = 0;
32        let mut first_is_literal = false;
33
34        let parts: Vec<InterpolPart<StrContent>> = self.parts().collect();
35
36        if multiline {
37            for part in &parts {
38                match part {
39                    InterpolPart::Interpolation(_) => {
40                        if at_start_of_line {
41                            at_start_of_line = false;
42                            min_indent = min_indent.min(cur_indent);
43                        }
44                        n += 1;
45                    }
46                    InterpolPart::Literal(literal) => {
47                        let mut token_text = literal.syntax().text();
48
49                        if n == 0 {
50                            first_is_literal = true;
51                        }
52
53                        if is_first_literal && first_is_literal {
54                            is_first_literal = false;
55                            if let Some(p) = token_text.find('\n') {
56                                if token_text[0..p].chars().all(|c| c == ' ') {
57                                    token_text = &token_text[p + 1..]
58                                }
59                            }
60                        }
61
62                        for c in token_text.chars() {
63                            if at_start_of_line {
64                                if c == ' ' {
65                                    cur_indent += 1;
66                                } else if c == '\n' {
67                                    cur_indent = 0;
68                                } else {
69                                    at_start_of_line = false;
70                                    min_indent = min_indent.min(cur_indent);
71                                }
72                            } else if c == '\n' {
73                                at_start_of_line = true;
74                                cur_indent = 0;
75                            }
76                        }
77
78                        n += 1;
79                    }
80                }
81            }
82        }
83
84        let mut normalized_parts = Vec::new();
85        let mut cur_dropped = 0;
86        let mut i = 0;
87        is_first_literal = true;
88        at_start_of_line = true;
89
90        for part in parts {
91            match part {
92                InterpolPart::Interpolation(interpol) => {
93                    at_start_of_line = false;
94                    cur_dropped = 0;
95                    normalized_parts.push(InterpolPart::Interpolation(interpol));
96                    i += 1;
97                }
98                InterpolPart::Literal(literal) => {
99                    let mut token_text = literal.syntax().text();
100
101                    if multiline {
102                        if is_first_literal && first_is_literal {
103                            is_first_literal = false;
104                            if let Some(p) = token_text.find('\n') {
105                                if token_text[0..p].chars().all(|c| c == ' ') {
106                                    token_text = &token_text[p + 1..];
107                                    if token_text.is_empty() {
108                                        i += 1;
109                                        continue;
110                                    }
111                                }
112                            }
113                        }
114
115                        let mut str = String::new();
116                        for c in token_text.chars() {
117                            if at_start_of_line {
118                                if c == ' ' {
119                                    if cur_dropped >= min_indent {
120                                        str.push(c);
121                                    }
122                                    cur_dropped += 1;
123                                } else if c == '\n' {
124                                    cur_dropped = 0;
125                                    str.push(c);
126                                } else {
127                                    at_start_of_line = false;
128                                    cur_dropped = 0;
129                                    str.push(c);
130                                }
131                            } else {
132                                str.push(c);
133                                if c == '\n' {
134                                    at_start_of_line = true;
135                                }
136                            }
137                        }
138
139                        if i == n - 1 {
140                            if let Some(p) = str.rfind('\n') {
141                                if str[p + 1..].chars().all(|c| c == ' ') {
142                                    str.truncate(p + 1);
143                                }
144                            }
145                        }
146
147                        normalized_parts.push(InterpolPart::Literal(unescape(&str, multiline)));
148                        i += 1;
149                    } else {
150                        normalized_parts
151                            .push(InterpolPart::Literal(unescape(token_text, multiline)));
152                    }
153                }
154            }
155        }
156
157        normalized_parts
158    }
159}
160
161/// Interpret escape sequences in the nix string and return the converted value
162pub fn unescape(input: &str, multiline: bool) -> String {
163    let mut output = String::new();
164    let mut input = input.chars().peekable();
165    loop {
166        match input.next() {
167            None => break,
168            Some('"') if !multiline => break,
169            Some('\\') if !multiline => match input.next() {
170                None => break,
171                Some('n') => output.push('\n'),
172                Some('r') => output.push('\r'),
173                Some('t') => output.push('\t'),
174                Some(c) => output.push(c),
175            },
176            Some('\'') if multiline => match input.next() {
177                None => {
178                    output.push('\'');
179                }
180                Some('\'') => match input.peek() {
181                    Some('\'') => {
182                        input.next().unwrap();
183                        output.push_str("''");
184                    }
185                    Some('$') => {
186                        input.next().unwrap();
187                        output.push('$');
188                    }
189                    Some('\\') => {
190                        input.next().unwrap();
191                        match input.next() {
192                            None => break,
193                            Some('n') => output.push('\n'),
194                            Some('r') => output.push('\r'),
195                            Some('t') => output.push('\t'),
196                            Some(c) => output.push(c),
197                        }
198                    }
199                    _ => break,
200                },
201                Some(c) => {
202                    output.push('\'');
203                    output.push(c);
204                }
205            },
206            Some(c) => output.push(c),
207        }
208    }
209    output
210}
211
212#[cfg(test)]
213mod tests {
214    use crate::Root;
215
216    use super::*;
217
218    #[test]
219    fn string_unescapes() {
220        assert_eq!(unescape(r#"Hello\n\"World\" :D"#, false), "Hello\n\"World\" :D");
221        assert_eq!(unescape(r#"\"Hello\""#, false), "\"Hello\"");
222
223        assert_eq!(unescape(r#"Hello''\n'''World''' :D"#, true), "Hello\n''World'' :D");
224        assert_eq!(unescape(r#""Hello""#, true), "\"Hello\"");
225    }
226    #[test]
227    fn parts_leading_ws() {
228        let inp = "''\n  hello\n  world''";
229        let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
230        match expr {
231            ast::Expr::Str(str) => {
232                assert_eq!(
233                    str.normalized_parts(),
234                    vec![InterpolPart::Literal("hello\nworld".to_string())]
235                )
236            }
237            _ => unreachable!(),
238        }
239    }
240    #[test]
241    fn parts_trailing_ws_single_line() {
242        let inp = "''hello ''";
243        let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
244        match expr {
245            ast::Expr::Str(str) => {
246                assert_eq!(
247                    str.normalized_parts(),
248                    vec![InterpolPart::Literal("hello ".to_string())]
249                )
250            }
251            _ => unreachable!(),
252        }
253    }
254    #[test]
255    fn parts_trailing_ws_multiline() {
256        let inp = "''hello\n ''";
257        let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
258        match expr {
259            ast::Expr::Str(str) => {
260                assert_eq!(
261                    str.normalized_parts(),
262                    vec![InterpolPart::Literal("hello\n".to_string())]
263                )
264            }
265            _ => unreachable!(),
266        }
267    }
268    #[test]
269    fn parts() {
270        use crate::{NixLanguage, SyntaxNode};
271        use rowan::{GreenNodeBuilder, Language};
272
273        fn string_node(content: &str) -> ast::Str {
274            let mut builder = GreenNodeBuilder::new();
275            builder.start_node(NixLanguage::kind_to_raw(NODE_STRING));
276            builder.token(NixLanguage::kind_to_raw(TOKEN_STRING_START), "''");
277            builder.token(NixLanguage::kind_to_raw(TOKEN_STRING_CONTENT), content);
278            builder.token(NixLanguage::kind_to_raw(TOKEN_STRING_END), "''");
279            builder.finish_node();
280
281            ast::Str::cast(SyntaxNode::new_root(builder.finish())).unwrap()
282        }
283
284        let txtin = r#"
285                        |trailing-whitespace
286                              |trailing-whitespace
287                    This is a multiline string :D
288                      indented by two
289                    \'\'\'\'\
290                    ''${ interpolation was escaped }
291                    two single quotes: '''
292                    three single quotes: ''''
293                "#
294        .replace("|trailing-whitespace", "");
295
296        if let [InterpolPart::Literal(lit)] =
297            &ast::Str::normalized_parts(&string_node(txtin.as_str()))[..]
298        {
299            assert_eq!(lit,
300                // Get the below with nix repl
301                "    \n          \nThis is a multiline string :D\n  indented by two\n\\'\\'\\'\\'\\\n${ interpolation was escaped }\ntwo single quotes: ''\nthree single quotes: '''\n"
302            );
303        } else {
304            unreachable!();
305        }
306    }
307
308    #[test]
309    fn parts_ast() {
310        fn assert_eq_ast_ctn(it: &mut dyn Iterator<Item = InterpolPart<String>>, x: &str) {
311            let tmp = it.next().expect("unexpected EOF");
312            if let InterpolPart::Interpolation(astn) = tmp {
313                assert_eq!(astn.expr().unwrap().syntax().to_string(), x);
314            } else {
315                unreachable!("unexpected literal {:?}", tmp);
316            }
317        }
318
319        let inp = r#"''
320
321        This version of Nixpkgs requires Nix >= ${requiredVersion}, please upgrade:
322
323        - If you are running NixOS, `nixos-rebuild' can be used to upgrade your system.
324
325        - Alternatively, with Nix > 2.0 `nix upgrade-nix' can be used to imperatively
326          upgrade Nix. You may use `nix-env --version' to check which version you have.
327
328        - If you installed Nix using the install script (https://nixos.org/nix/install),
329          it is safe to upgrade by running it again:
330
331              curl -L https://nixos.org/nix/install | sh
332
333        For more information, please see the NixOS release notes at
334        https://nixos.org/nixos/manual or locally at
335        ${toString ./nixos/doc/manual/release-notes}.
336
337        If you need further help, see https://nixos.org/nixos/support.html
338      ''"#;
339        let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
340        match expr {
341            ast::Expr::Str(s) => {
342                let mut it = s.normalized_parts().into_iter();
343                assert_eq!(
344                    it.next().unwrap(),
345                    InterpolPart::Literal("\nThis version of Nixpkgs requires Nix >= ".to_string())
346                );
347                assert_eq_ast_ctn(&mut it, "requiredVersion");
348                assert_eq!(it.next().unwrap(), InterpolPart::Literal(
349                        ", please upgrade:\n\n- If you are running NixOS, `nixos-rebuild' can be used to upgrade your system.\n\n- Alternatively, with Nix > 2.0 `nix upgrade-nix' can be used to imperatively\n  upgrade Nix. You may use `nix-env --version' to check which version you have.\n\n- If you installed Nix using the install script (https://nixos.org/nix/install),\n  it is safe to upgrade by running it again:\n\n      curl -L https://nixos.org/nix/install | sh\n\nFor more information, please see the NixOS release notes at\nhttps://nixos.org/nixos/manual or locally at\n".to_string()
350                    ));
351                assert_eq_ast_ctn(&mut it, "toString ./nixos/doc/manual/release-notes");
352                assert_eq!(
353                    it.next().unwrap(),
354                    InterpolPart::Literal(
355                        ".\n\nIf you need further help, see https://nixos.org/nixos/support.html\n"
356                            .to_string()
357                    )
358                );
359            }
360            _ => unreachable!(),
361        }
362    }
363}