Skip to main content

apollo_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::ScalarTypeExtension(it) => it.name(),
102            Self::ObjectTypeExtension(it) => it.name(),
103            Self::InterfaceTypeExtension(it) => it.name(),
104            Self::UnionTypeExtension(it) => it.name(),
105            Self::EnumTypeExtension(it) => it.name(),
106            Self::InputObjectTypeExtension(it) => it.name(),
107        }
108    }
109
110    pub fn kind(&self) -> &'static str {
111        match self {
112            cst::Definition::OperationDefinition(_) => "OperationDefinition",
113            cst::Definition::FragmentDefinition(_) => "FragmentDefinition",
114            cst::Definition::DirectiveDefinition(_) => "DirectiveDefinition",
115            cst::Definition::ScalarTypeDefinition(_) => "ScalarTypeDefinition",
116            cst::Definition::ObjectTypeDefinition(_) => "ObjectTypeDefinition",
117            cst::Definition::InterfaceTypeDefinition(_) => "InterfaceTypeDefinition",
118            cst::Definition::UnionTypeDefinition(_) => "UnionTypeDefinition",
119            cst::Definition::EnumTypeDefinition(_) => "EnumTypeDefinition",
120            cst::Definition::InputObjectTypeDefinition(_) => "InputObjectTypeDefinition",
121            cst::Definition::SchemaDefinition(_) => "SchemaDefinition",
122            cst::Definition::SchemaExtension(_) => "SchemaExtension",
123            cst::Definition::ScalarTypeExtension(_) => "ScalarTypeExtension",
124            cst::Definition::ObjectTypeExtension(_) => "ObjectTypeExtension",
125            cst::Definition::InterfaceTypeExtension(_) => "InterfaceTypeExtension",
126            cst::Definition::UnionTypeExtension(_) => "UnionTypeExtension",
127            cst::Definition::EnumTypeExtension(_) => "EnumTypeExtension",
128            cst::Definition::InputObjectTypeExtension(_) => "InputObjectTypeExtension",
129        }
130    }
131
132    pub fn is_executable_definition(&self) -> bool {
133        matches!(
134            self,
135            Self::OperationDefinition(_) | Self::FragmentDefinition(_)
136        )
137    }
138
139    pub fn is_extension_definition(&self) -> bool {
140        matches!(
141            self,
142            Self::SchemaExtension(_)
143                | Self::ScalarTypeExtension(_)
144                | Self::ObjectTypeExtension(_)
145                | Self::InterfaceTypeExtension(_)
146                | Self::UnionTypeExtension(_)
147                | Self::EnumTypeExtension(_)
148                | Self::InputObjectTypeExtension(_)
149        )
150    }
151}
152
153impl From<cst::StringValue> for String {
154    fn from(val: cst::StringValue) -> Self {
155        Self::from(&val)
156    }
157}
158
159/// Handle escaped characters in a StringValue.
160///
161/// Panics on invalid escape sequences. Those should be rejected in the lexer already.
162fn unescape_string(input: &str) -> String {
163    /// Read the 4 hexadecimal digits of a fixed-width `\uXXXX` escape sequence.
164    fn hex4(iter: &mut std::str::Chars<'_>) -> u32 {
165        iter.by_ref().take(4).fold(0, |acc, c| {
166            let digit = c.to_digit(16).unwrap();
167            (acc << 4) + digit
168        })
169    }
170
171    let mut output = String::with_capacity(input.len());
172
173    let mut iter = input.chars();
174    while let Some(c) = iter.next() {
175        match c {
176            '\\' => {
177                let Some(c2) = iter.next() else {
178                    output.push(c);
179                    break;
180                };
181
182                // https://spec.graphql.org/September2025/#EscapedUnicode
183                let mut unicode = || {
184                    let value = if iter.clone().next() == Some('{') {
185                        // Variable-width `\u{HexDigits}` escape sequence.
186                        iter.next(); // consume '{'
187                        let mut value = 0;
188                        loop {
189                            let c = iter.next().unwrap();
190                            if c == '}' {
191                                break;
192                            }
193                            value = (value << 4) + c.to_digit(16).unwrap();
194                        }
195                        value
196                    } else {
197                        let value = hex4(&mut iter);
198                        if (0xD800..=0xDBFF).contains(&value) {
199                            // A leading surrogate: the lexer guarantees it is followed
200                            // by a `\uXXXX` trailing surrogate escape; combine the pair
201                            // into a single code point.
202                            iter.next(); // consume '\'
203                            iter.next(); // consume 'u'
204                            let trail = hex4(&mut iter);
205                            (value - 0xD800) * 0x400 + (trail - 0xDC00) + 0x10000
206                        } else {
207                            value
208                        }
209                    };
210                    char::from_u32(value).unwrap()
211                };
212
213                match c2 {
214                    '"' | '\\' | '/' => output.push(c2),
215                    'b' => output.push('\u{0008}'),
216                    'f' => output.push('\u{000c}'),
217                    'n' => output.push('\n'),
218                    'r' => output.push('\r'),
219                    't' => output.push('\t'),
220                    'u' => output.push(unicode()),
221                    _ => (),
222                }
223            }
224            _ => output.push(c),
225        }
226    }
227
228    output
229}
230
231const ESCAPED_TRIPLE_QUOTE: &str = r#"\""""#;
232const TRIPLE_QUOTE: &str = r#"""""#;
233
234fn is_block_string(input: &str) -> bool {
235    input.starts_with(TRIPLE_QUOTE)
236}
237
238/// Iterator over the lines in a GraphQL string, using GraphQL's definition of newlines
239/// (\r\n, \n, or just \r).
240struct GraphQLLines<'a> {
241    input: &'a str,
242    finished: bool,
243}
244
245impl<'a> GraphQLLines<'a> {
246    fn new(input: &'a str) -> Self {
247        Self {
248            input,
249            finished: false,
250        }
251    }
252}
253
254impl<'a> Iterator for GraphQLLines<'a> {
255    type Item = &'a str;
256    fn next(&mut self) -> Option<Self::Item> {
257        // Can't just check for the input string being empty, as an empty string should still
258        // produce one line.
259        if self.finished {
260            return None;
261        }
262
263        let Some(index) = memchr::memchr2(b'\r', b'\n', self.input.as_bytes()) else {
264            self.finished = true;
265            return Some(self.input);
266        };
267        let line = &self.input[..index];
268        let rest = match self.input.get(index..=index + 1) {
269            Some("\r\n") => &self.input[index + 2..],
270            _ => &self.input[index + 1..],
271        };
272        self.input = rest;
273        Some(line)
274    }
275}
276
277/// Split lines on \n, \r\n, and just \r
278fn split_lines(input: &str) -> impl Iterator<Item = &str> {
279    GraphQLLines::new(input)
280}
281
282/// Replace a literal pattern in a string but push the output to an existing string.
283///
284/// Like `str::replace`, but doesn't allocate if there's enough space in the provided output.
285fn replace_into(input: &str, pattern: &str, replace: &str, output: &mut String) {
286    let mut last_index = 0;
287    for index in memchr::memmem::find_iter(input.as_bytes(), pattern.as_bytes()) {
288        output.push_str(&input[last_index..index]);
289        output.push_str(replace);
290        last_index = index + pattern.len();
291    }
292    if last_index < input.len() {
293        output.push_str(&input[last_index..]);
294    }
295}
296
297/// Implementation of the spec function `BlockStringValue(rawValue)`. In addition to handling
298/// indents and newline normalization, this also handles escape sequences (strictly not part of
299/// BlockStringValue in the spec, but more efficient to do it at the same time).
300///
301/// Spec: https://spec.graphql.org/September2025/#BlockStringValue()
302fn unescape_block_string(raw_value: &str) -> String {
303    /// Whitespace :: Horizontal Tab (U+0009) Space (U+0020)
304    fn is_whitespace(c: char) -> bool {
305        matches!(c, ' ' | '\t')
306    }
307    /// Check if a string is all Whitespace. This expects a single line of input.
308    fn is_whitespace_line(line: &str) -> bool {
309        line.chars().all(is_whitespace)
310    }
311    /// Count the indentation of a single line (how many Whitespace characters are at the start).
312    fn count_indent(line: &str) -> usize {
313        line.chars().take_while(|&c| is_whitespace(c)).count()
314    }
315
316    // 1. Let lines be the result of splitting rawValue by LineTerminator.
317    // 2. Let commonIndent be null.
318    // 3. For each line in lines:
319    let common_indent = split_lines(raw_value)
320        // 3.a. If line is the first item in lines, continue to the next line.
321        .skip(1)
322        .filter_map(|line| {
323            // 3.b. Let length be the number of characters in line.
324            // We will compare this byte length to a character length below, but
325            // `count_indent` only ever counts one-byte characters, so it's equivalent.
326            let length = line.len();
327            // 3.c. Let indent be the number of leading consecutive Whitespace characters in line.
328            let indent = count_indent(line);
329            // 3.d. If indent is less than length:
330            (indent < length).then_some(indent)
331        })
332        .min()
333        .unwrap_or(0);
334
335    let mut lines = split_lines(raw_value)
336        .enumerate()
337        // 4.a. For each line in lines:
338        .map(|(index, line)| {
339            // 4.a.i. If line is the first item in lines, continue to the next line.
340            if index == 0 {
341                line
342            } else {
343                // 4.a.ii. Remove commonIndent characters from the beginning of line.
344                &line[common_indent.min(line.len())..]
345            }
346        })
347        // 5. While the first item line in lines contains only Whitespace:
348        // 5.a. Remove the first item from lines.
349        .skip_while(|line| is_whitespace_line(line));
350
351    // (Step 6 is done at the end so we don't need an intermediate allocation.)
352
353    // 7. Let formatted be the empty character sequence.
354    let mut formatted = String::with_capacity(raw_value.len());
355
356    // 8. For each line in lines:
357    // 8.a. If line is the first item in lines:
358    if let Some(line) = lines.next() {
359        // 8.a.i. Append formatted with line.
360        replace_into(line, ESCAPED_TRIPLE_QUOTE, TRIPLE_QUOTE, &mut formatted);
361    };
362
363    let mut final_char_index = formatted.len();
364
365    // 8.b. Otherwise:
366    for line in lines {
367        // 8.b.i. Append formatted with a line feed character (U+000A).
368        formatted.push('\n');
369        // 8.b.ii. Append formatted with line.
370        replace_into(line, ESCAPED_TRIPLE_QUOTE, TRIPLE_QUOTE, &mut formatted);
371
372        // Track the last non-whitespace line for implementing step 6 in the spec.
373        if !is_whitespace_line(line) {
374            final_char_index = formatted.len();
375        }
376    }
377
378    // 6. Implemented differently: remove Whitespace-only lines from the end.
379    formatted.truncate(final_char_index);
380
381    // 9. Return formatted.
382    formatted
383}
384
385// TODO(@goto-bus-stop) As this handles escaping, which can fail in theory, it should be TryFrom
386impl From<&'_ cst::StringValue> for String {
387    fn from(val: &'_ cst::StringValue) -> Self {
388        let text = text_of_first_token(val.syntax());
389        // These slices would panic if the contents are invalid, but the lexer already guarantees that the
390        // string is valid.
391        if is_block_string(&text) {
392            unescape_block_string(&text[3..text.len() - 3])
393        } else {
394            unescape_string(&text[1..text.len() - 1])
395        }
396    }
397}
398
399impl TryFrom<cst::IntValue> for i32 {
400    type Error = ParseIntError;
401
402    fn try_from(val: cst::IntValue) -> Result<Self, Self::Error> {
403        Self::try_from(&val)
404    }
405}
406
407impl TryFrom<&'_ cst::IntValue> for i32 {
408    type Error = ParseIntError;
409
410    fn try_from(val: &'_ cst::IntValue) -> Result<Self, Self::Error> {
411        let text = text_of_first_token(val.syntax());
412        text.parse()
413    }
414}
415
416impl TryFrom<cst::IntValue> for f64 {
417    type Error = ParseFloatError;
418
419    fn try_from(val: cst::IntValue) -> Result<Self, Self::Error> {
420        Self::try_from(&val)
421    }
422}
423
424impl TryFrom<&'_ cst::IntValue> for f64 {
425    type Error = ParseFloatError;
426
427    fn try_from(val: &'_ cst::IntValue) -> Result<Self, Self::Error> {
428        let text = text_of_first_token(val.syntax());
429        text.parse()
430    }
431}
432
433impl TryFrom<cst::FloatValue> for f64 {
434    type Error = ParseFloatError;
435
436    fn try_from(val: cst::FloatValue) -> Result<Self, Self::Error> {
437        Self::try_from(&val)
438    }
439}
440
441impl TryFrom<&'_ cst::FloatValue> for f64 {
442    type Error = ParseFloatError;
443
444    fn try_from(val: &'_ cst::FloatValue) -> Result<Self, Self::Error> {
445        let text = text_of_first_token(val.syntax());
446        text.parse()
447    }
448}
449
450impl TryFrom<cst::BooleanValue> for bool {
451    type Error = std::str::ParseBoolError;
452
453    fn try_from(val: cst::BooleanValue) -> Result<Self, Self::Error> {
454        Self::try_from(&val)
455    }
456}
457
458impl TryFrom<&'_ cst::BooleanValue> for bool {
459    type Error = std::str::ParseBoolError;
460
461    fn try_from(val: &'_ cst::BooleanValue) -> Result<Self, Self::Error> {
462        let text = text_of_first_token(val.syntax());
463        text.parse()
464    }
465}
466
467fn text_of_first_token(node: &SyntaxNode) -> TokenText {
468    let first_token = node
469        .green()
470        .children()
471        .next()
472        .and_then(|it| it.into_token())
473        .unwrap()
474        .to_owned();
475
476    TokenText(first_token)
477}
478
479#[cfg(test)]
480mod string_tests {
481    use super::unescape_string;
482
483    #[test]
484    fn it_parses_strings() {
485        assert_eq!(unescape_string(r"simple"), "simple");
486        assert_eq!(unescape_string(r" white space "), " white space ");
487    }
488
489    #[test]
490    fn it_unescapes_strings() {
491        assert_eq!(unescape_string(r#"quote \""#), "quote \"");
492        assert_eq!(
493            unescape_string(r"escaped \n\r\b\t\f"),
494            "escaped \n\r\u{0008}\t\u{000c}"
495        );
496        assert_eq!(unescape_string(r"slashes \\ \/"), r"slashes \ /");
497        assert_eq!(
498            unescape_string("unescaped unicode outside BMP 😀"),
499            "unescaped unicode outside BMP 😀"
500        );
501        assert_eq!(
502            unescape_string(r"unicode \u1234\u5678\u90AB\uCDEF"),
503            "unicode \u{1234}\u{5678}\u{90AB}\u{CDEF}"
504        );
505        assert_eq!(
506            unescape_string(r"variable width \u{0} \u{2708} \u{1F4A9} \u{10FFFF}"),
507            "variable width \u{0} \u{2708} \u{1F4A9} \u{10FFFF}"
508        );
509        assert_eq!(
510            unescape_string(r"surrogate pair \uD83D\uDCA9"),
511            "surrogate pair \u{1F4A9}"
512        );
513        assert_eq!(
514            unescape_string(r"extremes \uD800\uDC00 \uDBFF\uDFFF"),
515            "extremes \u{10000} \u{10FFFF}"
516        );
517    }
518}
519
520#[cfg(test)]
521mod block_string_tests {
522    use super::split_lines;
523    use super::unescape_block_string;
524
525    #[test]
526    fn it_splits_lines_by_graphql_newline_definition() {
527        let plain_newlines: Vec<_> = split_lines(
528            r#"source text
529    with some
530    new
531
532
533    lines
534        "#,
535        )
536        .collect();
537
538        assert_eq!(
539            plain_newlines,
540            [
541                "source text",
542                "    with some",
543                "    new",
544                "",
545                "",
546                "    lines",
547                "        ",
548            ]
549        );
550
551        let different_endings: Vec<_> =
552            split_lines("with\nand\r\nand\rall in the same\r\nstring").collect();
553        assert_eq!(
554            different_endings,
555            ["with", "and", "and", "all in the same", "string",]
556        );
557
558        let empty_string: Vec<_> = split_lines("").collect();
559        assert_eq!(empty_string, [""]);
560
561        let empty_line: Vec<_> = split_lines("\n\r\r\n").collect();
562        assert_eq!(empty_line, ["", "", "", ""]);
563    }
564
565    #[test]
566    fn it_normalizes_block_string_newlines() {
567        assert_eq!(unescape_block_string("multi\nline"), "multi\nline");
568        assert_eq!(unescape_block_string("multi\r\nline"), "multi\nline");
569        assert_eq!(unescape_block_string("multi\rline"), "multi\nline");
570    }
571
572    #[test]
573    fn it_does_not_unescape_block_strings() {
574        assert_eq!(
575            unescape_block_string(r"escaped \n\r\b\t\f"),
576            r"escaped \n\r\b\t\f"
577        );
578        assert_eq!(unescape_block_string(r"slashes \\ \/"), r"slashes \\ \/");
579        assert_eq!(
580            unescape_block_string("unescaped unicode outside BMP \u{1f600}"),
581            "unescaped unicode outside BMP \u{1f600}"
582        );
583    }
584
585    #[test]
586    fn it_dedents_block_strings() {
587        assert_eq!(
588            unescape_block_string("  intact whitespace with one line  "),
589            "  intact whitespace with one line  "
590        );
591
592        assert_eq!(
593            unescape_block_string(
594                r"
595            This is
596            indented
597            quite a lot
598    "
599            ),
600            r"This is
601indented
602quite a lot"
603        );
604
605        assert_eq!(
606            unescape_block_string(
607                r"
608
609        spans
610          multiple
611            lines
612
613    "
614            ),
615            r"spans
616  multiple
617    lines"
618        );
619    }
620}