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}