Skip to main content

libgraphql_parser/
parse_result.rs

1//! Result type for parsing operations that may produce partial results.
2
3use crate::ast;
4use crate::token::GraphQLTriviaToken;
5use crate::ByteSpan;
6use crate::GraphQLParseError;
7use crate::SourceMap;
8
9/// The result of a parsing operation.
10///
11/// Unlike `Result<T, E>`, `ParseResult` can represent a *recovered* parse:
12/// one that produced an AST **and** encountered errors. This enables error
13/// recovery — the parser can report multiple errors while still producing
14/// as much AST as possible.
15///
16/// # Variants
17///
18/// | Variant     | AST | Errors | Meaning                                  |
19/// |-------------|-----|--------|------------------------------------------|
20/// | `Ok`        | yes | no     | Completely successful parse               |
21/// | `Recovered` | yes | yes    | Best-effort AST with errors encountered   |
22///
23/// An AST is **always** present — there is no "complete failure" variant.
24/// Even when the parser encounters errors, it produces a partial/recovered
25/// AST via error recovery.
26///
27/// # Design Rationale
28///
29/// Traditional `Result<T, E>` forces a binary choice: either success with a
30/// value, or failure with an error. But GraphQL tooling often benefits from
31/// having both:
32///
33/// - **IDE integration**: Show syntax errors while still providing completions
34///   based on the partially-parsed document
35/// - **Batch error reporting**: Report all syntax errors in one pass rather
36///   than stopping at the first error
37/// - **Graceful degradation**: Process as much of a document as possible even
38///   when parts are invalid
39///
40/// # SourceMap
41///
42/// Every `ParseResult` carries a [`SourceMap`] that maps byte offsets
43/// (stored in [`ByteSpan`](crate::ByteSpan)s on AST nodes and tokens)
44/// to line/column positions on demand, and provides access to the
45/// source text via [`source()`](SourceMap::source). Parse errors carry
46/// pre-resolved [`SourceSpan`](crate::SourceSpan)s and do not need the
47/// `SourceMap` for position resolution.
48///
49/// # Accessing the AST
50///
51/// - [`recovered()`](Self::recovered) - Returns the error-recovered AST, a
52///   `&[GraphQLParseError]`, and a [`SourceMap`] if there were 1 or more errors
53///   parsing the AST. The AST is "recovered" AST in the sense that the parser
54///   made a best-effort attempt at either guessing the intended AST or skipping
55///   any portion of the input for which it could not make a reasonable guess.
56///
57/// - [`valid()`](Self::valid) — Returns the AST and [`SourceMap`] only if
58///   parsing was completely successful (no errors). Use this when you need
59///   guaranteed-valid input.
60///
61/// - [`into_recovered`](Self::into_recovered) - Consuming version of
62///   [`recovered`](Self::recovered).
63///
64/// - [`into_valid()`](Self::into_valid) — Consuming version of
65///   [`valid()`](Self::valid).
66///
67/// - [`into_ast()`](Self::into_ast) — Extracts the AST unconditionally,
68///   consuming the `ParseResult`. Use this for tools that want best-effort
69///   results (formatters, IDE features, linters).
70///
71/// - **Pattern matching** — For borrowing the AST unconditionally while
72///   retaining access to errors, match on the enum variants directly.
73///
74/// # Example
75///
76/// ```
77/// # use libgraphql_parser::ast;
78/// # use libgraphql_parser::GraphQLParser;
79/// # use libgraphql_parser::ParseResult;
80/// # use libgraphql_parser::SourceMap;
81/// #
82/// # let source = "type Query { foo: String }";
83/// # let parser = GraphQLParser::new(source);
84/// #
85/// # fn analyze_schema(schema: &ast::Document<'_>, source_map: &SourceMap<'_>) { }
86/// # fn provide_ide_completions(schema: &ast::Document) { }
87/// #
88/// let result = parser.parse_schema_document();
89///
90/// // Strict mode: only accept fully valid documents
91/// if let Some((doc, source_map)) = result.valid() {
92///     analyze_schema(doc, source_map);
93/// }
94///
95/// // Best-effort mode: match to borrow the AST unconditionally
96/// let ast = match &result {
97///     ParseResult::Ok { ast, .. }
98///     | ParseResult::Recovered { ast, .. } => ast,
99/// };
100/// provide_ide_completions(ast);
101///
102/// // Report any errors
103/// if result.has_errors() {
104///     for error in result.errors() {
105///         eprintln!("{}", error.format_detailed(result.source_map().source()));
106///     }
107/// }
108/// ```
109#[derive(Debug)]
110pub enum ParseResult<'src, TAst> {
111    /// Completely successful parse — the AST is valid and no errors were
112    /// encountered.
113    Ok {
114        /// The parsed AST.
115        ast: TAst,
116
117        /// Maps byte offsets to line/column positions.
118        source_map: SourceMap<'src>,
119    },
120
121    /// Recovered parse — an AST was produced via error recovery, but errors
122    /// were encountered. The AST may be incomplete or contain placeholder
123    /// values.
124    ///
125    /// Invariant: `errors` is always non-empty for this variant.
126    Recovered {
127        /// The recovered AST.
128        ast: TAst,
129
130        /// Errors encountered during parsing (always non-empty).
131        errors: Vec<GraphQLParseError>,
132
133        /// Maps byte offsets to line/column positions.
134        source_map: SourceMap<'src>,
135    },
136}
137
138impl<'src, TAst> ParseResult<'src, TAst> {
139    /// Returns a reference to the AST unconditionally.
140    ///
141    /// An AST is always present in a `ParseResult`, even when
142    /// parsing errors may have occurred. For the consuming
143    /// version, see [`into_ast()`](Self::into_ast).
144    pub fn ast(&self) -> &TAst {
145        match self {
146            Self::Ok { ast, .. } => ast,
147            Self::Recovered { ast, .. } => ast,
148        }
149    }
150
151    /// Returns the errors encountered during parsing.
152    ///
153    /// Returns an empty slice for `Ok`, or the non-empty error list for
154    /// `Recovered`.
155    pub fn errors(&self) -> &[GraphQLParseError] {
156        match self {
157            Self::Ok { .. } => &[],
158            Self::Recovered { errors, .. } => errors,
159        }
160    }
161
162    /// Formats all errors as a single string for display.
163    ///
164    /// Uses the bundled `SourceMap`'s source text (if available)
165    /// for snippet extraction.
166    pub fn formatted_errors(&self) -> String {
167        let source = self.source_map().source();
168        self.errors()
169            .iter()
170            .map(|e| e.format_detailed(source))
171            .collect::<Vec<_>>()
172            .join("\n")
173    }
174
175    /// Returns `true` if any errors were encountered during parsing.
176    pub fn has_errors(&self) -> bool {
177        matches!(self, Self::Recovered { .. })
178    }
179
180    /// Takes ownership of the AST unconditionally.
181    ///
182    /// An AST is always present in a `ParseResult`, even when parsing errors
183    /// may have occurred. Use this for tools that want best-effort results:
184    /// - IDE features (completions, hover info)
185    /// - Formatters (format what we can parse)
186    /// - Linters (report issues even in partially-valid documents)
187    ///
188    /// Check [`has_errors()`](Self::has_errors) before calling if you need
189    /// to know whether the AST was produced via error recovery.
190    pub fn into_ast(self) -> TAst {
191        match self {
192            Self::Ok { ast, .. } => ast,
193            Self::Recovered { ast, .. } => ast,
194        }
195    }
196
197    /// Takes ownership of the AST, errors, and source map only if
198    /// parsing encountered errors (recovered parse).
199    ///
200    /// Returns `None` for a completely successful parse.
201    /// This is the consuming version of
202    /// [`recovered()`](Self::recovered).
203    pub fn into_recovered(self) -> Option<(TAst, Vec<GraphQLParseError>, SourceMap<'src>)> {
204        match self {
205            Self::Ok { .. } => None,
206            Self::Recovered { ast, errors, source_map } => Some((ast, errors, source_map)),
207        }
208    }
209
210    /// Takes ownership of the AST only if parsing was completely successful.
211    ///
212    /// This is the consuming version of [`valid()`](Self::valid).
213    pub fn into_valid(self) -> Option<(TAst, SourceMap<'src>)> {
214        match self {
215            Self::Ok { ast, source_map } => Some((ast, source_map)),
216            Self::Recovered { .. } => None,
217        }
218    }
219
220    /// Creates a successful parse result with no errors.
221    pub(crate) fn new_ok(ast: TAst, source_map: SourceMap<'src>) -> Self {
222        Self::Ok { ast, source_map }
223    }
224
225    /// Creates a recovered parse result with both AST and errors.
226    ///
227    /// The AST was produced via error recovery and may be incomplete or
228    /// contain placeholder values.
229    ///
230    /// # Panics (debug only)
231    ///
232    /// Debug-asserts that `errors` is non-empty.
233    pub(crate) fn new_recovered(
234        ast: TAst,
235        errors: Vec<GraphQLParseError>,
236        source_map: SourceMap<'src>,
237    ) -> Self {
238        debug_assert!(
239            !errors.is_empty(),
240            "ParseResult::new_recovered() called with empty errors vec; \
241             use ParseResult::new_ok() instead",
242        );
243        Self::Recovered { ast, errors, source_map }
244    }
245
246    /// Returns the AST, errors, and source map only if parsing
247    /// encountered errors (recovered parse).
248    ///
249    /// Returns `None` for a completely successful parse. For the
250    /// consuming version, see
251    /// [`into_recovered()`](Self::into_recovered).
252    pub fn recovered(&self) -> Option<(&TAst, &[GraphQLParseError], &SourceMap<'src>)> {
253        match self {
254            Self::Ok { .. } => None,
255            Self::Recovered { ast, errors, source_map } => Some((ast, errors, source_map)),
256        }
257    }
258
259    /// Returns a reference to the [`SourceMap`] for resolving byte offsets
260    /// to line/column positions.
261    pub fn source_map(&self) -> &SourceMap<'src> {
262        match self {
263            Self::Ok { source_map, .. } => source_map,
264            Self::Recovered { source_map, .. } => source_map,
265        }
266    }
267
268    /// Returns the AST & [`SourceMap`] only if parsing was completely
269    /// successful (no errors).
270    ///
271    /// Use this when you need guaranteed-valid input, such as when compiling
272    /// a schema or executing a query.
273    ///
274    /// Returns `None` if parsing encountered errors (recovered parse).
275    pub fn valid(&self) -> Option<(&TAst, &SourceMap<'src>)> {
276        match self {
277            Self::Ok { ast, source_map } => Some((ast, source_map)),
278            Self::Recovered { .. } => None,
279        }
280    }
281}
282
283impl<'src> ParseResult<'src, ast::Document<'src>> {
284    /// Convenience function for accessing the
285    /// [`Document::definitions`](ast::Document::definitions) field after
286    /// parsing a [`Document`](ast::Document).
287    pub fn definitions(&self) -> &[ast::Definition<'src>] {
288        &self.ast().definitions
289    }
290
291    /// Convenience function for calling
292    /// [`Document::executable_definitions()`](ast::Document::executable_definitions)
293    /// after parsing a [`Document`](ast::Document).
294    pub fn executable_definitions(&self) -> impl Iterator<Item = &ast::Definition<'src>> {
295        self.ast().executable_definitions()
296    }
297
298    /// Convenience function for calling
299    /// [`Document::schema_definitions()`](ast::Document::schema_definitions)
300    /// after parsing a [`Document`](ast::Document).
301    pub fn schema_definitions(&self) -> impl Iterator<Item = &ast::Definition<'src>> {
302        self.ast().schema_definitions()
303    }
304
305    /// Convenience function for accessing the
306    /// [`Document::span`](ast::Document::span) field after parsing
307    /// a [`Document`](ast::Document).
308    pub fn span(&self) -> ByteSpan {
309        self.ast().span
310    }
311
312    /// Convenience function for accessing the
313    /// [`Document::syntax`](ast::Document::syntax) field after parsing
314    /// a [`Document`](ast::Document).
315    pub fn syntax(&self) -> &Option<Box<ast::DocumentSyntax<'src>>> {
316        &self.ast().syntax
317    }
318
319    /// Convenience function for calling
320    /// [`Document::trailing_trivia()`](ast::Document::trailing_trivia) after
321    /// parsing a [`Document`](ast::Document).
322    pub fn trailing_trivia(&self) -> Option<&[GraphQLTriviaToken<'src>]> {
323        self.ast().trailing_trivia()
324    }
325}
326
327impl<'src, TAst> From<ParseResult<'src, TAst>>
328    for Result<(TAst, SourceMap<'src>), Vec<GraphQLParseError>> {
329    /// Converts to a standard `Result`, treating recovered ASTs as errors.
330    ///
331    /// Returns `Ok((ast, source_map))` only if there were no errors.
332    /// Otherwise returns `Err(errors)`, discarding the recovered AST.
333    fn from(result: ParseResult<'src, TAst>) -> Self {
334        match result {
335            ParseResult::Ok { ast, source_map } => Ok((ast, source_map)),
336            ParseResult::Recovered { errors, .. } => Err(errors),
337        }
338    }
339}