Skip to main content

libgraphql_parser/
graphql_parser.rs

1//! Recursive descent parser for GraphQL documents.
2//!
3//! This module provides [`GraphQLParser`], a generic parser that works with any
4//! token source implementing [`GraphQLTokenSource`]. It supports parsing schema
5//! documents, executable documents, and mixed documents.
6//!
7//! # Architecture
8//!
9//! The parser uses recursive descent with a delimiter stack for error recovery.
10//! Most grammar rules have a corresponding `parse_*` method that returns
11//! `Result<AstNode, ()>`, where `Err(())` indicates a parse error was recorded
12//! and the caller should attempt recovery.
13//!
14//! # Error Recovery
15//!
16//! When an error is encountered:
17//! 1. An error is recorded via `record_error()`
18//! 2. The method returns `Err(())`
19//! 3. The caller can attempt recovery (e.g., skip to next definition)
20//!
21//! This allows collecting multiple errors in a single parse pass.
22
23use crate::ast;
24use crate::DefinitionKind;
25use crate::DocumentKind;
26use crate::GraphQLParseError;
27use crate::GraphQLParseErrorKind;
28use crate::GraphQLSourceSpan;
29use crate::GraphQLTokenStream;
30use crate::ParseResult;
31use crate::ReservedNameContext;
32use crate::SourcePosition;
33use crate::ValueParsingError;
34use crate::token::GraphQLToken;
35use crate::token::GraphQLTokenKind;
36use crate::token_source::GraphQLTokenSource;
37use crate::token_source::StrGraphQLTokenSource;
38use smallvec::SmallVec;
39use std::borrow::Cow;
40
41// =============================================================================
42// Delimiter tracking for error recovery
43// =============================================================================
44
45/// Context in which a delimiter was opened, for error messages.
46#[derive(Debug, Clone, Copy)]
47enum DelimiterContext {
48    /// `schema { ... }`
49    SchemaDefinition,
50    /// `type Foo { ... }` (object type definitions)
51    ObjectTypeDefinition,
52    /// `interface Foo { ... }`
53    InterfaceDefinition,
54    /// `enum Foo { ... }`
55    EnumDefinition,
56    /// `input Foo { ... }`
57    InputObjectDefinition,
58    /// `{ field ... }` in operations/fragments
59    SelectionSet,
60    /// `(arg: value)` in field arguments
61    FieldArguments,
62    /// `@directive(arg: value)` in directive arguments
63    DirectiveArguments,
64    /// `($var: Type)` in operation variable definitions
65    VariableDefinitions,
66    /// `[Type]` in type annotations
67    ListType,
68    /// `[value, ...]` in list literals
69    ListValue,
70    /// `{ field: value }` in object literals
71    ObjectValue,
72    /// `(name: Type)` in field/directive argument definitions
73    ArgumentDefinitions,
74}
75
76impl DelimiterContext {
77    /// Returns a human-readable description of this context.
78    fn description(&self) -> &'static str {
79        match self {
80            DelimiterContext::SchemaDefinition => "schema definition",
81            DelimiterContext::ObjectTypeDefinition => "object type definition",
82            DelimiterContext::InterfaceDefinition => "interface definition",
83            DelimiterContext::EnumDefinition => "enum definition",
84            DelimiterContext::InputObjectDefinition => "input object definition",
85            DelimiterContext::SelectionSet => "selection set",
86            DelimiterContext::FieldArguments => "field arguments",
87            DelimiterContext::DirectiveArguments => "directive arguments",
88            DelimiterContext::VariableDefinitions => "variable definitions",
89            DelimiterContext::ListType => "list type annotation",
90            DelimiterContext::ListValue => "list value",
91            DelimiterContext::ObjectValue => "object value",
92            DelimiterContext::ArgumentDefinitions => "argument definitions",
93        }
94    }
95}
96
97/// Tracks an open delimiter for error recovery.
98#[derive(Debug, Clone)]
99struct OpenDelimiter {
100    /// Where the delimiter was opened
101    span: GraphQLSourceSpan,
102    /// The parsing context (also implicitly identifies the delimiter type)
103    context: DelimiterContext,
104}
105
106/// Internal enum for recovery actions, used to avoid borrow conflicts.
107enum RecoveryAction {
108    /// Stop recovery, we found a valid definition start.
109    Stop,
110    /// Skip this token and continue looking.
111    Skip,
112    /// Check if this keyword starts a definition.
113    CheckKeyword(String),
114    /// Check if this string is a description before a definition.
115    CheckDescription,
116}
117
118/// Context for parsing values, determining whether variables are allowed.
119///
120/// This enum replaces a simple `bool` to provide context-specific error
121/// messages when variables appear in const-only contexts.
122#[derive(Clone, Copy, Debug)]
123enum ConstContext {
124    /// Variables are allowed (e.g., field arguments in operations).
125    AllowVariables,
126    /// Parsing a default value for a variable definition.
127    VariableDefaultValue,
128    /// Parsing a directive argument in a const context.
129    DirectiveArgument,
130    /// Parsing a default value for an input field or argument definition.
131    InputDefaultValue,
132}
133
134impl ConstContext {
135    /// Returns a human-readable description for error messages.
136    ///
137    /// Only called when variables are disallowed, so `AllowVariables` is
138    /// unreachable.
139    fn description(&self) -> &'static str {
140        match self {
141            ConstContext::AllowVariables => {
142                unreachable!("description() called on AllowVariables")
143            }
144            ConstContext::VariableDefaultValue => "variable default values",
145            ConstContext::DirectiveArgument => "directive arguments",
146            ConstContext::InputDefaultValue => "input field default values",
147        }
148    }
149}
150
151// =============================================================================
152// Main parser struct
153// =============================================================================
154
155/// A recursive descent parser for GraphQL documents.
156///
157/// Generic over the token source, enabling parsing from both string input
158/// (`StrGraphQLTokenSource`) and proc-macro input
159/// (`RustMacroGraphQLTokenSource`).
160///
161/// # Usage
162///
163/// ```
164/// use libgraphql_parser::ast;
165/// use libgraphql_parser::GraphQLParser;
166///
167/// let source = "type Query { hello: String }";
168/// let parser = GraphQLParser::new(source);
169/// let result = parser.parse_schema_document();
170///
171/// assert!(result.is_ok());
172/// if let Some(doc) = result.valid_ast() {
173///     assert!(matches!(
174///         doc.definitions[0],
175///         ast::schema::Definition::TypeDefinition(_),
176///     ));
177/// }
178/// ```
179pub struct GraphQLParser<'src, TTokenSource: GraphQLTokenSource<'src>> {
180    /// The underlying token stream with lookahead support.
181    token_stream: GraphQLTokenStream<'src, TTokenSource>,
182
183    /// Accumulated parse errors.
184    errors: Vec<GraphQLParseError>,
185
186    /// Stack of open delimiters for error recovery.
187    ///
188    /// Uses SmallVec to avoid heap allocation for typical nesting depths
189    /// (most GraphQL documents nest fewer than 8 delimiters deep).
190    delimiter_stack: SmallVec<[OpenDelimiter; 8]>,
191
192    /// Current nesting depth for recursive value parsing.
193    ///
194    /// Shared recursion depth counter, incremented on entry to
195    /// `parse_value`, `parse_selection_set`,
196    /// `parse_executable_type_annotation`, and `parse_schema_type_annotation`;
197    /// decremented on exit. Prevents stack overflow from deeply
198    /// nested constructs (e.g., `[[[...` values,
199    /// `{ f { f { ...` selection sets, `[[[String]]]` types).
200    recursion_depth: usize,
201
202    /// End position of the most recently consumed token, used by
203    /// `eof_span()` to anchor EOF errors to the last known source
204    /// location.
205    last_end_position: Option<SourcePosition>,
206}
207
208impl<'src> GraphQLParser<'src, StrGraphQLTokenSource<'src>> {
209    /// Creates a new parser from a string-like source.
210    ///
211    /// Accepts any type that can be referenced as a `str`,
212    /// including `&str`, `&String`, and `&Cow<str>`.
213    ///
214    /// # Example
215    ///
216    /// ```
217    /// use libgraphql_parser::GraphQLParser;
218    ///
219    /// let source = "type Query { hello: String }";
220    /// let parser = GraphQLParser::new(source);
221    /// let result = parser.parse_schema_document();
222    /// assert!(result.is_ok());
223    /// ```
224    pub fn new<S: AsRef<str> + ?Sized>(
225        source: &'src S,
226    ) -> Self {
227        let token_source =
228            StrGraphQLTokenSource::new(source.as_ref());
229        Self::from_token_source(token_source)
230    }
231}
232
233impl<'src, TTokenSource: GraphQLTokenSource<'src>> GraphQLParser<'src, TTokenSource> {
234    /// Maximum nesting depth for recursive parsing (values, selection
235    /// sets, and type annotations).
236    ///
237    /// Prevents stack overflow from adversarial inputs like `[[[[[...`
238    /// with hundreds of unclosed brackets. 64 levels is far beyond any
239    /// realistic GraphQL document (most real-world documents nest
240    /// fewer than 15 levels) while staying safe even in debug builds
241    /// where un-optimized stack frames can be 4-8 KB each.
242    const MAX_RECURSION_DEPTH: usize = 64;
243
244    /// Creates a new parser from a token source.
245    pub fn from_token_source(
246        token_source: TTokenSource,
247    ) -> Self {
248        Self {
249            token_stream: GraphQLTokenStream::new(token_source),
250            errors: Vec::new(),
251            delimiter_stack: SmallVec::new(),
252            recursion_depth: 0,
253            last_end_position: None,
254        }
255    }
256
257    // =========================================================================
258    // Error recording and recovery
259    // =========================================================================
260
261    /// Records a parse error.
262    fn record_error(&mut self, error: GraphQLParseError) {
263        self.errors.push(error);
264    }
265
266    /// Push an open delimiter onto the stack.
267    fn push_delimiter(
268        &mut self,
269        span: GraphQLSourceSpan,
270        context: DelimiterContext,
271    ) {
272        self.delimiter_stack.push(OpenDelimiter { span, context });
273    }
274
275    /// Pop the most recent open delimiter.
276    fn pop_delimiter(&mut self) -> Option<OpenDelimiter> {
277        self.delimiter_stack.pop()
278    }
279
280    /// Skip tokens until we find the start of a new definition.
281    ///
282    /// Definition keywords: `type`, `interface`, `union`, `enum`, `scalar`,
283    /// `input`, `directive`, `schema`, `extend`, `query`, `mutation`,
284    /// `subscription`, `fragment`, or `{` (anonymous query).
285    fn recover_to_next_definition(&mut self) {
286        loop {
287            // Extract info from peek without holding the borrow
288            let action = match self.token_stream.peek() {
289                None => RecoveryAction::Stop,
290                Some(token) => match &token.kind {
291                    GraphQLTokenKind::Eof => RecoveryAction::Stop,
292                    GraphQLTokenKind::CurlyBraceOpen => RecoveryAction::Stop,
293                    GraphQLTokenKind::Name(name) => {
294                        let name_owned = name.to_string();
295                        RecoveryAction::CheckKeyword(name_owned)
296                    }
297                    GraphQLTokenKind::StringValue(_) => {
298                        RecoveryAction::CheckDescription
299                    }
300                    _ => RecoveryAction::Skip,
301                },
302            };
303
304            match action {
305                RecoveryAction::Stop => break,
306                RecoveryAction::Skip => {
307                    self.consume_token();
308                }
309                RecoveryAction::CheckKeyword(keyword) => {
310                    if self.looks_like_definition_start(&keyword) {
311                        break;
312                    }
313                    self.consume_token();
314                }
315                RecoveryAction::CheckDescription => {
316                    // Check if next token after string is a definition keyword
317                    let is_description_for_def =
318                        if let Some(next) = self.token_stream.peek_nth(1)
319                            && let GraphQLTokenKind::Name(name) = &next.kind {
320                            matches!(
321                                name.as_ref(),
322                                "type"
323                                    | "interface"
324                                    | "union"
325                                    | "enum"
326                                    | "scalar"
327                                    | "input"
328                                    | "directive"
329                                    | "schema"
330                                    | "extend"
331                            )
332                        } else {
333                            false
334                        };
335                    if is_description_for_def {
336                        break;
337                    }
338                    self.consume_token();
339                }
340            }
341        }
342        // Clear delimiter stack since we're starting fresh
343        self.delimiter_stack.clear();
344    }
345
346    /// Checks if the current keyword looks like the start of a definition by
347    /// peeking at the next token.
348    ///
349    /// This helps avoid false recovery points like "type: String" where `type`
350    /// appears as a field name rather than a type definition keyword.
351    fn looks_like_definition_start(&mut self, keyword: &str) -> bool {
352        let next = self.token_stream.peek_nth(1);
353
354        match keyword {
355            // Type definitions: `type Name`, `interface Name`, etc.
356            // Next token should be a Name (the type name)
357            "type" | "interface" | "union" | "enum" | "scalar" | "input" => {
358                matches!(
359                    next.map(|t| &t.kind),
360                    Some(
361                        GraphQLTokenKind::Name(_)
362                            | GraphQLTokenKind::True
363                            | GraphQLTokenKind::False
364                            | GraphQLTokenKind::Null
365                    )
366                )
367            }
368
369            // `directive @Name` - next should be @
370            "directive" => {
371                matches!(next.map(|t| &t.kind), Some(GraphQLTokenKind::At))
372            }
373
374            // `schema { ... }` or `schema @directive` - next should be { or @
375            "schema" => {
376                matches!(
377                    next.map(|t| &t.kind),
378                    Some(GraphQLTokenKind::CurlyBraceOpen | GraphQLTokenKind::At)
379                )
380            }
381
382            // `extend type ...` - next should be a type keyword
383            "extend" => {
384                if let Some(next_token) = next {
385                    if let GraphQLTokenKind::Name(n) = &next_token.kind {
386                        matches!(
387                            n.as_ref(),
388                            "type"
389                                | "interface"
390                                | "union"
391                                | "enum"
392                                | "scalar"
393                                | "input"
394                                | "schema"
395                        )
396                    } else {
397                        false
398                    }
399                } else {
400                    false
401                }
402            }
403
404            // Operations: `query Name`, `query {`, `query(`, `query @`
405            "query" | "mutation" | "subscription" => {
406                matches!(
407                    next.map(|t| &t.kind),
408                    Some(
409                        GraphQLTokenKind::Name(_)
410                            | GraphQLTokenKind::True
411                            | GraphQLTokenKind::False
412                            | GraphQLTokenKind::Null
413                            | GraphQLTokenKind::CurlyBraceOpen
414                            | GraphQLTokenKind::ParenOpen
415                            | GraphQLTokenKind::At
416                    )
417                ) || next.is_none() // `query` at EOF is still a recovery point
418            }
419
420            // `fragment Name on Type` - next should be a name (not "on")
421            "fragment" => {
422                if let Some(next_token) = next {
423                    if let GraphQLTokenKind::Name(n) = &next_token.kind {
424                        // Fragment name cannot be "on"
425                        n.as_ref() != "on"
426                    } else {
427                        matches!(
428                            &next_token.kind,
429                            GraphQLTokenKind::True
430                                | GraphQLTokenKind::False
431                                | GraphQLTokenKind::Null
432                        )
433                    }
434                } else {
435                    false
436                }
437            }
438
439            _ => false,
440        }
441    }
442
443    // =========================================================================
444    // Token expectation helpers
445    // =========================================================================
446
447    /// Expects a specific token kind and consumes it.
448    ///
449    /// Returns the owned token if it matches, or records an error
450    /// and returns `Err(())`.
451    fn expect(
452        &mut self,
453        expected_kind: &GraphQLTokenKind,
454    ) -> Result<GraphQLToken<'src>, ()> {
455        // Check token kind via peek (scoped borrow). We extract
456        // what we need for the error path before dropping the
457        // borrow so that consume_token() can be called on the
458        // success path without a clone.
459        let mismatch_info = match self.token_stream.peek() {
460            None => {
461                let span = self.eof_span();
462                self.record_error(GraphQLParseError::new(
463                    format!(
464                        "expected `{}`",
465                        Self::token_kind_display(expected_kind),
466                    ),
467                    span,
468                    GraphQLParseErrorKind::UnexpectedEof {
469                        expected: vec![
470                            Self::token_kind_display(
471                                expected_kind,
472                            ),
473                        ],
474                    },
475                ));
476                return Err(());
477            },
478            Some(token) => {
479                if Self::token_kinds_match(
480                    &token.kind,
481                    expected_kind,
482                ) {
483                    None
484                } else {
485                    Some((
486                        token.span.clone(),
487                        Self::token_kind_display(&token.kind),
488                    ))
489                }
490            },
491        };
492        // Peek borrow is dropped — safe to mutate.
493        if let Some((span, found)) = mismatch_info {
494            self.record_error(GraphQLParseError::new(
495                format!(
496                    "expected `{}`, found `{}`",
497                    Self::token_kind_display(expected_kind),
498                    found,
499                ),
500                span,
501                GraphQLParseErrorKind::UnexpectedToken {
502                    expected: vec![
503                        Self::token_kind_display(expected_kind),
504                    ],
505                    found,
506                },
507            ));
508            Err(())
509        } else {
510            Ok(self.consume_token().unwrap())
511        }
512    }
513
514    /// Expects a name token and returns its value along with its source span.
515    ///
516    /// This is a thin wrapper around [`expect_name_only()`](Self::expect_name_only)
517    /// for callers that need the source span. Use `expect_name_only()` when the
518    /// span isn't needed to avoid an unnecessary clone.
519    ///
520    /// **Important**: Per the GraphQL spec, `true`, `false`, and `null` are
521    /// valid names in most contexts (they match the Name regex). The lexer
522    /// tokenizes them as distinct token kinds for type safety in value
523    /// contexts, but this method accepts them as valid names.
524    fn expect_name(&mut self) -> Result<(Cow<'src, str>, GraphQLSourceSpan), ()> {
525        // Capture span before consuming - peek doesn't consume
526        let span = self
527            .token_stream
528            .peek()
529            .map(|t| t.span.clone())
530            .unwrap_or_else(|| self.eof_span());
531        let name = self.expect_name_only()?;
532        Ok((name, span))
533    }
534
535    /// Expects a name token and returns its value without the span.
536    ///
537    /// Returns a `Cow<'src, str>` to avoid unnecessary allocations when the
538    /// name is borrowed from the source. For `Name` tokens, returns the
539    /// borrowed string; for `true`/`false`/`null` tokens, returns a static
540    /// borrowed string.
541    ///
542    /// This is the core implementation that avoids cloning the span in the
543    /// success case. Use [`expect_name()`](Self::expect_name) when you need
544    /// the source span.
545    ///
546    /// **Important**: Per the GraphQL spec, `true`, `false`, and `null` are
547    /// valid names in most contexts (they match the Name regex). The lexer
548    /// tokenizes them as distinct token kinds for type safety in value
549    /// contexts, but this method accepts them as valid names.
550    fn expect_name_only(
551        &mut self,
552    ) -> Result<Cow<'src, str>, ()> {
553        let mismatch = match self.token_stream.peek() {
554            None => {
555                let span = self.eof_span();
556                self.record_error(GraphQLParseError::new(
557                    "expected name",
558                    span,
559                    GraphQLParseErrorKind::UnexpectedEof {
560                        expected: vec!["name".to_string()],
561                    },
562                ));
563                return Err(());
564            },
565            Some(token) => match &token.kind {
566                GraphQLTokenKind::Name(_)
567                | GraphQLTokenKind::True
568                | GraphQLTokenKind::False
569                | GraphQLTokenKind::Null => None,
570                _ => Some((
571                    token.span.clone(),
572                    Self::token_kind_display(&token.kind),
573                )),
574            },
575        };
576        if let Some((span, found)) = mismatch {
577            self.record_error(GraphQLParseError::new(
578                format!("expected name, found `{found}`"),
579                span,
580                GraphQLParseErrorKind::UnexpectedToken {
581                    expected: vec!["name".to_string()],
582                    found,
583                },
584            ));
585            return Err(());
586        }
587        let token = self.consume_token().unwrap();
588        match token.kind {
589            GraphQLTokenKind::Name(s) => Ok(s),
590            GraphQLTokenKind::True => {
591                Ok(Cow::Borrowed("true"))
592            },
593            GraphQLTokenKind::False => {
594                Ok(Cow::Borrowed("false"))
595            },
596            GraphQLTokenKind::Null => {
597                Ok(Cow::Borrowed("null"))
598            },
599            _ => unreachable!(),
600        }
601    }
602
603    /// Expects a specific keyword (a Name token with specific text).
604    ///
605    /// This is used for GraphQL structural keywords like `query`, `mutation`,
606    /// `type`, `interface`, etc.
607    ///
608    /// # Note on `true`, `false`, `null`
609    ///
610    /// This function does **not** match `True`, `False`, or `Null` tokens.
611    /// Those are lexed as distinct token kinds, not as `Name` tokens. This is
612    /// intentional: `expect_keyword()` is for structural keywords, not for
613    /// boolean/null literals. If you need to accept `true`/`false`/`null` as
614    /// names, use [`expect_name()`](Self::expect_name) instead.
615    // TODO: Ensure test coverage verifies expect_keyword("true") does NOT
616    // match a True token.
617    fn expect_keyword(
618        &mut self,
619        keyword: &str,
620    ) -> Result<GraphQLSourceSpan, ()> {
621        let mismatch = match self.token_stream.peek() {
622            None => {
623                let span = self.eof_span();
624                self.record_error(GraphQLParseError::new(
625                    format!("expected `{keyword}`"),
626                    span,
627                    GraphQLParseErrorKind::UnexpectedEof {
628                        expected: vec![keyword.to_string()],
629                    },
630                ));
631                return Err(());
632            },
633            Some(token) => {
634                if let GraphQLTokenKind::Name(name) = &token.kind
635                    && name.as_ref() == keyword {
636                    None
637                } else {
638                    Some((
639                        token.span.clone(),
640                        Self::token_kind_display(
641                            &token.kind,
642                        ),
643                    ))
644                }
645            },
646        };
647        if let Some((span, found)) = mismatch {
648            self.record_error(GraphQLParseError::new(
649                format!(
650                    "expected `{keyword}`, found `{found}`"
651                ),
652                span,
653                GraphQLParseErrorKind::UnexpectedToken {
654                    expected: vec![keyword.to_string()],
655                    found,
656                },
657            ));
658            return Err(());
659        }
660        Ok(self.consume_token().unwrap().span)
661    }
662
663    /// Checks if the current token is a specific keyword without consuming.
664    ///
665    /// This is used for GraphQL structural keywords like `query`, `mutation`,
666    /// `type`, `interface`, etc.
667    ///
668    /// # Note on `true`, `false`, `null`
669    ///
670    /// This function returns `false` for `True`, `False`, and `Null` tokens,
671    /// even if you call `peek_is_keyword("true")`. Those are lexed as distinct
672    /// token kinds, not as `Name` tokens. This is intentional:
673    /// `peek_is_keyword()` is for structural keywords, not for boolean/null
674    /// literals.
675    // TODO: Ensure test coverage verifies peek_is_keyword("true") returns
676    // false when looking at a True token.
677    fn peek_is_keyword(&mut self, keyword: &str) -> bool {
678        match self.token_stream.peek() {
679            Some(token) => {
680                if let GraphQLTokenKind::Name(name) = &token.kind {
681                    name.as_ref() == keyword
682                } else {
683                    false
684                }
685            }
686            None => false,
687        }
688    }
689
690    /// Checks if the current token matches the given kind without consuming.
691    fn peek_is(&mut self, kind: &GraphQLTokenKind) -> bool {
692        match self.token_stream.peek() {
693            Some(token) => Self::token_kinds_match(&token.kind, kind),
694            None => false,
695        }
696    }
697
698    // =========================================================================
699    // Helper methods
700    // =========================================================================
701
702    /// Consumes the next token from the stream and tracks its end
703    /// position for EOF error reporting.
704    fn consume_token(
705        &mut self,
706    ) -> Option<GraphQLToken<'src>> {
707        let token = self.token_stream.consume();
708        if let Some(ref t) = token {
709            self.last_end_position =
710                Some(t.span.end_exclusive.clone());
711        }
712        token
713    }
714
715    /// Returns a span for EOF errors, anchored to the end of the
716    /// last consumed token if available.
717    fn eof_span(&self) -> GraphQLSourceSpan {
718        if let Some(ref pos) = self.last_end_position {
719            GraphQLSourceSpan::new(pos.clone(), pos.clone())
720        } else {
721            let zero = SourcePosition::new(0, 0, Some(0), 0);
722            GraphQLSourceSpan::new(zero.clone(), zero)
723        }
724    }
725
726    /// Returns a human-readable display string for a token kind.
727    fn token_kind_display(kind: &GraphQLTokenKind) -> String {
728        match kind {
729            GraphQLTokenKind::Ampersand => "&".to_string(),
730            GraphQLTokenKind::At => "@".to_string(),
731            GraphQLTokenKind::Bang => "!".to_string(),
732            GraphQLTokenKind::Colon => ":".to_string(),
733            GraphQLTokenKind::CurlyBraceClose => "}".to_string(),
734            GraphQLTokenKind::CurlyBraceOpen => "{".to_string(),
735            GraphQLTokenKind::Dollar => "$".to_string(),
736            GraphQLTokenKind::Ellipsis => "...".to_string(),
737            GraphQLTokenKind::Equals => "=".to_string(),
738            GraphQLTokenKind::ParenClose => ")".to_string(),
739            GraphQLTokenKind::ParenOpen => "(".to_string(),
740            GraphQLTokenKind::Pipe => "|".to_string(),
741            GraphQLTokenKind::SquareBracketClose => "]".to_string(),
742            GraphQLTokenKind::SquareBracketOpen => "[".to_string(),
743            GraphQLTokenKind::Name(s) => s.to_string(),
744            GraphQLTokenKind::IntValue(s) => s.to_string(),
745            GraphQLTokenKind::FloatValue(s) => s.to_string(),
746            GraphQLTokenKind::StringValue(_) => "string".to_string(),
747            GraphQLTokenKind::True => "true".to_string(),
748            GraphQLTokenKind::False => "false".to_string(),
749            GraphQLTokenKind::Null => "null".to_string(),
750            GraphQLTokenKind::Eof => "end of input".to_string(),
751            GraphQLTokenKind::Error { message, .. } => {
752                format!("tokenization error: {message}")
753            }
754        }
755    }
756
757    /// Compares token kinds for equality, ignoring payload for variant
758    /// matching.
759    ///
760    /// # Structure Note
761    ///
762    /// This function intentionally uses an exhaustive match on `actual` rather
763    /// than a wildcard. This ensures that if a new `GraphQLTokenKind` variant
764    /// is added, the compiler will produce an exhaustive-matching error,
765    /// forcing us to explicitly handle the new variant. Do not refactor this
766    /// to use catch-all match cases.
767    fn token_kinds_match(
768        actual: &GraphQLTokenKind,
769        expected: &GraphQLTokenKind,
770    ) -> bool {
771        match actual {
772            // For payload-carrying variants, we just check the variant matches
773            // (not the payload) since we're checking "is this a Name?" not "is
774            // this the specific name 'foo'?"
775            GraphQLTokenKind::Name(_) => matches!(expected, GraphQLTokenKind::Name(_)),
776            GraphQLTokenKind::IntValue(_) => {
777                matches!(expected, GraphQLTokenKind::IntValue(_))
778            }
779            GraphQLTokenKind::FloatValue(_) => {
780                matches!(expected, GraphQLTokenKind::FloatValue(_))
781            }
782            GraphQLTokenKind::StringValue(_) => {
783                matches!(expected, GraphQLTokenKind::StringValue(_))
784            }
785            GraphQLTokenKind::Error { .. } => {
786                matches!(expected, GraphQLTokenKind::Error { .. })
787            }
788            // Unit variants - exhaustive to catch new variants at compile time
789            GraphQLTokenKind::Ampersand => actual == expected,
790            GraphQLTokenKind::At => actual == expected,
791            GraphQLTokenKind::Bang => actual == expected,
792            GraphQLTokenKind::Colon => actual == expected,
793            GraphQLTokenKind::CurlyBraceClose => actual == expected,
794            GraphQLTokenKind::CurlyBraceOpen => actual == expected,
795            GraphQLTokenKind::Dollar => actual == expected,
796            GraphQLTokenKind::Ellipsis => actual == expected,
797            GraphQLTokenKind::Equals => actual == expected,
798            GraphQLTokenKind::ParenClose => actual == expected,
799            GraphQLTokenKind::ParenOpen => actual == expected,
800            GraphQLTokenKind::Pipe => actual == expected,
801            GraphQLTokenKind::SquareBracketClose => actual == expected,
802            GraphQLTokenKind::SquareBracketOpen => actual == expected,
803            GraphQLTokenKind::True => actual == expected,
804            GraphQLTokenKind::False => actual == expected,
805            GraphQLTokenKind::Null => actual == expected,
806            GraphQLTokenKind::Eof => actual == expected,
807        }
808    }
809
810    /// Handles a lexer error token by converting it to a parse error.
811    fn handle_lexer_error(&mut self, token: &GraphQLToken<'src>) {
812        if let GraphQLTokenKind::Error {
813            message,
814            error_notes,
815        } = &token.kind {
816            self.record_error(GraphQLParseError::from_lexer_error(
817                message.clone(),
818                token.span.clone(),
819                error_notes.clone(),
820            ));
821        }
822    }
823
824    // =========================================================================
825    // Value parsing
826    // =========================================================================
827
828    /// Checks recursion depth and returns an error if the limit is
829    /// exceeded. On success, increments the depth counter; the caller
830    /// must call `exit_recursion()` when done (use the wrapper pattern
831    /// to guarantee this).
832    fn enter_recursion(&mut self) -> Result<(), ()> {
833        self.recursion_depth += 1;
834        if self.recursion_depth > Self::MAX_RECURSION_DEPTH {
835            let span = self
836                .token_stream.peek()
837                .map(|t| t.span.clone())
838                .unwrap_or_else(|| self.eof_span());
839            self.consume_token();
840            self.record_error(GraphQLParseError::new(
841                "maximum nesting depth exceeded",
842                span,
843                GraphQLParseErrorKind::InvalidSyntax,
844            ));
845            self.recursion_depth -= 1;
846            return Err(());
847        }
848        Ok(())
849    }
850
851    /// Decrements the recursion depth counter.
852    fn exit_recursion(&mut self) {
853        self.recursion_depth -= 1;
854    }
855
856    /// Parses a value (literal or variable reference).
857    ///
858    /// The `context` parameter specifies whether variables are allowed and
859    /// provides context for error messages when they're not.
860    fn parse_value(
861        &mut self,
862        context: ConstContext,
863    ) -> Result<ast::Value, ()> {
864        self.enter_recursion()?;
865        let result = self.parse_value_impl(context);
866        self.exit_recursion();
867        result
868    }
869
870    /// Inner implementation of value parsing.
871    fn parse_value_impl(
872        &mut self,
873        context: ConstContext,
874    ) -> Result<ast::Value, ()> {
875        match self.token_stream.peek() {
876            None => {
877                let span = self.eof_span();
878                self.record_error(GraphQLParseError::new(
879                    "expected value",
880                    span,
881                    GraphQLParseErrorKind::UnexpectedEof {
882                        expected: vec!["value".to_string()],
883                    },
884                ));
885                Err(())
886            }
887            Some(token) => {
888                let span = token.span.clone();
889                match &token.kind {
890                    // Variable reference: $name
891                    GraphQLTokenKind::Dollar => {
892                        if !matches!(context, ConstContext::AllowVariables) {
893                            self.consume_token();
894                            self.record_error(GraphQLParseError::new(
895                                format!(
896                                    "variables are not allowed in {}",
897                                    context.description()
898                                ),
899                                span,
900                                GraphQLParseErrorKind::InvalidSyntax,
901                            ));
902                            return Err(());
903                        }
904                        self.consume_token(); // consume $
905                        let name = self.expect_name_only()?;
906                        Ok(ast::Value::Variable(name.into_owned()))
907                    }
908
909                    // Integer literal
910                    GraphQLTokenKind::IntValue(raw) => {
911                        // Call parse_int_value() before consume - result is owned
912                        let parse_result = token.kind.parse_int_value();
913                        match parse_result {
914                            Some(Ok(val)) => {
915                                // GraphQL integers should fit in i32
916                                if val > i32::MAX as i64 || val < i32::MIN as i64 {
917                                    // Clone Cow only in error path (before consume)
918                                    let raw_str = raw.clone().into_owned();
919                                    self.consume_token();
920                                    self.record_error(GraphQLParseError::new(
921                                        format!(
922                                            "integer `{raw_str}` overflows 32-bit integer"
923                                        ),
924                                        span,
925                                        GraphQLParseErrorKind::InvalidValue(
926                                            ValueParsingError::Int(raw_str),
927                                        ),
928                                    ));
929                                    Err(())
930                                } else {
931                                    self.consume_token();
932                                    Ok(ast::Value::Int(ast::Number::from(val as i32)))
933                                }
934                            }
935                            Some(Err(_)) => {
936                                // Clone Cow only in error path (before consume)
937                                let raw_str = raw.clone().into_owned();
938                                self.consume_token();
939                                self.record_error(GraphQLParseError::new(
940                                    format!("invalid integer `{raw_str}`"),
941                                    span,
942                                    GraphQLParseErrorKind::InvalidValue(
943                                        ValueParsingError::Int(raw_str),
944                                    ),
945                                ));
946                                Err(())
947                            }
948                            None => unreachable!(
949                                "parse_int_value returned None for IntValue token"
950                            ),
951                        }
952                    }
953
954                    // Float literal
955                    GraphQLTokenKind::FloatValue(raw) => {
956                        // Call parse_float_value() before consume - result is owned
957                        let parse_result = token.kind.parse_float_value();
958                        match parse_result {
959                            Some(Ok(val)) => {
960                                if val.is_infinite() || val.is_nan() {
961                                    // Clone Cow only in error path (before consume)
962                                    let raw_str = raw.clone().into_owned();
963                                    self.consume_token();
964                                    self.record_error(GraphQLParseError::new(
965                                        format!(
966                                            "float `{raw_str}` is not a finite number"
967                                        ),
968                                        span,
969                                        GraphQLParseErrorKind::InvalidValue(
970                                            ValueParsingError::Float(raw_str),
971                                        ),
972                                    ));
973                                    Err(())
974                                } else {
975                                    self.consume_token();
976                                    Ok(ast::Value::Float(val))
977                                }
978                            }
979                            Some(Err(_)) => {
980                                // Clone Cow only in error path (before consume)
981                                let raw_str = raw.clone().into_owned();
982                                self.consume_token();
983                                self.record_error(GraphQLParseError::new(
984                                    format!("invalid float `{raw_str}`"),
985                                    span,
986                                    GraphQLParseErrorKind::InvalidValue(
987                                        ValueParsingError::Float(raw_str),
988                                    ),
989                                ));
990                                Err(())
991                            }
992                            None => unreachable!(
993                                "parse_float_value returned None for FloatValue token"
994                            ),
995                        }
996                    }
997
998                    // String literal
999                    GraphQLTokenKind::StringValue(_) => {
1000                        // Clone token to avoid borrow issues
1001                        let token_clone = token.clone();
1002                        self.consume_token();
1003                        match token_clone.kind.parse_string_value() {
1004                            Some(Ok(parsed)) => Ok(ast::Value::String(parsed)),
1005                            Some(Err(e)) => {
1006                                self.record_error(GraphQLParseError::new(
1007                                    format!("invalid string: {e}"),
1008                                    span,
1009                                    GraphQLParseErrorKind::InvalidValue(
1010                                        ValueParsingError::String(e),
1011                                    ),
1012                                ));
1013                                Err(())
1014                            }
1015                            None => {
1016                                // Shouldn't happen since we matched StringValue
1017                                self.record_error(GraphQLParseError::new(
1018                                    "invalid string",
1019                                    span,
1020                                    GraphQLParseErrorKind::InvalidSyntax,
1021                                ));
1022                                Err(())
1023                            }
1024                        }
1025                    }
1026
1027                    // Boolean literals
1028                    GraphQLTokenKind::True => {
1029                        self.consume_token();
1030                        Ok(ast::Value::Boolean(true))
1031                    }
1032                    GraphQLTokenKind::False => {
1033                        self.consume_token();
1034                        Ok(ast::Value::Boolean(false))
1035                    }
1036
1037                    // Null literal
1038                    GraphQLTokenKind::Null => {
1039                        self.consume_token();
1040                        Ok(ast::Value::Null)
1041                    }
1042
1043                    // List literal: [value, ...]
1044                    GraphQLTokenKind::SquareBracketOpen => self.parse_list_value(context),
1045
1046                    // Object literal: { field: value, ... }
1047                    GraphQLTokenKind::CurlyBraceOpen => self.parse_object_value(context),
1048
1049                    // Enum value (any other name)
1050                    GraphQLTokenKind::Name(name) => {
1051                        let enum_value = name.to_string();
1052                        self.consume_token();
1053                        Ok(ast::Value::Enum(enum_value))
1054                    }
1055
1056                    // Lexer error
1057                    GraphQLTokenKind::Error { .. } => {
1058                        // TODO: Consider if we can eliminate this clone. It's
1059                        // required because `token` borrows `self` via peek(),
1060                        // and handle_lexer_error() needs &mut self.
1061                        let token = token.clone();
1062                        self.handle_lexer_error(&token);
1063                        self.consume_token();
1064                        Err(())
1065                    }
1066
1067                    // Unexpected token
1068                    _ => {
1069                        let found = Self::token_kind_display(&token.kind);
1070                        self.record_error(GraphQLParseError::new(
1071                            format!("expected value, found `{found}`"),
1072                            span,
1073                            GraphQLParseErrorKind::UnexpectedToken {
1074                                expected: vec!["value".to_string()],
1075                                found,
1076                            },
1077                        ));
1078                        Err(())
1079                    }
1080                }
1081            }
1082        }
1083    }
1084
1085    /// Parses a list value: `[value, value, ...]`
1086    fn parse_list_value(&mut self, context: ConstContext) -> Result<ast::Value, ()> {
1087        let open_token = self.expect(&GraphQLTokenKind::SquareBracketOpen)?;
1088        self.push_delimiter(open_token.span.clone(), DelimiterContext::ListValue);
1089
1090        let mut values = Vec::new();
1091
1092        loop {
1093            if self.peek_is(&GraphQLTokenKind::SquareBracketClose) {
1094                break;
1095            }
1096            if self.token_stream.is_at_end() {
1097                let span = self.eof_span();
1098                let open_delim = self.pop_delimiter();
1099                let mut error = GraphQLParseError::new(
1100                    "unclosed `[`",
1101                    span,
1102                    GraphQLParseErrorKind::UnclosedDelimiter {
1103                        delimiter: "[".to_string(),
1104                    },
1105                );
1106                if let Some(delim) = open_delim {
1107                    error.add_note_with_span("opening `[` here", delim.span);
1108                }
1109                self.record_error(error);
1110                return Err(());
1111            }
1112
1113            match self.parse_value(context) {
1114                Ok(value) => values.push(value),
1115                Err(()) => {
1116                    // Try to recover by skipping to ] or next value
1117                    self.skip_to_list_recovery_point();
1118                    if self.peek_is(&GraphQLTokenKind::SquareBracketClose) {
1119                        break;
1120                    }
1121                }
1122            }
1123        }
1124
1125        self.expect(&GraphQLTokenKind::SquareBracketClose)?;
1126        self.pop_delimiter();
1127
1128        Ok(ast::Value::List(values))
1129    }
1130
1131    /// Parses an object value: `{ field: value, ... }`
1132    fn parse_object_value(&mut self, context: ConstContext) -> Result<ast::Value, ()> {
1133        let open_token = self.expect(&GraphQLTokenKind::CurlyBraceOpen)?;
1134        self.push_delimiter(open_token.span.clone(), DelimiterContext::ObjectValue);
1135
1136        let mut fields = Vec::new();
1137
1138        loop {
1139            if self.peek_is(&GraphQLTokenKind::CurlyBraceClose) {
1140                break;
1141            }
1142            if self.token_stream.is_at_end() {
1143                let span = self.eof_span();
1144                let open_delim = self.pop_delimiter();
1145                let mut error = GraphQLParseError::new(
1146                    "unclosed `{`",
1147                    span,
1148                    GraphQLParseErrorKind::UnclosedDelimiter {
1149                        delimiter: "{".to_string(),
1150                    },
1151                );
1152                if let Some(delim) = open_delim {
1153                    error.add_note_with_span(
1154                        format!(
1155                            "opening `{{` in {} here",
1156                            delim.context.description()
1157                        ),
1158                        delim.span,
1159                    );
1160                }
1161                self.record_error(error);
1162                return Err(());
1163            }
1164
1165            // Parse field name (can be true/false/null per spec)
1166            let field_name = self.expect_name_only()?;
1167            self.expect(&GraphQLTokenKind::Colon)?;
1168            let value = self.parse_value(context)?;
1169
1170            fields.push((field_name.into_owned(), value));
1171        }
1172
1173        self.expect(&GraphQLTokenKind::CurlyBraceClose)?;
1174        self.pop_delimiter();
1175
1176        Ok(ast::Value::Object(fields.into_iter().collect()))
1177    }
1178
1179    /// Skip tokens to find a recovery point within a list value.
1180    ///
1181    /// # Structure Note
1182    ///
1183    /// This function intentionally uses an exhaustive match rather than a
1184    /// wildcard. This ensures that if a new `GraphQLTokenKind` variant is
1185    /// added, the compiler will produce an exhaustive-matching error, forcing
1186    /// us to explicitly decide whether the new variant is a recovery point.
1187    /// Do not refactor this to use catch-all match cases.
1188    fn skip_to_list_recovery_point(&mut self) {
1189        loop {
1190            match self.token_stream.peek() {
1191                None => break,
1192                Some(token) => match &token.kind {
1193                    // End of list or input - stop
1194                    GraphQLTokenKind::SquareBracketClose | GraphQLTokenKind::Eof => break,
1195                    // Value starters - potential recovery point
1196                    GraphQLTokenKind::Dollar
1197                    | GraphQLTokenKind::IntValue(_)
1198                    | GraphQLTokenKind::FloatValue(_)
1199                    | GraphQLTokenKind::StringValue(_)
1200                    | GraphQLTokenKind::True
1201                    | GraphQLTokenKind::False
1202                    | GraphQLTokenKind::Null
1203                    | GraphQLTokenKind::SquareBracketOpen
1204                    | GraphQLTokenKind::CurlyBraceOpen
1205                    | GraphQLTokenKind::Name(_) => break,
1206                    // Skip these tokens (not valid value starters)
1207                    GraphQLTokenKind::Ampersand
1208                    | GraphQLTokenKind::At
1209                    | GraphQLTokenKind::Bang
1210                    | GraphQLTokenKind::Colon
1211                    | GraphQLTokenKind::CurlyBraceClose
1212                    | GraphQLTokenKind::Ellipsis
1213                    | GraphQLTokenKind::Equals
1214                    | GraphQLTokenKind::ParenClose
1215                    | GraphQLTokenKind::ParenOpen
1216                    | GraphQLTokenKind::Pipe
1217                    | GraphQLTokenKind::Error { .. } => {
1218                        self.consume_token();
1219                    }
1220                },
1221            }
1222        }
1223    }
1224
1225    // =========================================================================
1226    // Type annotation parsing
1227    // =========================================================================
1228
1229    /// Parses a type annotation: `TypeName`, `[Type]`, `Type!`, `[Type]!`,
1230    /// etc.
1231    fn parse_executable_type_annotation(
1232        &mut self,
1233    ) -> Result<ast::operation::Type, ()> {
1234        self.enter_recursion()?;
1235        let result = self.parse_executable_type_annotation_impl();
1236        self.exit_recursion();
1237        result
1238    }
1239
1240    /// Inner implementation of type annotation parsing.
1241    fn parse_executable_type_annotation_impl(
1242        &mut self,
1243    ) -> Result<ast::operation::Type, ()> {
1244        let base_type = if self.peek_is(&GraphQLTokenKind::SquareBracketOpen) {
1245            // List type: [InnerType]
1246            self.parse_executable_list_type()?
1247        } else {
1248            // Named type
1249            self.parse_executable_named_type()?
1250        };
1251
1252        // Check for non-null modifier
1253        if self.peek_is(&GraphQLTokenKind::Bang) {
1254            self.consume_token();
1255            Ok(ast::operation::Type::NonNullType(Box::new(base_type)))
1256        } else {
1257            Ok(base_type)
1258        }
1259    }
1260
1261    /// Parses a named type annotation (just the type name).
1262    fn parse_executable_named_type(&mut self) -> Result<ast::operation::Type, ()> {
1263        let name = self.expect_name_only()?;
1264        Ok(ast::operation::Type::NamedType(name.into_owned()))
1265    }
1266
1267    /// Parses a list type annotation: `[InnerType]`
1268    fn parse_executable_list_type(&mut self) -> Result<ast::operation::Type, ()> {
1269        let open_token = self.expect(&GraphQLTokenKind::SquareBracketOpen)?;
1270        self.push_delimiter(open_token.span.clone(), DelimiterContext::ListType);
1271
1272        let inner = self.parse_executable_type_annotation()?;
1273
1274        self.expect(&GraphQLTokenKind::SquareBracketClose)?;
1275        self.pop_delimiter();
1276
1277        Ok(ast::operation::Type::ListType(Box::new(inner)))
1278    }
1279
1280    // =========================================================================
1281    // Directive annotation parsing
1282    // =========================================================================
1283
1284    /// Parses zero or more directive annotations: `@directive(args)...`
1285    fn parse_directive_annotations(
1286        &mut self,
1287    ) -> Result<Vec<ast::operation::Directive>, ()> {
1288        let mut directives = Vec::new();
1289        while self.peek_is(&GraphQLTokenKind::At) {
1290            directives.push(self.parse_directive_annotation()?);
1291        }
1292        Ok(directives)
1293    }
1294
1295    /// Parses a single directive annotation: `@name` or `@name(args)`
1296    fn parse_directive_annotation(&mut self) -> Result<ast::operation::Directive, ()> {
1297        // Performance: Extract AstPos (16 bytes, Copy) immediately from the
1298        // span rather than storing the full GraphQLSourceSpan (~104 bytes with
1299        // Option<PathBuf>). The span is consumed and dropped here; only the
1300        // lightweight position is retained.
1301        let position = self
1302            .expect(&GraphQLTokenKind::At)?
1303            .span
1304            .start_inclusive
1305            .to_ast_pos();
1306        let name = self.expect_name_only()?;
1307
1308        let arguments = if self.peek_is(&GraphQLTokenKind::ParenOpen) {
1309            self.parse_arguments(DelimiterContext::DirectiveArguments)?
1310        } else {
1311            Vec::new()
1312        };
1313
1314        Ok(ast::operation::Directive {
1315            position,
1316            name: name.into_owned(),
1317            arguments,
1318        })
1319    }
1320
1321    /// Parses directive annotations that may appear in const contexts
1322    /// (directive arguments must be const values).
1323    fn parse_const_directive_annotations(
1324        &mut self,
1325    ) -> Result<Vec<ast::operation::Directive>, ()> {
1326        let mut directives = Vec::new();
1327        while self.peek_is(&GraphQLTokenKind::At) {
1328            directives.push(self.parse_const_directive_annotation()?);
1329        }
1330        Ok(directives)
1331    }
1332
1333    /// Parses a directive annotation with const-only arguments.
1334    fn parse_const_directive_annotation(
1335        &mut self,
1336    ) -> Result<ast::operation::Directive, ()> {
1337        // Performance: Extract AstPos (16 bytes, Copy) immediately from the
1338        // span rather than storing the full GraphQLSourceSpan (~104 bytes with
1339        // Option<PathBuf>). The span is consumed and dropped here; only the
1340        // lightweight position is retained.
1341        let position = self
1342            .expect(&GraphQLTokenKind::At)?
1343            .span
1344            .start_inclusive
1345            .to_ast_pos();
1346        let name = self.expect_name_only()?;
1347
1348        let arguments = if self.peek_is(&GraphQLTokenKind::ParenOpen) {
1349            self.parse_const_arguments(DelimiterContext::DirectiveArguments)?
1350        } else {
1351            Vec::new()
1352        };
1353
1354        Ok(ast::operation::Directive {
1355            position,
1356            name: name.into_owned(),
1357            arguments,
1358        })
1359    }
1360
1361    // =========================================================================
1362    // Arguments parsing
1363    // =========================================================================
1364
1365    /// Parses arguments: `(name: value, ...)`
1366    fn parse_arguments(
1367        &mut self,
1368        context: DelimiterContext,
1369    ) -> Result<Vec<(String, ast::Value)>, ()> {
1370        let open_token = self.expect(&GraphQLTokenKind::ParenOpen)?;
1371        self.push_delimiter(open_token.span.clone(), context);
1372
1373        let mut arguments = Vec::new();
1374
1375        // Check for empty arguments
1376        if self.peek_is(&GraphQLTokenKind::ParenClose) {
1377            let span = open_token.span.clone();
1378            self.record_error(GraphQLParseError::new(
1379                "argument list cannot be empty; omit the parentheses instead",
1380                span,
1381                GraphQLParseErrorKind::InvalidEmptyConstruct {
1382                    construct: "argument list".to_string(),
1383                },
1384            ));
1385        }
1386
1387        loop {
1388            if self.peek_is(&GraphQLTokenKind::ParenClose) {
1389                break;
1390            }
1391            if self.token_stream.is_at_end() {
1392                self.handle_unclosed_paren();
1393                return Err(());
1394            }
1395
1396            let arg_name = self.expect_name_only()?;
1397            self.expect(&GraphQLTokenKind::Colon)?;
1398            let value = self.parse_value(ConstContext::AllowVariables)?;
1399
1400            arguments.push((arg_name.into_owned(), value));
1401        }
1402
1403        self.expect(&GraphQLTokenKind::ParenClose)?;
1404        self.pop_delimiter();
1405
1406        Ok(arguments)
1407    }
1408
1409    /// Parses arguments with const-only values.
1410    fn parse_const_arguments(
1411        &mut self,
1412        context: DelimiterContext,
1413    ) -> Result<Vec<(String, ast::Value)>, ()> {
1414        let open_token = self.expect(&GraphQLTokenKind::ParenOpen)?;
1415        self.push_delimiter(open_token.span.clone(), context);
1416
1417        let mut arguments = Vec::new();
1418
1419        if self.peek_is(&GraphQLTokenKind::ParenClose) {
1420            let span = open_token.span.clone();
1421            self.record_error(GraphQLParseError::new(
1422                "argument list cannot be empty; omit the parentheses instead",
1423                span,
1424                GraphQLParseErrorKind::InvalidEmptyConstruct {
1425                    construct: "argument list".to_string(),
1426                },
1427            ));
1428        }
1429
1430        loop {
1431            if self.peek_is(&GraphQLTokenKind::ParenClose) {
1432                break;
1433            }
1434            if self.token_stream.is_at_end() {
1435                self.handle_unclosed_paren();
1436                return Err(());
1437            }
1438
1439            let arg_name = self.expect_name_only()?;
1440            self.expect(&GraphQLTokenKind::Colon)?;
1441            let value = self.parse_value(ConstContext::DirectiveArgument)?;
1442
1443            arguments.push((arg_name.into_owned(), value));
1444        }
1445
1446        self.expect(&GraphQLTokenKind::ParenClose)?;
1447        self.pop_delimiter();
1448
1449        Ok(arguments)
1450    }
1451
1452    /// Helper for unclosed parenthesis errors.
1453    fn handle_unclosed_paren(&mut self) {
1454        let span = self.eof_span();
1455        let open_delim = self.pop_delimiter();
1456        let mut error = GraphQLParseError::new(
1457            "unclosed `(`",
1458            span,
1459            GraphQLParseErrorKind::UnclosedDelimiter {
1460                delimiter: "(".to_string(),
1461            },
1462        );
1463        if let Some(delim) = open_delim {
1464            error.add_note_with_span(
1465                format!("opening `(` in {} here", delim.context.description()),
1466                delim.span,
1467            );
1468        }
1469        self.record_error(error);
1470    }
1471
1472    // =========================================================================
1473    // Selection set parsing
1474    // =========================================================================
1475
1476    /// Parses a selection set: `{ selection... }`
1477    fn parse_selection_set(
1478        &mut self,
1479    ) -> Result<ast::operation::SelectionSet, ()> {
1480        self.enter_recursion()?;
1481        let result = self.parse_selection_set_impl();
1482        self.exit_recursion();
1483        result
1484    }
1485
1486    /// Inner implementation of selection set parsing.
1487    fn parse_selection_set_impl(
1488        &mut self,
1489    ) -> Result<ast::operation::SelectionSet, ()> {
1490        let open_token = self.expect(&GraphQLTokenKind::CurlyBraceOpen)?;
1491        // Performance: Store only the AstPos (Copy) from the open brace, not
1492        // the full GraphQLToken or GraphQLSourceSpan. The close brace position
1493        // will be extracted similarly when we reach it.
1494        let open_pos = open_token.span.start_inclusive.to_ast_pos();
1495        self.push_delimiter(open_token.span.clone(), DelimiterContext::SelectionSet);
1496
1497        let mut selections = Vec::new();
1498
1499        // Check for empty selection set
1500        if self.peek_is(&GraphQLTokenKind::CurlyBraceClose) {
1501            let span = open_token.span.clone();
1502            self.record_error(GraphQLParseError::new(
1503                "selection set cannot be empty",
1504                span,
1505                GraphQLParseErrorKind::InvalidEmptyConstruct {
1506                    construct: "selection set".to_string(),
1507                },
1508            ));
1509        }
1510
1511        loop {
1512            if self.peek_is(&GraphQLTokenKind::CurlyBraceClose) {
1513                break;
1514            }
1515            if self.token_stream.is_at_end() {
1516                self.handle_unclosed_brace();
1517                return Err(());
1518            }
1519
1520            match self.parse_selection() {
1521                Ok(selection) => selections.push(selection),
1522                Err(()) => {
1523                    // Try to recover by skipping to next selection or }
1524                    self.skip_to_selection_recovery_point();
1525                }
1526            }
1527        }
1528
1529        // Performance: Extract AstPos (16 bytes, Copy) immediately from the
1530        // close brace span rather than storing the full GraphQLSourceSpan.
1531        let close_token = self.expect(&GraphQLTokenKind::CurlyBraceClose)?;
1532        let close_pos = close_token.span.start_inclusive.to_ast_pos();
1533        self.pop_delimiter();
1534
1535        Ok(ast::operation::SelectionSet {
1536            span: (open_pos, close_pos),
1537            items: selections,
1538        })
1539    }
1540
1541    /// Parses a single selection (field, fragment spread, or inline fragment).
1542    fn parse_selection(&mut self) -> Result<ast::operation::Selection, ()> {
1543        if self.peek_is(&GraphQLTokenKind::Ellipsis) {
1544            // Fragment spread or inline fragment.
1545            // Performance: Extract AstPos (16 bytes, Copy) immediately from the
1546            // span rather than storing the full GraphQLSourceSpan (~104 bytes
1547            // with Option<PathBuf>). Pass AstPos by value (Copy) to helpers.
1548            let ellipsis_pos = self
1549                .expect(&GraphQLTokenKind::Ellipsis)?
1550                .span
1551                .start_inclusive
1552                .to_ast_pos();
1553
1554            if self.peek_is_keyword("on")
1555                || self.peek_is(&GraphQLTokenKind::At)
1556                || self.peek_is(&GraphQLTokenKind::CurlyBraceOpen) {
1557                // Inline fragment
1558                // Performance: Pass AstPos by value (Copy, 16 bytes) rather
1559                // than GraphQLSourceSpan by reference, as the callee only needs
1560                // the position.
1561                self.parse_inline_fragment(ellipsis_pos)
1562            } else {
1563                // Fragment spread
1564                // Performance: Pass AstPos by value (Copy, 16 bytes) rather
1565                // than GraphQLSourceSpan by reference, as the callee only needs
1566                // the position.
1567                self.parse_fragment_spread(ellipsis_pos)
1568            }
1569        } else {
1570            // Field
1571            self.parse_field().map(ast::operation::Selection::Field)
1572        }
1573    }
1574
1575    /// Parses a field: `alias: name(args) @directives { selections }`
1576    fn parse_field(&mut self) -> Result<ast::operation::Field, ()> {
1577        // Parse name or alias. We use expect_name() (not expect_name_only()) to
1578        // capture the span for position tracking. The position is the start of
1579        // the field, which could be an alias or the field name itself.
1580        // Performance: Extract AstPos (16 bytes, Copy) immediately from the
1581        // span rather than storing the full GraphQLSourceSpan (~104 bytes with
1582        // Option<PathBuf>). The span is consumed and dropped here; only the
1583        // lightweight position is retained.
1584        let (first_name, first_span) = self.expect_name()?;
1585        let position = first_span.start_inclusive.to_ast_pos();
1586
1587        // Check for alias
1588        let (alias, name) = if self.peek_is(&GraphQLTokenKind::Colon) {
1589            self.consume_token();
1590            let field_name = self.expect_name_only()?;
1591            (Some(first_name), field_name)
1592        } else {
1593            (None, first_name)
1594        };
1595
1596        // Parse arguments
1597        let arguments = if self.peek_is(&GraphQLTokenKind::ParenOpen) {
1598            self.parse_arguments(DelimiterContext::FieldArguments)?
1599        } else {
1600            Vec::new()
1601        };
1602
1603        // Parse directives
1604        let directives = self.parse_directive_annotations()?;
1605
1606        // Parse nested selection set
1607        let selection_set = if self.peek_is(&GraphQLTokenKind::CurlyBraceOpen) {
1608            self.parse_selection_set()?
1609        } else {
1610            // Performance: For fields without a selection set, use the field's
1611            // position (already extracted as AstPos) for the empty span rather
1612            // than (0,0). This provides useful location context for tooling
1613            // while avoiding any additional span extraction.
1614            ast::operation::SelectionSet {
1615                span: (position, position),
1616                items: Vec::new(),
1617            }
1618        };
1619
1620        Ok(ast::operation::Field {
1621            position,
1622            alias: alias.map(|a| a.into_owned()),
1623            name: name.into_owned(),
1624            arguments,
1625            directives,
1626            selection_set,
1627        })
1628    }
1629
1630    /// Parses a fragment spread: `...FragmentName @directives`
1631    /// (called after consuming `...`)
1632    ///
1633    /// # Arguments
1634    /// * `position` - The position of the `...` token, passed as `AstPos`
1635    ///   (Copy, 16 bytes) rather than `GraphQLSourceSpan` (~104 bytes, contains
1636    ///   `Option<PathBuf>`) to avoid unnecessary allocation/copying of the full
1637    ///   span when only the start position is needed for the AST node.
1638    fn parse_fragment_spread(
1639        &mut self,
1640        position: ast::AstPos,
1641    ) -> Result<ast::operation::Selection, ()> {
1642        let fragment_name = self.expect_name_only()?;
1643        let directives = self.parse_directive_annotations()?;
1644
1645        Ok(ast::operation::Selection::FragmentSpread(
1646            ast::operation::FragmentSpread {
1647                position,
1648                fragment_name: fragment_name.into_owned(),
1649                directives,
1650            },
1651        ))
1652    }
1653
1654    /// Parses an inline fragment: `... on Type @directives { selections }`
1655    /// or `... @directives { selections }` (called after consuming `...`)
1656    ///
1657    /// # Arguments
1658    /// * `position` - The position of the `...` token, passed as `AstPos`
1659    ///   (Copy, 16 bytes) rather than `GraphQLSourceSpan` (~104 bytes, contains
1660    ///   `Option<PathBuf>`) to avoid unnecessary allocation/copying of the full
1661    ///   span when only the start position is needed for the AST node.
1662    fn parse_inline_fragment(
1663        &mut self,
1664        position: ast::AstPos,
1665    ) -> Result<ast::operation::Selection, ()> {
1666        // Optional type condition
1667        let type_condition = if self.peek_is_keyword("on") {
1668            self.consume_token(); // consume 'on'
1669            let type_name = self.expect_name_only()?;
1670            Some(ast::operation::TypeCondition::On(type_name.into_owned()))
1671        } else {
1672            None
1673        };
1674
1675        let directives = self.parse_directive_annotations()?;
1676        let selection_set = self.parse_selection_set()?;
1677
1678        Ok(ast::operation::Selection::InlineFragment(
1679            ast::operation::InlineFragment {
1680                position,
1681                type_condition,
1682                directives,
1683                selection_set,
1684            },
1685        ))
1686    }
1687
1688    /// Skip tokens to find a recovery point within a selection set.
1689    fn skip_to_selection_recovery_point(&mut self) {
1690        loop {
1691            match self.token_stream.peek() {
1692                None => break,
1693                Some(token) => match &token.kind {
1694                    GraphQLTokenKind::CurlyBraceClose | GraphQLTokenKind::Eof => break,
1695                    // Selection starters
1696                    GraphQLTokenKind::Ellipsis | GraphQLTokenKind::Name(_) => break,
1697                    // Also treat true/false/null as potential field names
1698                    GraphQLTokenKind::True
1699                    | GraphQLTokenKind::False
1700                    | GraphQLTokenKind::Null => break,
1701                    _ => {
1702                        self.consume_token();
1703                    }
1704                },
1705            }
1706        }
1707    }
1708
1709    /// Helper for unclosed brace errors.
1710    fn handle_unclosed_brace(&mut self) {
1711        let span = self.eof_span();
1712        let open_delim = self.pop_delimiter();
1713        let mut error = GraphQLParseError::new(
1714            "unclosed `{`",
1715            span,
1716            GraphQLParseErrorKind::UnclosedDelimiter {
1717                delimiter: "{".to_string(),
1718            },
1719        );
1720        if let Some(delim) = open_delim {
1721            error.add_note_with_span(
1722                format!(
1723                    "opening `{{` in {} here",
1724                    delim.context.description()
1725                ),
1726                delim.span,
1727            );
1728        }
1729        self.record_error(error);
1730    }
1731
1732    // =========================================================================
1733    // Operation parsing
1734    // =========================================================================
1735
1736    /// Parses an operation definition.
1737    fn parse_operation_definition(
1738        &mut self,
1739    ) -> Result<ast::operation::OperationDefinition, ()> {
1740        // Check for shorthand query (just a selection set)
1741        if self.peek_is(&GraphQLTokenKind::CurlyBraceOpen) {
1742            let selection_set = self.parse_selection_set()?;
1743            return Ok(ast::operation::OperationDefinition::SelectionSet(
1744                selection_set,
1745            ));
1746        }
1747
1748        // Parse operation type keyword and capture position.
1749        // Performance: Extract AstPos (16 bytes, Copy) immediately from the
1750        // span rather than storing the full GraphQLSourceSpan (~104 bytes with
1751        // Option<PathBuf>). The span is consumed and dropped here; only the
1752        // lightweight position is retained.
1753        let (op_type, position) = if self.peek_is_keyword("query") {
1754            (
1755                "query",
1756                self.expect_keyword("query")?.start_inclusive.to_ast_pos(),
1757            )
1758        } else if self.peek_is_keyword("mutation") {
1759            (
1760                "mutation",
1761                self.expect_keyword("mutation")?
1762                    .start_inclusive
1763                    .to_ast_pos(),
1764            )
1765        } else if self.peek_is_keyword("subscription") {
1766            (
1767                "subscription",
1768                self.expect_keyword("subscription")?
1769                    .start_inclusive
1770                    .to_ast_pos(),
1771            )
1772        } else {
1773            let span = self
1774                .token_stream.peek()
1775                .map(|t| t.span.clone())
1776                .unwrap_or_else(|| self.eof_span());
1777            let found = self
1778                .token_stream.peek()
1779                .map(|t| Self::token_kind_display(&t.kind))
1780                .unwrap_or_else(|| "end of input".to_string());
1781            self.record_error(GraphQLParseError::new(
1782                format!(
1783                    "expected operation type (`query`, `mutation`, or \
1784                    `subscription`), found `{found}`"
1785                ),
1786                span,
1787                GraphQLParseErrorKind::UnexpectedToken {
1788                    expected: vec![
1789                        "query".to_string(),
1790                        "mutation".to_string(),
1791                        "subscription".to_string(),
1792                    ],
1793                    found,
1794                },
1795            ));
1796            return Err(());
1797        };
1798
1799        // Optional operation name
1800        let name = if !self.peek_is(&GraphQLTokenKind::ParenOpen)
1801            && !self.peek_is(&GraphQLTokenKind::At)
1802            && !self.peek_is(&GraphQLTokenKind::CurlyBraceOpen) {
1803            if let Some(token) = self.token_stream.peek() {
1804                match &token.kind {
1805                    GraphQLTokenKind::Name(_)
1806                    | GraphQLTokenKind::True
1807                    | GraphQLTokenKind::False
1808                    | GraphQLTokenKind::Null => {
1809                        let n = self.expect_name_only()?;
1810                        Some(n)
1811                    }
1812                    _ => None,
1813                }
1814            } else {
1815                None
1816            }
1817        } else {
1818            None
1819        };
1820
1821        // Optional variable definitions
1822        let variable_definitions = if self.peek_is(&GraphQLTokenKind::ParenOpen) {
1823            self.parse_variable_definitions()?
1824        } else {
1825            Vec::new()
1826        };
1827
1828        // Optional directives
1829        let directives = self.parse_directive_annotations()?;
1830
1831        // Required selection set
1832        let selection_set = self.parse_selection_set()?;
1833
1834        // Build the appropriate operation type
1835        let name = name.map(|n| n.into_owned());
1836        match op_type {
1837            "query" => Ok(ast::operation::OperationDefinition::Query(
1838                ast::operation::Query {
1839                    position,
1840                    name,
1841                    variable_definitions,
1842                    directives,
1843                    selection_set,
1844                },
1845            )),
1846            "mutation" => Ok(ast::operation::OperationDefinition::Mutation(
1847                ast::operation::Mutation {
1848                    position,
1849                    name,
1850                    variable_definitions,
1851                    directives,
1852                    selection_set,
1853                },
1854            )),
1855            "subscription" => Ok(ast::operation::OperationDefinition::Subscription(
1856                ast::operation::Subscription {
1857                    position,
1858                    name,
1859                    variable_definitions,
1860                    directives,
1861                    selection_set,
1862                },
1863            )),
1864            _ => unreachable!(),
1865        }
1866    }
1867
1868    /// Parses variable definitions: `($var: Type = default, ...)`
1869    fn parse_variable_definitions(
1870        &mut self,
1871    ) -> Result<Vec<ast::operation::VariableDefinition>, ()> {
1872        let open_token = self.expect(&GraphQLTokenKind::ParenOpen)?;
1873        self.push_delimiter(
1874            open_token.span.clone(),
1875            DelimiterContext::VariableDefinitions,
1876        );
1877
1878        let mut definitions = Vec::new();
1879
1880        if self.peek_is(&GraphQLTokenKind::ParenClose) {
1881            let span = open_token.span.clone();
1882            self.record_error(GraphQLParseError::new(
1883                "variable definitions cannot be empty; omit the parentheses \
1884                instead",
1885                span,
1886                GraphQLParseErrorKind::InvalidEmptyConstruct {
1887                    construct: "variable definitions".to_string(),
1888                },
1889            ));
1890        }
1891
1892        loop {
1893            if self.peek_is(&GraphQLTokenKind::ParenClose) {
1894                break;
1895            }
1896            if self.token_stream.is_at_end() {
1897                self.handle_unclosed_paren();
1898                return Err(());
1899            }
1900
1901            definitions.push(self.parse_variable_definition()?);
1902        }
1903
1904        self.expect(&GraphQLTokenKind::ParenClose)?;
1905        self.pop_delimiter();
1906
1907        Ok(definitions)
1908    }
1909
1910    /// Parses a single variable definition: `$name: Type = default @directives`
1911    fn parse_variable_definition(
1912        &mut self,
1913    ) -> Result<ast::operation::VariableDefinition, ()> {
1914        // Performance: Extract AstPos (16 bytes, Copy) immediately from the
1915        // span rather than storing the full GraphQLSourceSpan (~104 bytes with
1916        // Option<PathBuf>). The span is consumed and dropped here; only the
1917        // lightweight position is retained.
1918        let position = self
1919            .expect(&GraphQLTokenKind::Dollar)?
1920            .span
1921            .start_inclusive
1922            .to_ast_pos();
1923        let name = self.expect_name_only()?;
1924        self.expect(&GraphQLTokenKind::Colon)?;
1925        let var_type = self.parse_executable_type_annotation()?;
1926
1927        // Optional default value
1928        let default_value = if self.peek_is(&GraphQLTokenKind::Equals) {
1929            self.consume_token();
1930            Some(self.parse_value(ConstContext::VariableDefaultValue)?)
1931        } else {
1932            None
1933        };
1934
1935        // Note: Variable directives are supported in the GraphQL spec but not
1936        // in the graphql_parser crate's AST. We parse and discard them for now.
1937        // TODO: Track these when we have a custom AST.
1938        let _directives = self.parse_const_directive_annotations()?;
1939
1940        Ok(ast::operation::VariableDefinition {
1941            position,
1942            name: name.into_owned(),
1943            var_type,
1944            default_value,
1945        })
1946    }
1947
1948    // =========================================================================
1949    // Fragment parsing
1950    // =========================================================================
1951
1952    /// Parses a fragment definition: `fragment Name on Type @directives {
1953    /// ... }`
1954    fn parse_fragment_definition(
1955        &mut self,
1956    ) -> Result<ast::operation::FragmentDefinition, ()> {
1957        // Performance: Extract AstPos (16 bytes, Copy) immediately from the
1958        // span rather than storing the full GraphQLSourceSpan (~104 bytes with
1959        // Option<PathBuf>). The span is consumed and dropped here; only the
1960        // lightweight position is retained.
1961        let position = self
1962            .expect_keyword("fragment")?
1963            .start_inclusive
1964            .to_ast_pos();
1965
1966        // Parse fragment name - must not be "on"
1967        let (name, name_span) = self.expect_name()?;
1968        if name == "on" {
1969            // Still produce AST but record error
1970            let mut error = GraphQLParseError::new(
1971                "fragment name cannot be `on`",
1972                name_span.clone(),
1973                GraphQLParseErrorKind::ReservedName {
1974                    name: "on".to_string(),
1975                    context: ReservedNameContext::FragmentName,
1976                },
1977            );
1978            error.add_spec(
1979                "https://spec.graphql.org/October2021/#sec-Fragment-Name-Uniqueness",
1980            );
1981            self.record_error(error);
1982        }
1983
1984        // Type condition
1985        let type_condition = self.parse_type_condition()?;
1986
1987        // Optional directives
1988        let directives = self.parse_directive_annotations()?;
1989
1990        // Selection set
1991        let selection_set = self.parse_selection_set()?;
1992
1993        Ok(ast::operation::FragmentDefinition {
1994            position,
1995            name: name.into_owned(),
1996            type_condition,
1997            directives,
1998            selection_set,
1999        })
2000    }
2001
2002    /// Parses a type condition: `on TypeName`
2003    fn parse_type_condition(&mut self) -> Result<ast::operation::TypeCondition, ()> {
2004        self.expect_keyword("on")?;
2005        let type_name = self.expect_name_only()?;
2006        Ok(ast::operation::TypeCondition::On(type_name.into_owned()))
2007    }
2008
2009    // =========================================================================
2010    // Type definition parsing
2011    // =========================================================================
2012
2013    /// Parses an optional description (string before a definition).
2014    fn parse_description(&mut self) -> Option<String> {
2015        if let Some(token) = self.token_stream.peek()
2016            && matches!(&token.kind, GraphQLTokenKind::StringValue(_)) {
2017            let token = self.consume_token().unwrap();
2018            match token.kind.parse_string_value() {
2019                Some(Ok(parsed)) => return Some(parsed),
2020                Some(Err(err)) => {
2021                    self.record_error(GraphQLParseError::new(
2022                        format!(
2023                            "invalid string in description: {err}"
2024                        ),
2025                        token.span,
2026                        GraphQLParseErrorKind::InvalidSyntax,
2027                    ));
2028                },
2029                None => unreachable!(),
2030            }
2031        }
2032        None
2033    }
2034
2035    /// Parses a schema definition: `schema @directives { query: Query, ... }`
2036    fn parse_schema_definition(&mut self) -> Result<ast::schema::SchemaDefinition, ()> {
2037        // Performance: Extract AstPos (16 bytes, Copy) immediately from the
2038        // span rather than storing the full GraphQLSourceSpan (~104 bytes with
2039        // Option<PathBuf>). The span is consumed and dropped here; only the
2040        // lightweight position is retained.
2041        let position = self
2042            .expect_keyword("schema")?
2043            .start_inclusive
2044            .to_ast_pos();
2045
2046        let directives = self.parse_const_directive_annotations()?;
2047
2048        let open_token = self.expect(&GraphQLTokenKind::CurlyBraceOpen)?;
2049        self.push_delimiter(
2050            open_token.span.clone(),
2051            DelimiterContext::SchemaDefinition,
2052        );
2053
2054        let mut query = None;
2055        let mut mutation = None;
2056        let mut subscription = None;
2057
2058        loop {
2059            if self.peek_is(&GraphQLTokenKind::CurlyBraceClose) {
2060                break;
2061            }
2062            if self.token_stream.is_at_end() {
2063                self.handle_unclosed_brace();
2064                return Err(());
2065            }
2066
2067            let (operation_type, operation_type_span) =
2068                self.expect_name()?;
2069            self.expect(&GraphQLTokenKind::Colon)?;
2070            let type_name = self.expect_name_only()?;
2071
2072            match &*operation_type {
2073                "query" => query = Some(type_name.into_owned()),
2074                "mutation" => {
2075                    mutation = Some(type_name.into_owned())
2076                },
2077                "subscription" => {
2078                    subscription = Some(type_name.into_owned())
2079                },
2080                _ => {
2081                    self.record_error(GraphQLParseError::new(
2082                        format!(
2083                            "unknown operation type \
2084                            `{operation_type}`; expected \
2085                            `query`, `mutation`, or \
2086                            `subscription`"
2087                        ),
2088                        operation_type_span,
2089                        GraphQLParseErrorKind::InvalidSyntax,
2090                    ));
2091                }
2092            }
2093        }
2094
2095        self.expect(&GraphQLTokenKind::CurlyBraceClose)?;
2096        self.pop_delimiter();
2097
2098        // Convert directives for schema type
2099        let schema_directives = self.convert_directives_to_schema(directives);
2100
2101        Ok(ast::schema::SchemaDefinition {
2102            position,
2103            directives: schema_directives,
2104            query,
2105            mutation,
2106            subscription,
2107        })
2108    }
2109
2110    /// Parses a scalar type definition: `scalar Name @directives`
2111    fn parse_scalar_type_definition(
2112        &mut self,
2113        description: Option<String>,
2114    ) -> Result<ast::schema::TypeDefinition, ()> {
2115        // Performance: Extract AstPos (16 bytes, Copy) immediately from the
2116        // span rather than storing the full GraphQLSourceSpan (~104 bytes with
2117        // Option<PathBuf>). The span is consumed and dropped here; only the
2118        // lightweight position is retained.
2119        let position = self.expect_keyword("scalar")?.start_inclusive.to_ast_pos();
2120        let name = self.expect_name_only()?;
2121        let directives = self.parse_const_directive_annotations()?;
2122
2123        let schema_directives = self.convert_directives_to_schema(directives);
2124
2125        Ok(ast::schema::TypeDefinition::Scalar(ast::schema::ScalarType {
2126            position,
2127            description,
2128            name: name.into_owned(),
2129            directives: schema_directives,
2130        }))
2131    }
2132
2133    /// Parses an object type definition: `type Name implements I & J
2134    /// @directives { fields }`
2135    fn parse_object_type_definition(
2136        &mut self,
2137        description: Option<String>,
2138    ) -> Result<ast::schema::TypeDefinition, ()> {
2139        // Performance: Extract AstPos (16 bytes, Copy) immediately from the
2140        // span rather than storing the full GraphQLSourceSpan (~104 bytes with
2141        // Option<PathBuf>). The span is consumed and dropped here; only the
2142        // lightweight position is retained.
2143        let position = self.expect_keyword("type")?.start_inclusive.to_ast_pos();
2144        let name = self.expect_name_only()?;
2145
2146        let implements_interfaces = if self.peek_is_keyword("implements") {
2147            self.parse_implements_interfaces()?
2148        } else {
2149            Vec::new()
2150        };
2151
2152        let directives = self.parse_const_directive_annotations()?;
2153        let schema_directives = self.convert_directives_to_schema(directives);
2154
2155        let fields = if self.peek_is(&GraphQLTokenKind::CurlyBraceOpen) {
2156            self.parse_fields_definition(DelimiterContext::ObjectTypeDefinition)?
2157        } else {
2158            Vec::new()
2159        };
2160
2161        Ok(ast::schema::TypeDefinition::Object(ast::schema::ObjectType {
2162            position,
2163            description,
2164            name: name.into_owned(),
2165            implements_interfaces,
2166            directives: schema_directives,
2167            fields,
2168        }))
2169    }
2170
2171    /// Parses an interface type definition.
2172    fn parse_interface_type_definition(
2173        &mut self,
2174        description: Option<String>,
2175    ) -> Result<ast::schema::TypeDefinition, ()> {
2176        // Performance: Extract AstPos (16 bytes, Copy) immediately from the
2177        // span rather than storing the full GraphQLSourceSpan (~104 bytes with
2178        // Option<PathBuf>). The span is consumed and dropped here; only the
2179        // lightweight position is retained.
2180        let position = self
2181            .expect_keyword("interface")?
2182            .start_inclusive
2183            .to_ast_pos();
2184        let name = self.expect_name_only()?;
2185
2186        let implements_interfaces = if self.peek_is_keyword("implements") {
2187            self.parse_implements_interfaces()?
2188        } else {
2189            Vec::new()
2190        };
2191
2192        let directives = self.parse_const_directive_annotations()?;
2193        let schema_directives = self.convert_directives_to_schema(directives);
2194
2195        let fields = if self.peek_is(&GraphQLTokenKind::CurlyBraceOpen) {
2196            self.parse_fields_definition(DelimiterContext::InterfaceDefinition)?
2197        } else {
2198            Vec::new()
2199        };
2200
2201        Ok(ast::schema::TypeDefinition::Interface(
2202            ast::schema::InterfaceType {
2203                position,
2204                description,
2205                name: name.into_owned(),
2206                implements_interfaces,
2207                directives: schema_directives,
2208                fields,
2209            },
2210        ))
2211    }
2212
2213    /// Parses a union type definition: `union Name @directives = A | B | C`
2214    fn parse_union_type_definition(
2215        &mut self,
2216        description: Option<String>,
2217    ) -> Result<ast::schema::TypeDefinition, ()> {
2218        // Performance: Extract AstPos (16 bytes, Copy) immediately from the
2219        // span rather than storing the full GraphQLSourceSpan (~104 bytes with
2220        // Option<PathBuf>). The span is consumed and dropped here; only the
2221        // lightweight position is retained.
2222        let position = self.expect_keyword("union")?.start_inclusive.to_ast_pos();
2223        let name = self.expect_name_only()?;
2224
2225        let directives = self.parse_const_directive_annotations()?;
2226        let schema_directives = self.convert_directives_to_schema(directives);
2227
2228        let mut types = Vec::new();
2229        if self.peek_is(&GraphQLTokenKind::Equals) {
2230            self.consume_token();
2231
2232            // Optional leading |
2233            if self.peek_is(&GraphQLTokenKind::Pipe) {
2234                self.consume_token();
2235            }
2236
2237            let first_type = self.expect_name_only()?;
2238            types.push(first_type.into_owned());
2239
2240            while self.peek_is(&GraphQLTokenKind::Pipe) {
2241                self.consume_token();
2242                let member_type = self.expect_name_only()?;
2243                types.push(member_type.into_owned());
2244            }
2245        }
2246
2247        Ok(ast::schema::TypeDefinition::Union(ast::schema::UnionType {
2248            position,
2249            description,
2250            name: name.into_owned(),
2251            directives: schema_directives,
2252            types,
2253        }))
2254    }
2255
2256    /// Parses an enum type definition: `enum Name @directives { VALUES }`
2257    fn parse_enum_type_definition(
2258        &mut self,
2259        description: Option<String>,
2260    ) -> Result<ast::schema::TypeDefinition, ()> {
2261        // Performance: Extract AstPos (16 bytes, Copy) immediately from the
2262        // span rather than storing the full GraphQLSourceSpan (~104 bytes with
2263        // Option<PathBuf>). The span is consumed and dropped here; only the
2264        // lightweight position is retained.
2265        let position = self.expect_keyword("enum")?.start_inclusive.to_ast_pos();
2266        let name = self.expect_name_only()?;
2267
2268        let directives = self.parse_const_directive_annotations()?;
2269        let schema_directives = self.convert_directives_to_schema(directives);
2270
2271        let values = if self.peek_is(&GraphQLTokenKind::CurlyBraceOpen) {
2272            self.parse_enum_values_definition()?
2273        } else {
2274            Vec::new()
2275        };
2276
2277        Ok(ast::schema::TypeDefinition::Enum(ast::schema::EnumType {
2278            position,
2279            description,
2280            name: name.into_owned(),
2281            directives: schema_directives,
2282            values,
2283        }))
2284    }
2285
2286    /// Parses an input object type definition.
2287    fn parse_input_object_type_definition(
2288        &mut self,
2289        description: Option<String>,
2290    ) -> Result<ast::schema::TypeDefinition, ()> {
2291        // Performance: Extract AstPos (16 bytes, Copy) immediately from the
2292        // span rather than storing the full GraphQLSourceSpan (~104 bytes with
2293        // Option<PathBuf>). The span is consumed and dropped here; only the
2294        // lightweight position is retained.
2295        let position = self.expect_keyword("input")?.start_inclusive.to_ast_pos();
2296        let name = self.expect_name_only()?;
2297
2298        let directives = self.parse_const_directive_annotations()?;
2299        let schema_directives = self.convert_directives_to_schema(directives);
2300
2301        let fields = if self.peek_is(&GraphQLTokenKind::CurlyBraceOpen) {
2302            self.parse_input_fields_definition()?
2303        } else {
2304            Vec::new()
2305        };
2306
2307        Ok(ast::schema::TypeDefinition::InputObject(
2308            ast::schema::InputObjectType {
2309                position,
2310                description,
2311                name: name.into_owned(),
2312                directives: schema_directives,
2313                fields,
2314            },
2315        ))
2316    }
2317
2318    /// Parses a directive definition.
2319    fn parse_directive_definition(
2320        &mut self,
2321        description: Option<String>,
2322    ) -> Result<ast::schema::DirectiveDefinition, ()> {
2323        // Performance: Extract AstPos (16 bytes, Copy) immediately from the
2324        // span rather than storing the full GraphQLSourceSpan (~104 bytes with
2325        // Option<PathBuf>). The span is consumed and dropped here; only the
2326        // lightweight position is retained.
2327        let position = self
2328            .expect_keyword("directive")?
2329            .start_inclusive
2330            .to_ast_pos();
2331        self.expect(&GraphQLTokenKind::At)?;
2332        let name = self.expect_name_only()?;
2333
2334        let arguments = if self.peek_is(&GraphQLTokenKind::ParenOpen) {
2335            self.parse_arguments_definition()?
2336        } else {
2337            Vec::new()
2338        };
2339
2340        let repeatable = if self.peek_is_keyword("repeatable") {
2341            self.consume_token();
2342            true
2343        } else {
2344            false
2345        };
2346
2347        self.expect_keyword("on")?;
2348
2349        // Parse directive locations
2350        let locations = self.parse_directive_locations()?;
2351
2352        Ok(ast::schema::DirectiveDefinition {
2353            position,
2354            description,
2355            name: name.into_owned(),
2356            arguments,
2357            repeatable,
2358            locations,
2359        })
2360    }
2361
2362    /// Parses implements interfaces: `implements A & B & C`
2363    fn parse_implements_interfaces(&mut self) -> Result<Vec<String>, ()> {
2364        self.expect_keyword("implements")?;
2365
2366        // Optional leading &
2367        if self.peek_is(&GraphQLTokenKind::Ampersand) {
2368            self.consume_token();
2369        }
2370
2371        let mut interfaces = Vec::new();
2372        let first = self.expect_name_only()?;
2373        interfaces.push(first.into_owned());
2374
2375        while self.peek_is(&GraphQLTokenKind::Ampersand) {
2376            self.consume_token();
2377            let iface = self.expect_name_only()?;
2378            interfaces.push(iface.into_owned());
2379        }
2380
2381        Ok(interfaces)
2382    }
2383
2384    /// Parses field definitions: `{ field: Type, ... }`
2385    fn parse_fields_definition(
2386        &mut self,
2387        context: DelimiterContext,
2388    ) -> Result<Vec<ast::schema::Field>, ()> {
2389        let open_token = self.expect(&GraphQLTokenKind::CurlyBraceOpen)?;
2390        self.push_delimiter(open_token.span.clone(), context);
2391
2392        let mut fields = Vec::new();
2393
2394        loop {
2395            if self.peek_is(&GraphQLTokenKind::CurlyBraceClose) {
2396                break;
2397            }
2398            if self.token_stream.is_at_end() {
2399                self.handle_unclosed_brace();
2400                return Err(());
2401            }
2402
2403            fields.push(self.parse_field_definition()?);
2404        }
2405
2406        self.expect(&GraphQLTokenKind::CurlyBraceClose)?;
2407        self.pop_delimiter();
2408
2409        Ok(fields)
2410    }
2411
2412    /// Parses a single field definition.
2413    fn parse_field_definition(&mut self) -> Result<ast::schema::Field, ()> {
2414        let description = self.parse_description();
2415        // Use expect_name() (not expect_name_only()) to capture the span for
2416        // position tracking.
2417        // Performance: Extract AstPos (16 bytes, Copy) immediately from the
2418        // span rather than storing the full GraphQLSourceSpan (~104 bytes with
2419        // Option<PathBuf>). The span is consumed and dropped here; only the
2420        // lightweight position is retained.
2421        let (name, name_span) = self.expect_name()?;
2422        let position = name_span.start_inclusive.to_ast_pos();
2423
2424        let arguments = if self.peek_is(&GraphQLTokenKind::ParenOpen) {
2425            self.parse_arguments_definition()?
2426        } else {
2427            Vec::new()
2428        };
2429
2430        self.expect(&GraphQLTokenKind::Colon)?;
2431        let field_type = self.parse_schema_type_annotation()?;
2432
2433        let directives = self.parse_const_directive_annotations()?;
2434        let schema_directives = self.convert_directives_to_schema(directives);
2435
2436        Ok(ast::schema::Field {
2437            position,
2438            description,
2439            name: name.into_owned(),
2440            arguments,
2441            field_type,
2442            directives: schema_directives,
2443        })
2444    }
2445
2446    /// Parses argument definitions: `(arg: Type = default, ...)`
2447    fn parse_arguments_definition(&mut self) -> Result<Vec<ast::schema::InputValue>, ()> {
2448        let open_token = self.expect(&GraphQLTokenKind::ParenOpen)?;
2449        self.push_delimiter(
2450            open_token.span.clone(),
2451            DelimiterContext::ArgumentDefinitions,
2452        );
2453
2454        let mut arguments = Vec::new();
2455
2456        loop {
2457            if self.peek_is(&GraphQLTokenKind::ParenClose) {
2458                break;
2459            }
2460            if self.token_stream.is_at_end() {
2461                self.handle_unclosed_paren();
2462                return Err(());
2463            }
2464
2465            arguments.push(self.parse_input_value_definition()?);
2466        }
2467
2468        self.expect(&GraphQLTokenKind::ParenClose)?;
2469        self.pop_delimiter();
2470
2471        Ok(arguments)
2472    }
2473
2474    /// Parses input fields definition (for input objects).
2475    fn parse_input_fields_definition(
2476        &mut self,
2477    ) -> Result<Vec<ast::schema::InputValue>, ()> {
2478        let open_token = self.expect(&GraphQLTokenKind::CurlyBraceOpen)?;
2479        self.push_delimiter(
2480            open_token.span.clone(),
2481            DelimiterContext::InputObjectDefinition,
2482        );
2483
2484        let mut fields = Vec::new();
2485
2486        loop {
2487            if self.peek_is(&GraphQLTokenKind::CurlyBraceClose) {
2488                break;
2489            }
2490            if self.token_stream.is_at_end() {
2491                self.handle_unclosed_brace();
2492                return Err(());
2493            }
2494
2495            fields.push(self.parse_input_value_definition()?);
2496        }
2497
2498        self.expect(&GraphQLTokenKind::CurlyBraceClose)?;
2499        self.pop_delimiter();
2500
2501        Ok(fields)
2502    }
2503
2504    /// Parses an input value definition (used for arguments and input fields).
2505    fn parse_input_value_definition(&mut self) -> Result<ast::schema::InputValue, ()> {
2506        let description = self.parse_description();
2507        // Use expect_name() (not expect_name_only()) to capture the span for
2508        // position tracking.
2509        // Performance: Extract AstPos (16 bytes, Copy) immediately from the
2510        // span rather than storing the full GraphQLSourceSpan (~104 bytes with
2511        // Option<PathBuf>). The span is consumed and dropped here; only the
2512        // lightweight position is retained.
2513        let (name, name_span) = self.expect_name()?;
2514        let position = name_span.start_inclusive.to_ast_pos();
2515        self.expect(&GraphQLTokenKind::Colon)?;
2516        let value_type = self.parse_schema_type_annotation()?;
2517
2518        let default_value = if self.peek_is(&GraphQLTokenKind::Equals) {
2519            self.consume_token();
2520            Some(self.parse_value(ConstContext::InputDefaultValue)?)
2521        } else {
2522            None
2523        };
2524
2525        let directives = self.parse_const_directive_annotations()?;
2526        let schema_directives = self.convert_directives_to_schema(directives);
2527
2528        Ok(ast::schema::InputValue {
2529            position,
2530            description,
2531            name: name.into_owned(),
2532            value_type,
2533            default_value,
2534            directives: schema_directives,
2535        })
2536    }
2537
2538    /// Parses enum value definitions.
2539    fn parse_enum_values_definition(
2540        &mut self,
2541    ) -> Result<Vec<ast::schema::EnumValue>, ()> {
2542        let open_token = self.expect(&GraphQLTokenKind::CurlyBraceOpen)?;
2543        self.push_delimiter(
2544            open_token.span.clone(),
2545            DelimiterContext::EnumDefinition,
2546        );
2547
2548        let mut values = Vec::new();
2549
2550        loop {
2551            if self.peek_is(&GraphQLTokenKind::CurlyBraceClose) {
2552                break;
2553            }
2554            if self.token_stream.is_at_end() {
2555                self.handle_unclosed_brace();
2556                return Err(());
2557            }
2558
2559            values.push(self.parse_enum_value_definition()?);
2560        }
2561
2562        self.expect(&GraphQLTokenKind::CurlyBraceClose)?;
2563        self.pop_delimiter();
2564
2565        Ok(values)
2566    }
2567
2568    /// Parses a single enum value definition.
2569    fn parse_enum_value_definition(&mut self) -> Result<ast::schema::EnumValue, ()> {
2570        let description = self.parse_description();
2571
2572        // Check for reserved enum values (true, false, null)
2573        // Performance: Extract AstPos (16 bytes, Copy) immediately from the
2574        // span rather than storing the full GraphQLSourceSpan (~104 bytes with
2575        // Option<PathBuf>). The span is consumed and dropped here; only the
2576        // lightweight position is retained.
2577        let (name, name_span) = self.expect_name()?;
2578        let position = name_span.start_inclusive.to_ast_pos();
2579        if matches!(&*name, "true" | "false" | "null") {
2580            let mut error = GraphQLParseError::new(
2581                format!("enum value cannot be `{name}`"),
2582                name_span,
2583                GraphQLParseErrorKind::ReservedName {
2584                    name: name.clone().into_owned(),
2585                    context: ReservedNameContext::EnumValue,
2586                },
2587            );
2588            error.add_spec(
2589                "https://spec.graphql.org/October2021/#sec-Enum-Value-Uniqueness",
2590            );
2591            self.record_error(error);
2592            // Continue parsing to collect more errors
2593        }
2594
2595        let directives = self.parse_const_directive_annotations()?;
2596        let schema_directives = self.convert_directives_to_schema(directives);
2597
2598        Ok(ast::schema::EnumValue {
2599            position,
2600            description,
2601            name: name.into_owned(),
2602            directives: schema_directives,
2603        })
2604    }
2605
2606    /// Parses directive locations: `FIELD | OBJECT | ...`
2607    fn parse_directive_locations(
2608        &mut self,
2609    ) -> Result<Vec<ast::schema::DirectiveLocation>, ()> {
2610        // Optional leading |
2611        if self.peek_is(&GraphQLTokenKind::Pipe) {
2612            self.consume_token();
2613        }
2614
2615        let mut locations = Vec::new();
2616        locations.push(self.parse_directive_location()?);
2617
2618        while self.peek_is(&GraphQLTokenKind::Pipe) {
2619            self.consume_token();
2620            locations.push(self.parse_directive_location()?);
2621        }
2622
2623        Ok(locations)
2624    }
2625
2626    /// Parses a single directive location.
2627    fn parse_directive_location(
2628        &mut self,
2629    ) -> Result<ast::schema::DirectiveLocation, ()> {
2630        let (name, span) = self.expect_name()?;
2631
2632        // Match against known directive locations
2633        match &*name {
2634            // Executable locations
2635            "QUERY" => Ok(ast::schema::DirectiveLocation::Query),
2636            "MUTATION" => Ok(ast::schema::DirectiveLocation::Mutation),
2637            "SUBSCRIPTION" => Ok(ast::schema::DirectiveLocation::Subscription),
2638            "FIELD" => Ok(ast::schema::DirectiveLocation::Field),
2639            "FRAGMENT_DEFINITION" => {
2640                Ok(ast::schema::DirectiveLocation::FragmentDefinition)
2641            }
2642            "FRAGMENT_SPREAD" => Ok(ast::schema::DirectiveLocation::FragmentSpread),
2643            "INLINE_FRAGMENT" => Ok(ast::schema::DirectiveLocation::InlineFragment),
2644            "VARIABLE_DEFINITION" => {
2645                Ok(ast::schema::DirectiveLocation::VariableDefinition)
2646            }
2647
2648            // Type system locations
2649            "SCHEMA" => Ok(ast::schema::DirectiveLocation::Schema),
2650            "SCALAR" => Ok(ast::schema::DirectiveLocation::Scalar),
2651            "OBJECT" => Ok(ast::schema::DirectiveLocation::Object),
2652            "FIELD_DEFINITION" => Ok(ast::schema::DirectiveLocation::FieldDefinition),
2653            "ARGUMENT_DEFINITION" => {
2654                Ok(ast::schema::DirectiveLocation::ArgumentDefinition)
2655            }
2656            "INTERFACE" => Ok(ast::schema::DirectiveLocation::Interface),
2657            "UNION" => Ok(ast::schema::DirectiveLocation::Union),
2658            "ENUM" => Ok(ast::schema::DirectiveLocation::Enum),
2659            "ENUM_VALUE" => Ok(ast::schema::DirectiveLocation::EnumValue),
2660            "INPUT_OBJECT" => Ok(ast::schema::DirectiveLocation::InputObject),
2661            "INPUT_FIELD_DEFINITION" => {
2662                Ok(ast::schema::DirectiveLocation::InputFieldDefinition)
2663            }
2664
2665            _ => {
2666                // Unknown location - try to suggest closest match
2667                let mut error = GraphQLParseError::new(
2668                    format!("unknown directive location `{name}`"),
2669                    span,
2670                    GraphQLParseErrorKind::InvalidSyntax,
2671                );
2672
2673                if let Some(suggestion) = Self::suggest_directive_location(&name) {
2674                    error.add_help(format!("did you mean `{suggestion}`?"));
2675                }
2676
2677                self.record_error(error);
2678                Err(())
2679            }
2680        }
2681    }
2682
2683    /// Suggests the closest directive location for a typo.
2684    fn suggest_directive_location(input: &str) -> Option<&'static str> {
2685        const LOCATIONS: &[&str] = &[
2686            "QUERY",
2687            "MUTATION",
2688            "SUBSCRIPTION",
2689            "FIELD",
2690            "FRAGMENT_DEFINITION",
2691            "FRAGMENT_SPREAD",
2692            "INLINE_FRAGMENT",
2693            "VARIABLE_DEFINITION",
2694            "SCHEMA",
2695            "SCALAR",
2696            "OBJECT",
2697            "FIELD_DEFINITION",
2698            "ARGUMENT_DEFINITION",
2699            "INTERFACE",
2700            "UNION",
2701            "ENUM",
2702            "ENUM_VALUE",
2703            "INPUT_OBJECT",
2704            "INPUT_FIELD_DEFINITION",
2705        ];
2706
2707        // Simple edit distance for suggestions
2708        let input_upper = input.to_uppercase();
2709        let mut best_match: Option<&'static str> = None;
2710        let mut best_distance = usize::MAX;
2711
2712        for &location in LOCATIONS {
2713            let distance = Self::edit_distance(&input_upper, location);
2714            if distance < best_distance && distance <= 3 {
2715                best_distance = distance;
2716                best_match = Some(location);
2717            }
2718        }
2719
2720        best_match
2721    }
2722
2723    /// Simple Levenshtein edit distance.
2724    fn edit_distance(a: &str, b: &str) -> usize {
2725        let a_chars: Vec<char> = a.chars().collect();
2726        let b_chars: Vec<char> = b.chars().collect();
2727        let m = a_chars.len();
2728        let n = b_chars.len();
2729
2730        if m == 0 {
2731            return n;
2732        }
2733        if n == 0 {
2734            return m;
2735        }
2736
2737        let mut prev: Vec<usize> = (0..=n).collect();
2738        let mut curr = vec![0; n + 1];
2739
2740        for i in 1..=m {
2741            curr[0] = i;
2742            for j in 1..=n {
2743                let cost = if a_chars[i - 1] == b_chars[j - 1] {
2744                    0
2745                } else {
2746                    1
2747                };
2748                curr[j] = (prev[j] + 1)
2749                    .min(curr[j - 1] + 1)
2750                    .min(prev[j - 1] + cost);
2751            }
2752            std::mem::swap(&mut prev, &mut curr);
2753        }
2754
2755        prev[n]
2756    }
2757
2758    /// Parses a type annotation for schema definitions.
2759    fn parse_schema_type_annotation(
2760        &mut self,
2761    ) -> Result<ast::schema::Type, ()> {
2762        self.enter_recursion()?;
2763        let result = self.parse_schema_type_annotation_impl();
2764        self.exit_recursion();
2765        result
2766    }
2767
2768    /// Inner implementation of schema type annotation parsing.
2769    fn parse_schema_type_annotation_impl(
2770        &mut self,
2771    ) -> Result<ast::schema::Type, ()> {
2772        let base_type = if self.peek_is(&GraphQLTokenKind::SquareBracketOpen) {
2773            self.parse_schema_list_type()?
2774        } else {
2775            let name = self.expect_name_only()?;
2776            ast::schema::Type::NamedType(name.into_owned())
2777        };
2778
2779        if self.peek_is(&GraphQLTokenKind::Bang) {
2780            self.consume_token();
2781            Ok(ast::schema::Type::NonNullType(Box::new(base_type)))
2782        } else {
2783            Ok(base_type)
2784        }
2785    }
2786
2787    /// Parses a list type for schema definitions.
2788    fn parse_schema_list_type(&mut self) -> Result<ast::schema::Type, ()> {
2789        let open_token = self.expect(&GraphQLTokenKind::SquareBracketOpen)?;
2790        self.push_delimiter(open_token.span.clone(), DelimiterContext::ListType);
2791
2792        let inner = self.parse_schema_type_annotation()?;
2793
2794        self.expect(&GraphQLTokenKind::SquareBracketClose)?;
2795        self.pop_delimiter();
2796
2797        Ok(ast::schema::Type::ListType(Box::new(inner)))
2798    }
2799
2800    /// Convert operation directives to schema directives.
2801    fn convert_directives_to_schema(
2802        &self,
2803        directives: Vec<ast::operation::Directive>,
2804    ) -> Vec<ast::schema::Directive> {
2805        directives
2806            .into_iter()
2807            .map(|d| ast::schema::Directive {
2808                position: d.position,
2809                name: d.name,
2810                arguments: d.arguments,
2811            })
2812            .collect()
2813    }
2814
2815    // =========================================================================
2816    // Type extension parsing
2817    // =========================================================================
2818
2819    /// Parses a type extension.
2820    ///
2821    /// Note: Schema extensions (`extend schema`) are valid GraphQL but not
2822    /// supported by the underlying graphql_parser crate's AST.
2823    /// TODO: Support schema extensions when we have a custom AST.
2824    fn parse_type_extension(&mut self) -> Result<ast::schema::TypeExtension, ()> {
2825        // Performance: Extract AstPos (16 bytes, Copy) immediately from the
2826        // span rather than storing the full GraphQLSourceSpan (~104 bytes with
2827        // Option<PathBuf>). Pass AstPos by value (Copy) to helper methods.
2828        let extend_pos = self
2829            .expect_keyword("extend")?
2830            .start_inclusive
2831            .to_ast_pos();
2832
2833        if self.peek_is_keyword("schema") {
2834            // Schema extensions are valid GraphQL but not supported by
2835            // graphql_parser crate's AST.
2836            // TODO: Support schema extensions when we have a custom AST.
2837            self.skip_schema_extension()?;
2838            Err(())
2839        } else if self.peek_is_keyword("scalar") {
2840            // Performance: Pass AstPos by value (Copy, 16 bytes) rather than
2841            // GraphQLSourceSpan by reference, as the callee only needs the
2842            // position.
2843            self.parse_scalar_type_extension(extend_pos)
2844        } else if self.peek_is_keyword("type") {
2845            self.parse_object_type_extension(extend_pos)
2846        } else if self.peek_is_keyword("interface") {
2847            self.parse_interface_type_extension(extend_pos)
2848        } else if self.peek_is_keyword("union") {
2849            self.parse_union_type_extension(extend_pos)
2850        } else if self.peek_is_keyword("enum") {
2851            self.parse_enum_type_extension(extend_pos)
2852        } else if self.peek_is_keyword("input") {
2853            self.parse_input_object_type_extension(extend_pos)
2854        } else {
2855            let span = self
2856                .token_stream.peek()
2857                .map(|t| t.span.clone())
2858                .unwrap_or_else(|| self.eof_span());
2859            let found = self
2860                .token_stream.peek()
2861                .map(|t| Self::token_kind_display(&t.kind))
2862                .unwrap_or_else(|| "end of input".to_string());
2863            self.record_error(GraphQLParseError::new(
2864                format!(
2865                    "expected type extension keyword (`schema`, `scalar`, `type`, \
2866                    `interface`, `union`, `enum`, `input`), found `{found}`"
2867                ),
2868                span,
2869                GraphQLParseErrorKind::UnexpectedToken {
2870                    expected: vec![
2871                        "schema".to_string(),
2872                        "scalar".to_string(),
2873                        "type".to_string(),
2874                        "interface".to_string(),
2875                        "union".to_string(),
2876                        "enum".to_string(),
2877                        "input".to_string(),
2878                    ],
2879                    found,
2880                },
2881            ));
2882            Err(())
2883        }
2884    }
2885
2886    /// Skips a schema extension, recording an error that it's unsupported.
2887    ///
2888    /// Schema extensions are valid GraphQL but the graphql_parser crate's AST
2889    /// doesn't have a representation for them.
2890    /// TODO: Support schema extensions when we have a custom AST.
2891    fn skip_schema_extension(&mut self) -> Result<(), ()> {
2892        let start_span = self
2893            .token_stream.peek()
2894            .map(|t| t.span.clone())
2895            .unwrap_or_else(|| self.eof_span());
2896
2897        self.expect_keyword("schema")?;
2898
2899        // Record error for unsupported feature
2900        self.record_error(GraphQLParseError::new(
2901            "schema extensions (`extend schema`) are not yet supported".to_string(),
2902            start_span,
2903            GraphQLParseErrorKind::InvalidSyntax,
2904        ));
2905
2906        // Skip directives
2907        let _ = self.parse_const_directive_annotations();
2908
2909        // Skip body if present
2910        if self.peek_is(&GraphQLTokenKind::CurlyBraceOpen) {
2911            let open_token = self.expect(&GraphQLTokenKind::CurlyBraceOpen)?;
2912            self.push_delimiter(
2913                open_token.span.clone(),
2914                DelimiterContext::SchemaDefinition,
2915            );
2916
2917            loop {
2918                if self.peek_is(&GraphQLTokenKind::CurlyBraceClose) {
2919                    break;
2920                }
2921                if self.token_stream.is_at_end() {
2922                    self.handle_unclosed_brace();
2923                    return Err(());
2924                }
2925
2926                // Skip: operation_type : type_name
2927                let _ = self.expect_name();
2928                let _ = self.expect(&GraphQLTokenKind::Colon);
2929                let _ = self.expect_name();
2930            }
2931
2932            self.expect(&GraphQLTokenKind::CurlyBraceClose)?;
2933            self.pop_delimiter();
2934        }
2935
2936        Ok(())
2937    }
2938
2939    /// Parses a scalar type extension: `extend scalar Name @directives`
2940    ///
2941    /// # Arguments
2942    /// * `position` - The position of the `extend` keyword, passed as `AstPos`
2943    ///   (Copy, 16 bytes) rather than `GraphQLSourceSpan` (~104 bytes, contains
2944    ///   `Option<PathBuf>`) to avoid unnecessary allocation/copying of the full
2945    ///   span when only the start position is needed for the AST node.
2946    fn parse_scalar_type_extension(
2947        &mut self,
2948        position: ast::AstPos,
2949    ) -> Result<ast::schema::TypeExtension, ()> {
2950        self.expect_keyword("scalar")?;
2951        let name = self.expect_name_only()?;
2952        let directives = self.parse_const_directive_annotations()?;
2953        let schema_directives = self.convert_directives_to_schema(directives);
2954
2955        Ok(ast::schema::TypeExtension::Scalar(
2956            ast::schema::ScalarTypeExtension {
2957                position,
2958                name: name.into_owned(),
2959                directives: schema_directives,
2960            },
2961        ))
2962    }
2963
2964    /// Parses an object type extension: `extend type Name implements I & J
2965    /// @directives { fields }`
2966    ///
2967    /// # Arguments
2968    /// * `position` - The position of the `extend` keyword, passed as `AstPos`
2969    ///   (Copy, 16 bytes) rather than `GraphQLSourceSpan` (~104 bytes, contains
2970    ///   `Option<PathBuf>`) to avoid unnecessary allocation/copying of the full
2971    ///   span when only the start position is needed for the AST node.
2972    fn parse_object_type_extension(
2973        &mut self,
2974        position: ast::AstPos,
2975    ) -> Result<ast::schema::TypeExtension, ()> {
2976        self.expect_keyword("type")?;
2977        let name = self.expect_name_only()?;
2978
2979        let implements_interfaces = if self.peek_is_keyword("implements") {
2980            self.parse_implements_interfaces()?
2981        } else {
2982            Vec::new()
2983        };
2984
2985        let directives = self.parse_const_directive_annotations()?;
2986        let schema_directives = self.convert_directives_to_schema(directives);
2987
2988        let fields = if self.peek_is(&GraphQLTokenKind::CurlyBraceOpen) {
2989            self.parse_fields_definition(DelimiterContext::ObjectTypeDefinition)?
2990        } else {
2991            Vec::new()
2992        };
2993
2994        Ok(ast::schema::TypeExtension::Object(
2995            ast::schema::ObjectTypeExtension {
2996                position,
2997                name: name.into_owned(),
2998                implements_interfaces,
2999                directives: schema_directives,
3000                fields,
3001            },
3002        ))
3003    }
3004
3005    /// Parses an interface type extension.
3006    ///
3007    /// # Arguments
3008    /// * `position` - The position of the `extend` keyword, passed as `AstPos`
3009    ///   (Copy, 16 bytes) rather than `GraphQLSourceSpan` (~104 bytes, contains
3010    ///   `Option<PathBuf>`) to avoid unnecessary allocation/copying of the full
3011    ///   span when only the start position is needed for the AST node.
3012    fn parse_interface_type_extension(
3013        &mut self,
3014        position: ast::AstPos,
3015    ) -> Result<ast::schema::TypeExtension, ()> {
3016        self.expect_keyword("interface")?;
3017        let name = self.expect_name_only()?;
3018
3019        let implements_interfaces = if self.peek_is_keyword("implements") {
3020            self.parse_implements_interfaces()?
3021        } else {
3022            Vec::new()
3023        };
3024
3025        let directives = self.parse_const_directive_annotations()?;
3026        let schema_directives = self.convert_directives_to_schema(directives);
3027
3028        let fields = if self.peek_is(&GraphQLTokenKind::CurlyBraceOpen) {
3029            self.parse_fields_definition(DelimiterContext::InterfaceDefinition)?
3030        } else {
3031            Vec::new()
3032        };
3033
3034        Ok(ast::schema::TypeExtension::Interface(
3035            ast::schema::InterfaceTypeExtension {
3036                position,
3037                name: name.into_owned(),
3038                implements_interfaces,
3039                directives: schema_directives,
3040                fields,
3041            },
3042        ))
3043    }
3044
3045    /// Parses a union type extension: `extend union Name @directives = A | B`
3046    ///
3047    /// # Arguments
3048    /// * `position` - The position of the `extend` keyword, passed as `AstPos`
3049    ///   (Copy, 16 bytes) rather than `GraphQLSourceSpan` (~104 bytes, contains
3050    ///   `Option<PathBuf>`) to avoid unnecessary allocation/copying of the full
3051    ///   span when only the start position is needed for the AST node.
3052    fn parse_union_type_extension(
3053        &mut self,
3054        position: ast::AstPos,
3055    ) -> Result<ast::schema::TypeExtension, ()> {
3056        self.expect_keyword("union")?;
3057        let name = self.expect_name_only()?;
3058
3059        let directives = self.parse_const_directive_annotations()?;
3060        let schema_directives = self.convert_directives_to_schema(directives);
3061
3062        let mut types = Vec::new();
3063        if self.peek_is(&GraphQLTokenKind::Equals) {
3064            self.consume_token();
3065
3066            if self.peek_is(&GraphQLTokenKind::Pipe) {
3067                self.consume_token();
3068            }
3069
3070            let first_type = self.expect_name_only()?;
3071            types.push(first_type.into_owned());
3072
3073            while self.peek_is(&GraphQLTokenKind::Pipe) {
3074                self.consume_token();
3075                let member_type = self.expect_name_only()?;
3076                types.push(member_type.into_owned());
3077            }
3078        }
3079
3080        Ok(ast::schema::TypeExtension::Union(
3081            ast::schema::UnionTypeExtension {
3082                position,
3083                name: name.into_owned(),
3084                directives: schema_directives,
3085                types,
3086            },
3087        ))
3088    }
3089
3090    /// Parses an enum type extension: `extend enum Name @directives { VALUES }`
3091    ///
3092    /// # Arguments
3093    /// * `position` - The position of the `extend` keyword, passed as `AstPos`
3094    ///   (Copy, 16 bytes) rather than `GraphQLSourceSpan` (~104 bytes, contains
3095    ///   `Option<PathBuf>`) to avoid unnecessary allocation/copying of the full
3096    ///   span when only the start position is needed for the AST node.
3097    fn parse_enum_type_extension(
3098        &mut self,
3099        position: ast::AstPos,
3100    ) -> Result<ast::schema::TypeExtension, ()> {
3101        self.expect_keyword("enum")?;
3102        let name = self.expect_name_only()?;
3103
3104        let directives = self.parse_const_directive_annotations()?;
3105        let schema_directives = self.convert_directives_to_schema(directives);
3106
3107        let values = if self.peek_is(&GraphQLTokenKind::CurlyBraceOpen) {
3108            self.parse_enum_values_definition()?
3109        } else {
3110            Vec::new()
3111        };
3112
3113        Ok(ast::schema::TypeExtension::Enum(
3114            ast::schema::EnumTypeExtension {
3115                position,
3116                name: name.into_owned(),
3117                directives: schema_directives,
3118                values,
3119            },
3120        ))
3121    }
3122
3123    /// Parses an input object type extension.
3124    ///
3125    /// # Arguments
3126    /// * `position` - The position of the `extend` keyword, passed as `AstPos`
3127    ///   (Copy, 16 bytes) rather than `GraphQLSourceSpan` (~104 bytes, contains
3128    ///   `Option<PathBuf>`) to avoid unnecessary allocation/copying of the full
3129    ///   span when only the start position is needed for the AST node.
3130    fn parse_input_object_type_extension(
3131        &mut self,
3132        position: ast::AstPos,
3133    ) -> Result<ast::schema::TypeExtension, ()> {
3134        self.expect_keyword("input")?;
3135        let name = self.expect_name_only()?;
3136
3137        let directives = self.parse_const_directive_annotations()?;
3138        let schema_directives = self.convert_directives_to_schema(directives);
3139
3140        let fields = if self.peek_is(&GraphQLTokenKind::CurlyBraceOpen) {
3141            self.parse_input_fields_definition()?
3142        } else {
3143            Vec::new()
3144        };
3145
3146        Ok(ast::schema::TypeExtension::InputObject(
3147            ast::schema::InputObjectTypeExtension {
3148                position,
3149                name: name.into_owned(),
3150                directives: schema_directives,
3151                fields,
3152            },
3153        ))
3154    }
3155
3156    // =========================================================================
3157    // Document parsing (public API)
3158    // =========================================================================
3159
3160    /// Parses a schema document (type system definitions only).
3161    pub fn parse_schema_document(mut self) -> ParseResult<ast::schema::Document> {
3162        let mut definitions = Vec::new();
3163
3164        while !self.token_stream.is_at_end() {
3165            match self.parse_schema_definition_item() {
3166                Ok(def) => definitions.push(def),
3167                Err(()) => {
3168                    self.recover_to_next_definition();
3169                }
3170            }
3171        }
3172
3173        let document = ast::schema::Document { definitions };
3174
3175        if self.errors.is_empty() {
3176            ParseResult::ok(document)
3177        } else {
3178            ParseResult::recovered(document, self.errors)
3179        }
3180    }
3181
3182    /// Parses an executable document (operations and fragments only).
3183    pub fn parse_executable_document(
3184        mut self,
3185    ) -> ParseResult<ast::operation::Document> {
3186        let mut definitions = Vec::new();
3187
3188        while !self.token_stream.is_at_end() {
3189            match self.parse_executable_definition_item() {
3190                Ok(def) => definitions.push(def),
3191                Err(()) => {
3192                    self.recover_to_next_definition();
3193                }
3194            }
3195        }
3196
3197        let document = ast::operation::Document { definitions };
3198
3199        if self.errors.is_empty() {
3200            ParseResult::ok(document)
3201        } else {
3202            ParseResult::recovered(document, self.errors)
3203        }
3204    }
3205
3206    /// Parses a mixed document (both type system and executable definitions).
3207    pub fn parse_mixed_document(mut self) -> ParseResult<ast::MixedDocument> {
3208        let mut definitions = Vec::new();
3209
3210        while !self.token_stream.is_at_end() {
3211            match self.parse_mixed_definition_item() {
3212                Ok(def) => definitions.push(def),
3213                Err(()) => {
3214                    self.recover_to_next_definition();
3215                }
3216            }
3217        }
3218
3219        let document = ast::MixedDocument { definitions };
3220
3221        if self.errors.is_empty() {
3222            ParseResult::ok(document)
3223        } else {
3224            ParseResult::recovered(document, self.errors)
3225        }
3226    }
3227
3228    /// Parses a single schema definition item.
3229    fn parse_schema_definition_item(&mut self) -> Result<ast::schema::Definition, ()> {
3230        // Handle lexer errors
3231        if let Some(token) = self.token_stream.peek()
3232            && let GraphQLTokenKind::Error { .. } = &token.kind {
3233                let token = token.clone();
3234                self.handle_lexer_error(&token);
3235                self.consume_token();
3236                return Err(());
3237            }
3238
3239        let description = self.parse_description();
3240
3241        if self.peek_is_keyword("schema") {
3242            Ok(ast::schema::Definition::SchemaDefinition(
3243                self.parse_schema_definition()?,
3244            ))
3245        } else if self.peek_is_keyword("scalar") {
3246            Ok(ast::schema::Definition::TypeDefinition(
3247                self.parse_scalar_type_definition(description)?,
3248            ))
3249        } else if self.peek_is_keyword("type") {
3250            Ok(ast::schema::Definition::TypeDefinition(
3251                self.parse_object_type_definition(description)?,
3252            ))
3253        } else if self.peek_is_keyword("interface") {
3254            Ok(ast::schema::Definition::TypeDefinition(
3255                self.parse_interface_type_definition(description)?,
3256            ))
3257        } else if self.peek_is_keyword("union") {
3258            Ok(ast::schema::Definition::TypeDefinition(
3259                self.parse_union_type_definition(description)?,
3260            ))
3261        } else if self.peek_is_keyword("enum") {
3262            Ok(ast::schema::Definition::TypeDefinition(
3263                self.parse_enum_type_definition(description)?,
3264            ))
3265        } else if self.peek_is_keyword("input") {
3266            Ok(ast::schema::Definition::TypeDefinition(
3267                self.parse_input_object_type_definition(description)?,
3268            ))
3269        } else if self.peek_is_keyword("directive") {
3270            Ok(ast::schema::Definition::DirectiveDefinition(
3271                self.parse_directive_definition(description)?,
3272            ))
3273        } else if self.peek_is_keyword("extend") {
3274            Ok(ast::schema::Definition::TypeExtension(
3275                self.parse_type_extension()?,
3276            ))
3277        } else if self.peek_is_keyword("query")
3278            || self.peek_is_keyword("mutation")
3279            || self.peek_is_keyword("subscription")
3280            || self.peek_is_keyword("fragment")
3281            || self.peek_is(&GraphQLTokenKind::CurlyBraceOpen) {
3282            // Executable definition in schema document - record error
3283            let span = self
3284                .token_stream.peek()
3285                .map(|t| t.span.clone())
3286                .unwrap_or_else(|| self.eof_span());
3287            let kind = if self.peek_is_keyword("fragment") {
3288                DefinitionKind::Fragment
3289            } else {
3290                DefinitionKind::Operation
3291            };
3292            self.record_error(GraphQLParseError::new(
3293                format!(
3294                    "{} not allowed in schema document",
3295                    match kind {
3296                        DefinitionKind::Fragment => "fragment definition",
3297                        DefinitionKind::Operation => "operation definition",
3298                        _ => "definition",
3299                    }
3300                ),
3301                span,
3302                GraphQLParseErrorKind::WrongDocumentKind {
3303                    found: kind,
3304                    document_kind: DocumentKind::Schema,
3305                },
3306            ));
3307            // Consume the token to ensure forward progress during error
3308            // recovery. Without this, recovery sees `fragment`/`query`/etc.
3309            // as a definition start and breaks without consuming, causing
3310            // an infinite loop.
3311            self.consume_token();
3312            Err(())
3313        } else {
3314            let span = self
3315                .token_stream.peek()
3316                .map(|t| t.span.clone())
3317                .unwrap_or_else(|| self.eof_span());
3318            let found = self
3319                .token_stream.peek()
3320                .map(|t| Self::token_kind_display(&t.kind))
3321                .unwrap_or_else(|| "end of input".to_string());
3322            // Consume the token to ensure forward progress during error
3323            // recovery. Without this, recovery sees the unconsumed token
3324            // as a potential definition start and stops immediately,
3325            // causing an infinite loop.
3326            self.consume_token();
3327            self.record_error(GraphQLParseError::new(
3328                format!("expected schema definition, found `{found}`"),
3329                span,
3330                GraphQLParseErrorKind::UnexpectedToken {
3331                    expected: vec![
3332                        "type".to_string(),
3333                        "interface".to_string(),
3334                        "union".to_string(),
3335                        "enum".to_string(),
3336                        "scalar".to_string(),
3337                        "input".to_string(),
3338                        "directive".to_string(),
3339                        "schema".to_string(),
3340                        "extend".to_string(),
3341                    ],
3342                    found,
3343                },
3344            ));
3345            Err(())
3346        }
3347    }
3348
3349    /// Parses a single executable definition item.
3350    fn parse_executable_definition_item(
3351        &mut self,
3352    ) -> Result<ast::operation::Definition, ()> {
3353        // Handle lexer errors
3354        if let Some(token) = self.token_stream.peek()
3355            && let GraphQLTokenKind::Error { .. } = &token.kind {
3356                let token = token.clone();
3357                self.handle_lexer_error(&token);
3358                self.consume_token();
3359                return Err(());
3360            }
3361
3362        if self.peek_is_keyword("query")
3363            || self.peek_is_keyword("mutation")
3364            || self.peek_is_keyword("subscription")
3365            || self.peek_is(&GraphQLTokenKind::CurlyBraceOpen) {
3366            Ok(ast::operation::Definition::Operation(
3367                self.parse_operation_definition()?,
3368            ))
3369        } else if self.peek_is_keyword("fragment") {
3370            Ok(ast::operation::Definition::Fragment(
3371                self.parse_fragment_definition()?,
3372            ))
3373        } else if self.peek_is_keyword("type")
3374            || self.peek_is_keyword("interface")
3375            || self.peek_is_keyword("union")
3376            || self.peek_is_keyword("enum")
3377            || self.peek_is_keyword("scalar")
3378            || self.peek_is_keyword("input")
3379            || self.peek_is_keyword("directive")
3380            || self.peek_is_keyword("schema")
3381            || self.peek_is_keyword("extend") {
3382            // Schema definition in executable document - record error
3383            let span = self
3384                .token_stream.peek()
3385                .map(|t| t.span.clone())
3386                .unwrap_or_else(|| self.eof_span());
3387            let kind = if self.peek_is_keyword("directive") {
3388                DefinitionKind::DirectiveDefinition
3389            } else if self.peek_is_keyword("schema") || self.peek_is_keyword("extend") {
3390                DefinitionKind::Schema
3391            } else {
3392                DefinitionKind::TypeDefinition
3393            };
3394            self.consume_token();
3395            self.record_error(GraphQLParseError::new(
3396                format!(
3397                    "{} not allowed in executable document",
3398                    match kind {
3399                        DefinitionKind::TypeDefinition => "type definition",
3400                        DefinitionKind::DirectiveDefinition => "directive definition",
3401                        DefinitionKind::Schema => "schema definition",
3402                        _ => "definition",
3403                    }
3404                ),
3405                span,
3406                GraphQLParseErrorKind::WrongDocumentKind {
3407                    found: kind,
3408                    document_kind: DocumentKind::Executable,
3409                },
3410            ));
3411            Err(())
3412        } else {
3413            // Check for description followed by type definition (common mistake)
3414            // Extract info from first peek before taking second peek to avoid
3415            // double borrow.
3416            let first_is_string = self
3417                .token_stream.peek()
3418                .map(|t| matches!(&t.kind, GraphQLTokenKind::StringValue(_)))
3419                .unwrap_or(false);
3420
3421            if first_is_string {
3422                // Might be a description - peek ahead to check for type keyword
3423                let is_type_def = self.token_stream.peek_nth(1).is_some_and(|next| {
3424                    if let GraphQLTokenKind::Name(name) = &next.kind {
3425                        matches!(
3426                            name.as_ref(),
3427                            "type"
3428                                | "interface"
3429                                | "union"
3430                                | "enum"
3431                                | "scalar"
3432                                | "input"
3433                                | "directive"
3434                                | "schema"
3435                                | "extend"
3436                        )
3437                    } else {
3438                        false
3439                    }
3440                });
3441
3442                if is_type_def {
3443                    let span = self
3444                        .token_stream.peek()
3445                        .map(|t| t.span.clone())
3446                        .unwrap_or_else(|| self.eof_span());
3447                    self.consume_token();
3448                    self.record_error(GraphQLParseError::new(
3449                        "type definition not allowed in executable document",
3450                        span,
3451                        GraphQLParseErrorKind::WrongDocumentKind {
3452                            found: DefinitionKind::TypeDefinition,
3453                            document_kind: DocumentKind::Executable,
3454                        },
3455                    ));
3456                    return Err(());
3457                }
3458            }
3459
3460            let span = self
3461                .token_stream.peek()
3462                .map(|t| t.span.clone())
3463                .unwrap_or_else(|| self.eof_span());
3464            let found = self
3465                .token_stream.peek()
3466                .map(|t| Self::token_kind_display(&t.kind))
3467                .unwrap_or_else(|| "end of input".to_string());
3468            // Consume the token to ensure forward progress during error
3469            // recovery. Without this, recovery sees the unconsumed token
3470            // as a potential definition start and stops immediately,
3471            // causing an infinite loop.
3472            self.consume_token();
3473            self.record_error(GraphQLParseError::new(
3474                format!(
3475                    "expected operation or fragment definition, found `{found}`"
3476                ),
3477                span,
3478                GraphQLParseErrorKind::UnexpectedToken {
3479                    expected: vec![
3480                        "query".to_string(),
3481                        "mutation".to_string(),
3482                        "subscription".to_string(),
3483                        "fragment".to_string(),
3484                        "{".to_string(),
3485                    ],
3486                    found,
3487                },
3488            ));
3489            Err(())
3490        }
3491    }
3492
3493    /// Parses a definition for mixed documents.
3494    fn parse_mixed_definition_item(&mut self) -> Result<ast::MixedDefinition, ()> {
3495        // Handle lexer errors
3496        if let Some(token) = self.token_stream.peek()
3497            && let GraphQLTokenKind::Error { .. } = &token.kind {
3498                let token = token.clone();
3499                self.handle_lexer_error(&token);
3500                self.consume_token();
3501                return Err(());
3502            }
3503
3504        let description = self.parse_description();
3505
3506        // Schema definitions
3507        if self.peek_is_keyword("schema") {
3508            return Ok(ast::MixedDefinition::Schema(
3509                ast::schema::Definition::SchemaDefinition(self.parse_schema_definition()?),
3510            ));
3511        }
3512        if self.peek_is_keyword("scalar") {
3513            return Ok(ast::MixedDefinition::Schema(
3514                ast::schema::Definition::TypeDefinition(
3515                    self.parse_scalar_type_definition(description)?,
3516                ),
3517            ));
3518        }
3519        if self.peek_is_keyword("type") {
3520            return Ok(ast::MixedDefinition::Schema(
3521                ast::schema::Definition::TypeDefinition(
3522                    self.parse_object_type_definition(description)?,
3523                ),
3524            ));
3525        }
3526        if self.peek_is_keyword("interface") {
3527            return Ok(ast::MixedDefinition::Schema(
3528                ast::schema::Definition::TypeDefinition(
3529                    self.parse_interface_type_definition(description)?,
3530                ),
3531            ));
3532        }
3533        if self.peek_is_keyword("union") {
3534            return Ok(ast::MixedDefinition::Schema(
3535                ast::schema::Definition::TypeDefinition(
3536                    self.parse_union_type_definition(description)?,
3537                ),
3538            ));
3539        }
3540        if self.peek_is_keyword("enum") {
3541            return Ok(ast::MixedDefinition::Schema(
3542                ast::schema::Definition::TypeDefinition(
3543                    self.parse_enum_type_definition(description)?,
3544                ),
3545            ));
3546        }
3547        if self.peek_is_keyword("input") {
3548            return Ok(ast::MixedDefinition::Schema(
3549                ast::schema::Definition::TypeDefinition(
3550                    self.parse_input_object_type_definition(description)?,
3551                ),
3552            ));
3553        }
3554        if self.peek_is_keyword("directive") {
3555            return Ok(ast::MixedDefinition::Schema(
3556                ast::schema::Definition::DirectiveDefinition(
3557                    self.parse_directive_definition(description)?,
3558                ),
3559            ));
3560        }
3561        if self.peek_is_keyword("extend") {
3562            return Ok(ast::MixedDefinition::Schema(
3563                ast::schema::Definition::TypeExtension(self.parse_type_extension()?),
3564            ));
3565        }
3566
3567        // Executable definitions
3568        if self.peek_is_keyword("query")
3569            || self.peek_is_keyword("mutation")
3570            || self.peek_is_keyword("subscription")
3571            || self.peek_is(&GraphQLTokenKind::CurlyBraceOpen) {
3572            return Ok(ast::MixedDefinition::Executable(
3573                ast::operation::Definition::Operation(self.parse_operation_definition()?),
3574            ));
3575        }
3576        if self.peek_is_keyword("fragment") {
3577            return Ok(ast::MixedDefinition::Executable(
3578                ast::operation::Definition::Fragment(self.parse_fragment_definition()?),
3579            ));
3580        }
3581
3582        let span = self
3583            .token_stream.peek()
3584            .map(|t| t.span.clone())
3585            .unwrap_or_else(|| self.eof_span());
3586        let found = self
3587            .token_stream.peek()
3588            .map(|t| Self::token_kind_display(&t.kind))
3589            .unwrap_or_else(|| "end of input".to_string());
3590        // Consume the token to ensure forward progress during error
3591        // recovery. Without this, recovery sees the unconsumed token
3592        // as a potential definition start and stops immediately,
3593        // causing an infinite loop.
3594        self.consume_token();
3595        self.record_error(GraphQLParseError::new(
3596            format!("expected definition, found `{found}`"),
3597            span,
3598            GraphQLParseErrorKind::UnexpectedToken {
3599                expected: vec![
3600                    "type".to_string(),
3601                    "query".to_string(),
3602                    "fragment".to_string(),
3603                ],
3604                found,
3605            },
3606        ));
3607        Err(())
3608    }
3609}