Skip to main content

reed_parser/cst/
node_ext.rs

1use crate::cst;
2use crate::cst::CstNode;
3use crate::SyntaxNode;
4use crate::TokenText;
5use rowan::GreenToken;
6use rowan::SyntaxKind;
7use std::num::ParseFloatError;
8use std::num::ParseIntError;
9
10impl cst::Name {
11    pub fn text(&self) -> TokenText {
12        text_of_first_token(self.syntax())
13    }
14}
15
16impl cst::Variable {
17    pub fn text(&self) -> TokenText {
18        self.name()
19            .expect("Cannot get variable's NAME token")
20            .text()
21    }
22}
23
24impl cst::EnumValue {
25    pub fn text(&self) -> TokenText {
26        self.name()
27            .expect("Cannot get enum value's NAME token")
28            .text()
29    }
30}
31
32impl cst::DirectiveLocation {
33    pub fn text(self) -> Option<TokenText> {
34        let txt = if self.query_token().is_some() {
35            Some("QUERY")
36        } else if self.mutation_token().is_some() {
37            Some("MUTATION")
38        } else if self.subscription_token().is_some() {
39            Some("SUBSCRIPTION")
40        } else if self.field_token().is_some() {
41            Some("FIELD")
42        } else if self.fragment_definition_token().is_some() {
43            Some("FRAGMENT_DEFINITION")
44        } else if self.fragment_spread_token().is_some() {
45            Some("FRAGMENT_SPREAD")
46        } else if self.inline_fragment_token().is_some() {
47            Some("INLINE_FRAGMENT")
48        } else if self.variable_definition_token().is_some() {
49            Some("VARIABLE_DEFINITION")
50        } else if self.schema_token().is_some() {
51            Some("SCHEMA")
52        } else if self.scalar_token().is_some() {
53            Some("SCALAR")
54        } else if self.object_token().is_some() {
55            Some("OBJECT")
56        } else if self.field_definition_token().is_some() {
57            Some("FIELD_DEFINITION")
58        } else if self.argument_definition_token().is_some() {
59            Some("ARGUMENT_DEFINITION")
60        } else if self.interface_token().is_some() {
61            Some("INTERFACE")
62        } else if self.union_token().is_some() {
63            Some("UNION")
64        } else if self.enum_token().is_some() {
65            Some("ENUM")
66        } else if self.enum_value_token().is_some() {
67            Some("ENUM_VALUE")
68        } else if self.input_object_token().is_some() {
69            Some("INPUT_OBJECT")
70        } else if self.input_field_definition_token().is_some() {
71            Some("INPUT_FIELD_DEFINITION")
72        } else {
73            None
74        };
75
76        txt.map(|txt| {
77            TokenText(GreenToken::new(
78                SyntaxKind(crate::SyntaxKind::DIRECTIVE_LOCATION as u16),
79                txt,
80            ))
81        })
82    }
83}
84
85impl cst::Definition {
86    /// Return the name of this definition, if any. Schema definitions are unnamed and always
87    /// return `None`.
88    pub fn name(&self) -> Option<cst::Name> {
89        match self {
90            Self::OperationDefinition(it) => it.name(),
91            Self::FragmentDefinition(it) => it.fragment_name()?.name(),
92            Self::DirectiveDefinition(it) => it.name(),
93            Self::SchemaDefinition(_) => None,
94            Self::ScalarTypeDefinition(it) => it.name(),
95            Self::ObjectTypeDefinition(it) => it.name(),
96            Self::InterfaceTypeDefinition(it) => it.name(),
97            Self::UnionTypeDefinition(it) => it.name(),
98            Self::EnumTypeDefinition(it) => it.name(),
99            Self::InputObjectTypeDefinition(it) => it.name(),
100            Self::SchemaExtension(_) => None,
101            Self::DirectiveExtension(it) => it.name(),
102            Self::ScalarTypeExtension(it) => it.name(),
103            Self::ObjectTypeExtension(it) => it.name(),
104            Self::InterfaceTypeExtension(it) => it.name(),
105            Self::UnionTypeExtension(it) => it.name(),
106            Self::EnumTypeExtension(it) => it.name(),
107            Self::InputObjectTypeExtension(it) => it.name(),
108        }
109    }
110
111    pub fn kind(&self) -> &'static str {
112        match self {
113            cst::Definition::OperationDefinition(_) => "OperationDefinition",
114            cst::Definition::FragmentDefinition(_) => "FragmentDefinition",
115            cst::Definition::DirectiveDefinition(_) => "DirectiveDefinition",
116            cst::Definition::ScalarTypeDefinition(_) => "ScalarTypeDefinition",
117            cst::Definition::ObjectTypeDefinition(_) => "ObjectTypeDefinition",
118            cst::Definition::InterfaceTypeDefinition(_) => "InterfaceTypeDefinition",
119            cst::Definition::UnionTypeDefinition(_) => "UnionTypeDefinition",
120            cst::Definition::EnumTypeDefinition(_) => "EnumTypeDefinition",
121            cst::Definition::InputObjectTypeDefinition(_) => "InputObjectTypeDefinition",
122            cst::Definition::SchemaDefinition(_) => "SchemaDefinition",
123            cst::Definition::SchemaExtension(_) => "SchemaExtension",
124            cst::Definition::DirectiveExtension(_) => "DirectiveExtension",
125            cst::Definition::ScalarTypeExtension(_) => "ScalarTypeExtension",
126            cst::Definition::ObjectTypeExtension(_) => "ObjectTypeExtension",
127            cst::Definition::InterfaceTypeExtension(_) => "InterfaceTypeExtension",
128            cst::Definition::UnionTypeExtension(_) => "UnionTypeExtension",
129            cst::Definition::EnumTypeExtension(_) => "EnumTypeExtension",
130            cst::Definition::InputObjectTypeExtension(_) => "InputObjectTypeExtension",
131        }
132    }
133
134    pub fn is_executable_definition(&self) -> bool {
135        matches!(
136            self,
137            Self::OperationDefinition(_) | Self::FragmentDefinition(_)
138        )
139    }
140
141    pub fn is_extension_definition(&self) -> bool {
142        matches!(
143            self,
144            Self::SchemaExtension(_)
145                | Self::DirectiveExtension(_)
146                | Self::ScalarTypeExtension(_)
147                | Self::ObjectTypeExtension(_)
148                | Self::InterfaceTypeExtension(_)
149                | Self::UnionTypeExtension(_)
150                | Self::EnumTypeExtension(_)
151                | Self::InputObjectTypeExtension(_)
152        )
153    }
154}
155
156impl From<cst::StringValue> for String {
157    fn from(val: cst::StringValue) -> Self {
158        Self::from(&val)
159    }
160}
161
162/// Handle escaped characters in a StringValue.
163///
164/// Panics on invalid escape sequences. Those should be rejected in the lexer already.
165fn unescape_string(input: &str) -> String {
166    let mut output = String::with_capacity(input.len());
167
168    let mut iter = input.chars();
169    while let Some(c) = iter.next() {
170        match c {
171            '\\' => {
172                let Some(c2) = iter.next() else {
173                    output.push(c);
174                    break;
175                };
176
177                match c2 {
178                    '"' | '\\' | '/' => output.push(c2),
179                    'b' => output.push('\u{0008}'),
180                    'f' => output.push('\u{000c}'),
181                    'n' => output.push('\n'),
182                    'r' => output.push('\r'),
183                    't' => output.push('\t'),
184                    'u' => {
185                        let (value, length) = crate::lexer::unicode_escape(iter.as_str())
186                            .expect("unicode escapes have been validated by the lexer");
187                        iter.nth(length - 1);
188                        output.push(value);
189                    }
190                    _ => (),
191                }
192            }
193            _ => output.push(c),
194        }
195    }
196
197    output
198}
199
200const ESCAPED_TRIPLE_QUOTE: &str = r#"\""""#;
201const TRIPLE_QUOTE: &str = r#"""""#;
202
203fn is_block_string(input: &str) -> bool {
204    input.starts_with(TRIPLE_QUOTE)
205}
206
207/// Iterator over the lines in a GraphQL string, using GraphQL's definition of newlines
208/// (\r\n, \n, or just \r).
209struct GraphQLLines<'a> {
210    input: &'a str,
211    finished: bool,
212}
213
214impl<'a> GraphQLLines<'a> {
215    fn new(input: &'a str) -> Self {
216        Self {
217            input,
218            finished: false,
219        }
220    }
221}
222
223impl<'a> Iterator for GraphQLLines<'a> {
224    type Item = &'a str;
225    fn next(&mut self) -> Option<Self::Item> {
226        // Can't just check for the input string being empty, as an empty string should still
227        // produce one line.
228        if self.finished {
229            return None;
230        }
231
232        let Some(index) = memchr::memchr2(b'\r', b'\n', self.input.as_bytes()) else {
233            self.finished = true;
234            return Some(self.input);
235        };
236        let line = &self.input[..index];
237        let rest = match self.input.get(index..=index + 1) {
238            Some("\r\n") => &self.input[index + 2..],
239            _ => &self.input[index + 1..],
240        };
241        self.input = rest;
242        Some(line)
243    }
244}
245
246/// Split lines on \n, \r\n, and just \r
247fn split_lines(input: &str) -> impl Iterator<Item = &str> {
248    GraphQLLines::new(input)
249}
250
251/// Replace a literal pattern in a string but push the output to an existing string.
252///
253/// Like `str::replace`, but doesn't allocate if there's enough space in the provided output.
254fn replace_into(input: &str, pattern: &str, replace: &str, output: &mut String) {
255    let mut last_index = 0;
256    for index in memchr::memmem::find_iter(input.as_bytes(), pattern.as_bytes()) {
257        output.push_str(&input[last_index..index]);
258        output.push_str(replace);
259        last_index = index + pattern.len();
260    }
261    if last_index < input.len() {
262        output.push_str(&input[last_index..]);
263    }
264}
265
266/// Implementation of the spec function `BlockStringValue(rawValue)`. In addition to handling
267/// indents and newline normalization, this also handles escape sequences (strictly not part of
268/// BlockStringValue in the spec, but more efficient to do it at the same time).
269///
270/// Spec: https://spec.graphql.org/October2021/#BlockStringValue()
271fn unescape_block_string(raw_value: &str) -> String {
272    /// WhiteSpace :: Horizontal Tab (U+0009) Space (U+0020)
273    fn is_whitespace(c: char) -> bool {
274        matches!(c, ' ' | '\t')
275    }
276    /// Check if a string is all WhiteSpace. This expects a single line of input.
277    fn is_whitespace_line(line: &str) -> bool {
278        line.chars().all(is_whitespace)
279    }
280    /// Count the indentation of a single line (how many WhiteSpace characters are at the start).
281    fn count_indent(line: &str) -> usize {
282        line.chars().take_while(|&c| is_whitespace(c)).count()
283    }
284
285    // 1. Let lines be the result of splitting rawValue by LineTerminator.
286    // 2. Let commonIndent be null.
287    // 3. For each line in lines:
288    let common_indent = split_lines(raw_value)
289        // 3.a. If line is the first item in lines, continue to the next line.
290        .skip(1)
291        .filter_map(|line| {
292            // 3.b. Let length be the number of characters in line.
293            // We will compare this byte length to a character length below, but
294            // `count_indent` only ever counts one-byte characters, so it's equivalent.
295            let length = line.len();
296            // 3.c. Let indent be the number of leading consecutive WhiteSpace characters in line.
297            let indent = count_indent(line);
298            // 3.d. If indent is less than length:
299            (indent < length).then_some(indent)
300        })
301        .min()
302        .unwrap_or(0);
303
304    let mut lines = split_lines(raw_value)
305        .enumerate()
306        // 4.a. For each line in lines:
307        .map(|(index, line)| {
308            // 4.a.i. If line is the first item in lines, continue to the next line.
309            if index == 0 {
310                line
311            } else {
312                // 4.a.ii. Remove commonIndent characters from the beginning of line.
313                &line[common_indent.min(line.len())..]
314            }
315        })
316        // 5. While the first item line in lines contains only WhiteSpace:
317        // 5.a. Remove the first item from lines.
318        .skip_while(|line| is_whitespace_line(line));
319
320    // (Step 6 is done at the end so we don't need an intermediate allocation.)
321
322    // 7. Let formatted be the empty character sequence.
323    let mut formatted = String::with_capacity(raw_value.len());
324
325    // 8. For each line in lines:
326    // 8.a. If line is the first item in lines:
327    if let Some(line) = lines.next() {
328        // 8.a.i. Append formatted with line.
329        replace_into(line, ESCAPED_TRIPLE_QUOTE, TRIPLE_QUOTE, &mut formatted);
330    };
331
332    let mut final_char_index = formatted.len();
333
334    // 8.b. Otherwise:
335    for line in lines {
336        // 8.b.i. Append formatted with a line feed character (U+000A).
337        formatted.push('\n');
338        // 8.b.ii. Append formatted with line.
339        replace_into(line, ESCAPED_TRIPLE_QUOTE, TRIPLE_QUOTE, &mut formatted);
340
341        // Track the last non-whitespace line for implementing step 6 in the spec.
342        if !is_whitespace_line(line) {
343            final_char_index = formatted.len();
344        }
345    }
346
347    // 6. Implemented differently: remove WhiteSpace-only lines from the end.
348    formatted.truncate(final_char_index);
349
350    // 9. Return formatted.
351    formatted
352}
353
354// TODO(@goto-bus-stop) As this handles escaping, which can fail in theory, it should be TryFrom
355impl From<&'_ cst::StringValue> for String {
356    fn from(val: &'_ cst::StringValue) -> Self {
357        let text = text_of_first_token(val.syntax());
358        // These slices would panic if the contents are invalid, but the lexer already guarantees that the
359        // string is valid.
360        if is_block_string(&text) {
361            unescape_block_string(&text[3..text.len() - 3])
362        } else {
363            unescape_string(&text[1..text.len() - 1])
364        }
365    }
366}
367
368impl TryFrom<cst::IntValue> for i32 {
369    type Error = ParseIntError;
370
371    fn try_from(val: cst::IntValue) -> Result<Self, Self::Error> {
372        Self::try_from(&val)
373    }
374}
375
376impl TryFrom<&'_ cst::IntValue> for i32 {
377    type Error = ParseIntError;
378
379    fn try_from(val: &'_ cst::IntValue) -> Result<Self, Self::Error> {
380        let text = text_of_first_token(val.syntax());
381        text.parse()
382    }
383}
384
385impl TryFrom<cst::IntValue> for f64 {
386    type Error = ParseFloatError;
387
388    fn try_from(val: cst::IntValue) -> Result<Self, Self::Error> {
389        Self::try_from(&val)
390    }
391}
392
393impl TryFrom<&'_ cst::IntValue> for f64 {
394    type Error = ParseFloatError;
395
396    fn try_from(val: &'_ cst::IntValue) -> Result<Self, Self::Error> {
397        let text = text_of_first_token(val.syntax());
398        text.parse()
399    }
400}
401
402impl TryFrom<cst::FloatValue> for f64 {
403    type Error = ParseFloatError;
404
405    fn try_from(val: cst::FloatValue) -> Result<Self, Self::Error> {
406        Self::try_from(&val)
407    }
408}
409
410impl TryFrom<&'_ cst::FloatValue> for f64 {
411    type Error = ParseFloatError;
412
413    fn try_from(val: &'_ cst::FloatValue) -> Result<Self, Self::Error> {
414        let text = text_of_first_token(val.syntax());
415        text.parse()
416    }
417}
418
419impl TryFrom<cst::BooleanValue> for bool {
420    type Error = std::str::ParseBoolError;
421
422    fn try_from(val: cst::BooleanValue) -> Result<Self, Self::Error> {
423        Self::try_from(&val)
424    }
425}
426
427impl TryFrom<&'_ cst::BooleanValue> for bool {
428    type Error = std::str::ParseBoolError;
429
430    fn try_from(val: &'_ cst::BooleanValue) -> Result<Self, Self::Error> {
431        let text = text_of_first_token(val.syntax());
432        text.parse()
433    }
434}
435
436fn text_of_first_token(node: &SyntaxNode) -> TokenText {
437    let first_token = node
438        .green()
439        .children()
440        .next()
441        .and_then(|it| it.into_token())
442        .unwrap()
443        .to_owned();
444
445    TokenText(first_token)
446}
447
448#[cfg(test)]
449mod string_tests {
450    use super::unescape_string;
451
452    #[test]
453    fn it_parses_strings() {
454        assert_eq!(unescape_string(r"simple"), "simple");
455        assert_eq!(unescape_string(r" white space "), " white space ");
456    }
457
458    #[test]
459    fn it_unescapes_strings() {
460        assert_eq!(unescape_string(r#"quote \""#), "quote \"");
461        assert_eq!(
462            unescape_string(r"escaped \n\r\b\t\f"),
463            "escaped \n\r\u{0008}\t\u{000c}"
464        );
465        assert_eq!(unescape_string(r"slashes \\ \/"), r"slashes \ /");
466        assert_eq!(
467            unescape_string("unescaped unicode outside BMP 😀"),
468            "unescaped unicode outside BMP 😀"
469        );
470        assert_eq!(
471            unescape_string(r"unicode \u1234\u5678\u90AB\uCDEF"),
472            "unicode \u{1234}\u{5678}\u{90AB}\u{CDEF}"
473        );
474    }
475}
476
477#[cfg(test)]
478mod block_string_tests {
479    use super::split_lines;
480    use super::unescape_block_string;
481
482    #[test]
483    fn it_splits_lines_by_graphql_newline_definition() {
484        let plain_newlines: Vec<_> = split_lines(
485            r#"source text
486    with some
487    new
488
489
490    lines
491        "#,
492        )
493        .collect();
494
495        assert_eq!(
496            plain_newlines,
497            [
498                "source text",
499                "    with some",
500                "    new",
501                "",
502                "",
503                "    lines",
504                "        ",
505            ]
506        );
507
508        let different_endings: Vec<_> =
509            split_lines("with\nand\r\nand\rall in the same\r\nstring").collect();
510        assert_eq!(
511            different_endings,
512            ["with", "and", "and", "all in the same", "string",]
513        );
514
515        let empty_string: Vec<_> = split_lines("").collect();
516        assert_eq!(empty_string, [""]);
517
518        let empty_line: Vec<_> = split_lines("\n\r\r\n").collect();
519        assert_eq!(empty_line, ["", "", "", ""]);
520    }
521
522    #[test]
523    fn it_normalizes_block_string_newlines() {
524        assert_eq!(unescape_block_string("multi\nline"), "multi\nline");
525        assert_eq!(unescape_block_string("multi\r\nline"), "multi\nline");
526        assert_eq!(unescape_block_string("multi\rline"), "multi\nline");
527    }
528
529    #[test]
530    fn it_does_not_unescape_block_strings() {
531        assert_eq!(
532            unescape_block_string(r"escaped \n\r\b\t\f"),
533            r"escaped \n\r\b\t\f"
534        );
535        assert_eq!(unescape_block_string(r"slashes \\ \/"), r"slashes \\ \/");
536        assert_eq!(
537            unescape_block_string("unescaped unicode outside BMP \u{1f600}"),
538            "unescaped unicode outside BMP \u{1f600}"
539        );
540    }
541
542    #[test]
543    fn it_dedents_block_strings() {
544        assert_eq!(
545            unescape_block_string("  intact whitespace with one line  "),
546            "  intact whitespace with one line  "
547        );
548
549        assert_eq!(
550            unescape_block_string(
551                r"
552            This is
553            indented
554            quite a lot
555    "
556            ),
557            r"This is
558indented
559quite a lot"
560        );
561
562        assert_eq!(
563            unescape_block_string(
564                r"
565
566        spans
567          multiple
568            lines
569
570    "
571            ),
572            r"spans
573  multiple
574    lines"
575        );
576    }
577}