Skip to main content

apollo_compiler/validation/
mod.rs

1//! Supporting APIs for [GraphQL validation](https://spec.graphql.org/September2025/#sec-Validation)
2//! and other kinds of errors.
3
4pub(crate) mod argument;
5pub(crate) mod diagnostics;
6pub(crate) mod directive;
7pub(crate) mod enum_;
8pub(crate) mod field;
9pub(crate) mod fragment;
10pub(crate) mod input_object;
11pub(crate) mod interface;
12pub(crate) mod object;
13pub(crate) mod operation;
14pub(crate) mod scalar;
15pub(crate) mod schema;
16pub(crate) mod selection;
17pub(crate) mod union_;
18pub(crate) mod value;
19pub(crate) mod variable;
20
21use crate::collections::HashMap;
22use crate::collections::HashSet;
23use crate::collections::IndexSet;
24use crate::coordinate::SchemaCoordinate;
25use crate::diagnostic::CliReport;
26use crate::diagnostic::Diagnostic;
27use crate::diagnostic::ToCliReport;
28use crate::executable::BuildError as ExecutableBuildError;
29use crate::executable::ConflictingFieldArgument;
30use crate::executable::ConflictingFieldName;
31use crate::executable::ConflictingFieldType;
32#[cfg(doc)]
33use crate::executable::ExecutableDocument;
34use crate::executable::VariableDefinition;
35use crate::parser::SourceMap;
36use crate::parser::SourceSpan;
37use crate::response::GraphQLError;
38use crate::schema::BuildError as SchemaBuildError;
39use crate::schema::Implementers;
40use crate::Name;
41use crate::Node;
42use crate::Schema;
43use std::fmt;
44use std::sync::Arc;
45use std::sync::OnceLock;
46
47/// Wraps a [`Schema`] or [`ExecutableDocument`] to mark it
48/// as [valid](https://spec.graphql.org/September2025/#sec-Validation).
49///
50/// This is obtained either by running validation with one of:
51///
52/// * [`Schema::parse_and_validate`]
53/// * [`Schema::validate`]
54/// * [`ExecutableDocument::parse_and_validate`]
55/// * [`ExecutableDocument::validate`]
56/// * [`coerce_variable_values`][crate::request::coerce_variable_values]
57///
58/// … or by explicitly skipping it with [`Valid::assume_valid`].
59///
60/// The schema or document inside `Valid<T>` is immutable (`&mut T` is not given out).
61/// It can be extracted with [`into_inner`][Self::into_inner],
62/// such as to mutate it then possibly re-validate it.
63#[derive(Debug, Clone, Eq, PartialEq)]
64#[repr(transparent)]
65pub struct Valid<T>(pub(crate) T);
66
67impl<T> Valid<T> {
68    /// Construct a `Valid` document without actually running validation.
69    ///
70    /// This takes ownership of the document.
71    /// See also [`assume_valid_ref`][Self::assume_valid_ref] which only requires a reference.
72    ///
73    /// The caller takes responsibility to ascertain that
74    /// the document is known through some other means to be valid.
75    /// For example, if it was loaded from some external storage
76    /// where it was only stored after validation.
77    pub fn assume_valid(document: T) -> Self {
78        Self(document)
79    }
80
81    /// Mark a reference as `Valid` without actually running validation.
82    ///
83    /// See also [`assume_valid`][Self::assume_valid] returns an owned `Valid<T>`
84    /// instead of only a reference.
85    ///
86    /// The caller takes responsibility to ascertain that
87    /// the document is known through some other means to be valid.
88    /// For example, if it was loaded from some external storage
89    /// where it was only stored after validation.
90    pub fn assume_valid_ref(document: &T) -> &Self {
91        let ptr: *const T = document;
92        let ptr: *const Valid<T> = ptr.cast();
93        // SAFETY: `repr(transparent)` makes it valid to transmute `&T` to `&Valid<T>`:
94        // <https://doc.rust-lang.org/nomicon/other-reprs.html#reprtransparent>
95        unsafe { &*ptr }
96    }
97
98    /// Extract the schema or document, such as to mutate it then possibly re-validate it.
99    pub fn into_inner(self) -> T {
100        self.0
101    }
102}
103
104impl<T> std::ops::Deref for Valid<T> {
105    type Target = T;
106
107    fn deref(&self) -> &Self::Target {
108        &self.0
109    }
110}
111
112impl<T> AsRef<T> for Valid<T> {
113    fn as_ref(&self) -> &T {
114        &self.0
115    }
116}
117
118impl<T: fmt::Display> fmt::Display for Valid<T> {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        self.0.fmt(f)
121    }
122}
123
124/// Shared context with things that may be used throughout executable validation.
125#[derive(Debug)]
126pub(crate) struct ExecutableValidationContext<'a> {
127    /// When None, rules that require a schema to validate are disabled.
128    schema: Option<&'a Schema>,
129    /// `schema.implementers_map()` is expensive to compute. This caches it for reuse.
130    implementers_map: OnceLock<HashMap<Name, Implementers>>,
131}
132
133impl<'a> ExecutableValidationContext<'a> {
134    pub(crate) fn new(schema: Option<&'a Schema>) -> Self {
135        Self {
136            schema,
137            implementers_map: Default::default(),
138        }
139    }
140
141    /// Returns the schema to validate against, if any.
142    pub(crate) fn schema(&self) -> Option<&'a Schema> {
143        self.schema
144    }
145
146    /// Returns a cached reference to the implementers map.
147    pub(crate) fn implementers_map(&self) -> &HashMap<Name, Implementers> {
148        self.implementers_map.get_or_init(|| {
149            self.schema
150                .map(|schema| schema.implementers_map())
151                .unwrap_or_default()
152        })
153    }
154
155    /// Returns a context for operation validation.
156    pub(crate) fn operation_context<'o>(
157        &'o self,
158        variables: &'o [Node<VariableDefinition>],
159    ) -> OperationValidationContext<'o> {
160        OperationValidationContext {
161            executable: self,
162            variables,
163            validated_fragments: HashSet::default(),
164        }
165    }
166}
167
168/// Shared context when validating things inside an operation.
169#[derive(Debug)]
170pub(crate) struct OperationValidationContext<'a> {
171    /// Parent context. Using a reference so the `OnceLock` is shared between all operation
172    /// contexts.
173    executable: &'a ExecutableValidationContext<'a>,
174    /// The variables defined for this operation.
175    pub(crate) variables: &'a [Node<VariableDefinition>],
176    pub(crate) validated_fragments: HashSet<Name>,
177}
178
179impl<'a> OperationValidationContext<'a> {
180    pub(crate) fn schema(&self) -> Option<&'a Schema> {
181        self.executable.schema
182    }
183
184    /// Returns a cached reference to the implementers map.
185    pub(crate) fn implementers_map(&self) -> &HashMap<Name, Implementers> {
186        self.executable.implementers_map()
187    }
188}
189
190/// A conversion failed with some errors, but also resulted in a partial document.
191///
192/// The [`Debug`][fmt::Debug] trait is implemented by forwarding to [`Self::errors`] and
193/// ignoring [`Self::partial`].
194/// This is so that the panic message prints (only) errors when [`.unwrap()`][Result::unwrap]
195/// is called on a `Result<_, WithError<_>>` value as returned by various APIs.
196pub struct WithErrors<T> {
197    /// The partial result of the conversion.
198    /// Some components may be missing,
199    /// for example if an error causes them not to be representable in the target data structure.
200    pub partial: T,
201
202    /// Errors collected during the conversion.
203    /// Should be non-empty when `WithError` is returned.
204    pub errors: DiagnosticList,
205}
206
207impl<T> fmt::Debug for WithErrors<T> {
208    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209        self.errors.fmt(f)
210    }
211}
212
213impl<T> fmt::Display for WithErrors<T> {
214    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215        self.errors.fmt(f)
216    }
217}
218
219/// Returned as an error for situtations that should not happen with a valid schema or document.
220///
221/// Since the relevant APIs take [`Valid<_>`][crate::validation::Valid] parameters,
222/// either apollo-compiler has a validation bug
223/// or [`assume_valid`][crate::validation::Valid::assume_valid] was used incorrectly.
224///
225/// Can be [converted][std::convert] to [`GraphQLError`],
226/// which populates [`extensions`][GraphQLError::extensions]
227/// with a `"APOLLO_SUSPECTED_VALIDATION_BUG": true` entry.
228#[derive(Debug, Clone)]
229pub(crate) struct SuspectedValidationBug {
230    pub message: String,
231    pub location: Option<SourceSpan>,
232}
233
234/// A collection of diagnostics returned by some validation method
235#[derive(Clone)]
236pub struct DiagnosticList {
237    pub(crate) sources: SourceMap,
238    diagnostics_data: Vec<DiagnosticData>,
239}
240
241// TODO(@goto-bus-stop) Can/should this be non-pub?
242#[derive(thiserror::Error, Debug, Clone)]
243#[error("{details}")]
244pub struct DiagnosticData {
245    location: Option<SourceSpan>,
246    details: Details,
247}
248
249#[derive(thiserror::Error, Debug, Clone)]
250pub(crate) enum Details {
251    #[error("{message}")]
252    ParserLimit { message: String },
253    #[error("syntax error: {message}")]
254    SyntaxError { message: String },
255    #[error("{0}")]
256    SchemaBuildError(SchemaBuildError),
257    #[error("{0}")]
258    ExecutableBuildError(ExecutableBuildError),
259    // TODO: Merge ValidationError into this enum
260    #[error(transparent)]
261    CompilerDiagnostic(diagnostics::DiagnosticData),
262    #[error("too much recursion")]
263    RecursionLimitError,
264}
265
266impl DiagnosticData {
267    /// Returns the internal error name for an (operation) validation error.
268    /// This is meant for debugging apollo-rs, not for public consumption.
269    #[doc(hidden)]
270    pub fn unstable_error_name(&self) -> Option<&'static str> {
271        match &self.details {
272            Details::CompilerDiagnostic(diagnostic) => {
273                use diagnostics::DiagnosticData::*;
274                Some(match diagnostic {
275                    RecursionError { .. } => "RecursionError",
276                    UniqueVariable { .. } => "UniqueVariable",
277                    UniqueArgument { .. } => "UniqueArgument",
278                    UniqueInputValue { .. } => "UniqueInputValue",
279                    UndefinedArgument { .. } => "UndefinedArgument",
280                    UndefinedDefinition { .. } => "UndefinedDefinition",
281                    UndefinedDirective { .. } => "UndefinedDirective",
282                    UndefinedVariable { .. } => "UndefinedVariable",
283                    UndefinedFragment { .. } => "UndefinedFragment",
284                    UndefinedEnumValue { .. } => "UndefinedEnumValue",
285                    UndefinedInputValue { .. } => "UndefinedInputValue",
286                    MissingInterfaceField { .. } => "MissingInterfaceField",
287                    RequiredArgument { .. } => "RequiredArgument",
288                    RequiredField { .. } => "RequiredField",
289                    TransitiveImplementedInterfaces { .. } => "TransitiveImplementedInterfaces",
290                    OutputType { .. } => "OutputType",
291                    InputType { .. } => "InputType",
292                    VariableInputType { .. } => "VariableInputType",
293                    QueryRootOperationType => "QueryRootOperationType",
294                    UnusedVariable { .. } => "UnusedVariable",
295                    RootOperationObjectType { .. } => "RootOperationObjectType",
296                    DuplicateRootOperationType { .. } => "DuplicateRootOperationType",
297                    UnionMemberObjectType { .. } => "UnionMemberObjectType",
298                    UnsupportedLocation { .. } => "UnsupportedLocation",
299                    UnsupportedValueType { .. } => "UnsupportedValueType",
300                    IntCoercionError { .. } => "IntCoercionError",
301                    FloatCoercionError { .. } => "FloatCoercionError",
302                    UniqueDirective { .. } => "UniqueDirective",
303                    MissingSubselection { .. } => "MissingSubselection",
304                    InvalidFragmentTarget { .. } => "InvalidFragmentTarget",
305                    InvalidFragmentSpread { .. } => "InvalidFragmentSpread",
306                    UnusedFragment { .. } => "UnusedFragment",
307                    DisallowedVariableUsage { .. } => "DisallowedVariableUsage",
308                    RecursiveDirectiveDefinition { .. } => "RecursiveDirectiveDefinition",
309                    RecursiveInterfaceDefinition { .. } => "RecursiveInterfaceDefinition",
310                    RecursiveInputObjectDefinition { .. } => "RecursiveInputObjectDefinition",
311                    RecursiveFragmentDefinition { .. } => "RecursiveFragmentDefinition",
312                    DeeplyNestedType { .. } => "DeeplyNestedType",
313                    EmptyFieldSet { .. } => "EmptyFieldSet",
314                    EmptyValueSet { .. } => "EmptyValueSet",
315                    EmptyMemberSet { .. } => "EmptyMemberSet",
316                    EmptyInputValueSet { .. } => "EmptyInputValueSet",
317                    ReservedName { .. } => "ReservedName",
318                    OneOfInputObjectFieldNonNull { .. } => "OneOfInputObjectFieldNonNull",
319                    OneOfInputObjectFieldCount { .. } => "OneOfInputObjectFieldCount",
320                    UnsupportedDefault { .. } => "UnsupportedDefault",
321                    OneOfDirectiveOnExtension { .. } => "OneOfDirectiveOnExtension",
322                    DeprecatedRequiredInputValue { .. } => "DeprecatedRequiredInputValue",
323                    DeprecatedImplementationField { .. } => "DeprecatedImplementationField",
324                    InvalidImplementationFieldType { .. } => "InvalidImplementationFieldType",
325                    MissingInterfaceFieldArgument { .. } => "MissingInterfaceFieldArgument",
326                    InvalidImplementationFieldArgumentType { .. } => {
327                        "InvalidImplementationFieldArgumentType"
328                    }
329                    ExtraRequiredImplementationFieldArgument { .. } => {
330                        "ExtraRequiredImplementationFieldArgument"
331                    }
332                })
333            }
334            Details::ExecutableBuildError(error) => Some(match error {
335                ExecutableBuildError::UndefinedField { .. } => "UndefinedField",
336                ExecutableBuildError::TypeSystemDefinition { .. } => "TypeSystemDefinition",
337                ExecutableBuildError::AmbiguousAnonymousOperation => "AmbiguousAnonymousOperation",
338                ExecutableBuildError::OperationNameCollision { .. } => "OperationNameCollision",
339                ExecutableBuildError::FragmentNameCollision { .. } => "FragmentNameCollision",
340                ExecutableBuildError::UndefinedRootOperation { .. } => "UndefinedRootOperation",
341                ExecutableBuildError::UndefinedTypeInNamedFragmentTypeCondition { .. } => {
342                    "UndefinedTypeInNamedFragmentTypeCondition"
343                }
344                ExecutableBuildError::UndefinedTypeInInlineFragmentTypeCondition { .. } => {
345                    "UndefinedTypeInInlineFragmentTypeCondition"
346                }
347                ExecutableBuildError::SubselectionOnScalarType { .. } => "SubselectionOnScalarType",
348                ExecutableBuildError::SubselectionOnEnumType { .. } => "SubselectionOnEnumType",
349                ExecutableBuildError::SubscriptionUsesMultipleFields { .. } => {
350                    "SubscriptionUsesMultipleFields"
351                }
352                ExecutableBuildError::SubscriptionUsesIntrospection { .. } => {
353                    "SubscriptionUsesIntrospection"
354                }
355                ExecutableBuildError::SubscriptionUsesConditionalSelection { .. } => {
356                    "SubscriptionUsesConditionalSelection"
357                }
358                ExecutableBuildError::DuplicateDeferLabel { .. } => "DuplicateDeferLabel",
359                ExecutableBuildError::DeferLabelMustNotBeVariable => "DeferLabelMustNotBeVariable",
360                ExecutableBuildError::DeferOnRootMutationOrSubscriptionField { .. } => {
361                    "DeferOnRootMutationOrSubscriptionField"
362                }
363                ExecutableBuildError::DeferInSubscriptionMustBeConditional => {
364                    "DeferInSubscriptionMustBeConditional"
365                }
366                ExecutableBuildError::ConflictingFieldType(_) => "ConflictingFieldType",
367                ExecutableBuildError::ConflictingFieldName(_) => "ConflictingFieldName",
368                ExecutableBuildError::ConflictingFieldArgument(_) => "ConflictingFieldArgument",
369            }),
370            Details::RecursionLimitError => Some("RecursionLimitError"),
371            _ => None,
372        }
373    }
374
375    /// Returns an error message for this diagnostic, mimicking the graphql-js format.
376    ///
377    /// This is meant as a migration path for the Apollo Router, and use by other consumers
378    /// is not supported.
379    #[doc(hidden)]
380    pub fn unstable_compat_message(&self) -> Option<String> {
381        match &self.details {
382            Details::CompilerDiagnostic(diagnostic) => {
383                use diagnostics::DiagnosticData::*;
384                match diagnostic {
385                    RecursionError { .. } => None,
386                    UniqueVariable { name, .. } => Some(format!(
387                        r#"There can be only one variable named "${name}"."#
388                    )),
389                    UniqueArgument { name, .. } => {
390                        Some(format!(r#"There can be only one argument named "{name}"."#))
391                    }
392                    UniqueInputValue { .. } => None,
393                    UndefinedArgument {
394                        name, coordinate, ..
395                    } => Some(format!(
396                        r#"Unknown argument "{name}" on field "{coordinate}"."#
397                    )),
398                    UndefinedDefinition { name } => Some(format!(r#"Unknown type "{name}"."#)),
399                    UndefinedDirective { name } => Some(format!(r#"Unknown directive "@{name}"."#)),
400                    UndefinedVariable { name } => {
401                        Some(format!(r#"Variable "${name}" is not defined."#))
402                    }
403                    UndefinedFragment { name } => Some(format!(r#"Unknown fragment "{name}"."#)),
404                    UndefinedEnumValue {
405                        value, definition, ..
406                    } => Some(format!(
407                        r#"Value "{value}" does not exist in "{definition}" enum."#
408                    )),
409                    UndefinedInputValue {
410                        value, definition, ..
411                    } => Some(format!(
412                        r#"Field "{value}" is not defined by type "{definition}"."#
413                    )),
414                    MissingInterfaceField { .. } => None,
415                    RequiredArgument {
416                        name,
417                        coordinate,
418                        expected_type,
419                        ..
420                    } => match coordinate {
421                        SchemaCoordinate::FieldArgument(coordinate) => Some(format!(
422                            r#"Field "{}" argument "{name}" of type "{expected_type}" is required, but it was not provided."#,
423                            coordinate.field,
424                        )),
425                        SchemaCoordinate::DirectiveArgument(coordinate) => Some(format!(
426                            r#"Directive "@{}" argument "{name}" of type "{expected_type}" is required, but it was not provided."#,
427                            coordinate.directive,
428                        )),
429                        // It's always an argument coordinate so we don't need to handle other cases.
430                        _ => None,
431                    },
432                    RequiredField {
433                        coordinate,
434                        expected_type,
435                        ..
436                    } => Some(format!(
437                        r#"Field "{coordinate}" of required type "{expected_type}" was not provided."#
438                    )),
439                    TransitiveImplementedInterfaces { .. } => None,
440                    OutputType { .. } => None,
441                    InputType { .. } => None,
442                    VariableInputType { name, ty, .. } => Some(format!(
443                        r#"Variable "${name}" cannot be non-input type "{ty}"."#
444                    )),
445                    QueryRootOperationType => None,
446                    UnusedVariable { name } => {
447                        Some(format!(r#"Variable "${name}" is never used."#))
448                    }
449                    RootOperationObjectType { .. } => None,
450                    DuplicateRootOperationType { .. } => None,
451                    UnionMemberObjectType { .. } => None,
452                    UnsupportedLocation { name, location, .. } => Some(format!(
453                        r#"Directive "@{name}" may not be used on {location}."#
454                    )),
455                    UnsupportedValueType { ty, value, .. } => Some(format!(
456                        r#"{} cannot represent value: {value}"#,
457                        ty.inner_named_type()
458                    )),
459                    IntCoercionError { value } => {
460                        let is_integer = value
461                            .chars()
462                            // The possible characters in "-1e+100"
463                            .all(|c| matches!(c, '-' | '+' | 'e' | '0'..='9'));
464                        if is_integer {
465                            Some(format!(
466                                r#"Int cannot represent non 32-bit signed integer value: {value}"#
467                            ))
468                        } else {
469                            Some(format!(
470                                r#"Int cannot represent non-integer value: {value}"#
471                            ))
472                        }
473                    }
474                    FloatCoercionError { value } => Some(format!(
475                        r#"Float cannot represent non numeric value: {value}"#
476                    )),
477                    UniqueDirective { name, .. } => Some(format!(
478                        r#"The directive "@{name}" can only be used once at this location."#
479                    )),
480                    MissingSubselection { coordinate, .. } => Some(format!(
481                        r#"Field "{field}" of type "{ty}" must have a selection of subfields. Did you mean "{field} {{ ... }}"?"#,
482                        ty = coordinate.ty,
483                        field = coordinate.attribute,
484                    )),
485                    InvalidFragmentTarget { name, ty } => {
486                        if let Some(name) = name {
487                            Some(format!(
488                                r#"Fragment "{name}" cannot condition on non composite type "{ty}"."#
489                            ))
490                        } else {
491                            Some(format!(
492                                r#"Fragment cannot condition on non composite type "{ty}"."#
493                            ))
494                        }
495                    }
496                    InvalidFragmentSpread {
497                        name,
498                        type_name,
499                        type_condition,
500                        ..
501                    } => {
502                        if let Some(name) = name {
503                            Some(format!(
504                                r#"Fragment "{name}" cannot be spread here as objects of type "{type_name}" can never be of type "{type_condition}"."#
505                            ))
506                        } else {
507                            Some(format!(
508                                r#"Fragment cannot be spread here as objects of type "{type_name}" can never be of type "{type_condition}"."#
509                            ))
510                        }
511                    }
512                    UnusedFragment { name } => Some(format!(r#"Fragment "{name}" is never used."#)),
513                    DisallowedVariableUsage {
514                        variable,
515                        variable_type,
516                        argument_type,
517                        ..
518                    } => Some(format!(
519                        r#"Variable "${variable}" of type "{variable_type}" used in position expecting type "{argument_type}"."#
520                    )),
521                    RecursiveDirectiveDefinition { .. } => None,
522                    RecursiveInterfaceDefinition { .. } => None,
523                    RecursiveInputObjectDefinition { .. } => None,
524                    RecursiveFragmentDefinition { name, trace, .. } => Some(format!(
525                        r#"Cannot spread fragment "{name}" within itself via {}"#,
526                        // Some inefficient allocation but :shrug:, not a big deal here
527                        trace
528                            .iter()
529                            .map(|spread| format!(r#""{}""#, spread.fragment_name))
530                            .collect::<Vec<_>>()
531                            .join(", "),
532                    )),
533                    DeeplyNestedType { .. } => None,
534                    EmptyFieldSet { .. } => None,
535                    EmptyValueSet { .. } => None,
536                    EmptyMemberSet { .. } => None,
537                    EmptyInputValueSet { .. } => None,
538                    ReservedName { .. } => None,
539                    OneOfInputObjectFieldNonNull { coordinate, .. } => Some(format!(
540                        r#"OneOf input field "{coordinate}" must be nullable."#
541                    )),
542                    OneOfInputObjectFieldCount { name, .. } => Some(format!(
543                        r#"OneOf Input Object "{name}" must specify exactly one key."#
544                    )),
545                    UnsupportedDefault { coordinate, .. } => Some(format!(
546                        r#"OneOf input field "{coordinate}" cannot have a default value."#
547                    )),
548                    OneOfDirectiveOnExtension { type_name, .. } => Some(format!(
549                        r#"The @oneOf directive must not be provided by an input object type extension on "{type_name}"."#
550                    )),
551                    DeprecatedRequiredInputValue { .. } => None,
552                    DeprecatedImplementationField { .. } => None,
553                    InvalidImplementationFieldType {
554                        name,
555                        interface,
556                        field,
557                        interface_type,
558                        actual_type,
559                        ..
560                    } => Some(format!(
561                        r#"Interface field {interface}.{field} expects type {interface_type} but {name}.{field} of type {actual_type} is not a proper subtype."#
562                    )),
563                    MissingInterfaceFieldArgument {
564                        name,
565                        interface,
566                        field,
567                        argument,
568                        ..
569                    } => Some(format!(
570                        r#"Interface field argument {interface}.{field}({argument}:) expected but {name}.{field} does not provide it."#
571                    )),
572                    InvalidImplementationFieldArgumentType {
573                        name,
574                        interface,
575                        field,
576                        argument,
577                        interface_type,
578                        actual_type,
579                        ..
580                    } => Some(format!(
581                        r#"Interface field {interface}.{field}({argument}:) expects type {interface_type} but {name}.{field}({argument}:) is type {actual_type}."#
582                    )),
583                    ExtraRequiredImplementationFieldArgument {
584                        name,
585                        interface,
586                        field,
587                        argument,
588                        ..
589                    } => Some(format!(
590                        r#"Object field {name}.{field} includes required argument {argument} that is missing from the Interface field {interface}.{field}."#
591                    )),
592                }
593            }
594            Details::ExecutableBuildError(error) => match error {
595                ExecutableBuildError::UndefinedField {
596                    type_name,
597                    field_name,
598                    ..
599                } => Some(format!(
600                    r#"Cannot query field "{field_name}" on type "{type_name}"."#
601                )),
602                ExecutableBuildError::TypeSystemDefinition { name, .. } => {
603                    if let Some(name) = name {
604                        Some(format!(r#"The "{name}" definition is not executable."#))
605                    } else {
606                        // Among type system definitions, only schema definitions do have a name
607                        Some("The schema definition is not executable.".to_string())
608                    }
609                }
610                ExecutableBuildError::AmbiguousAnonymousOperation => {
611                    Some("This anonymous operation must be the only defined operation.".to_string())
612                }
613                ExecutableBuildError::OperationNameCollision {
614                    name_at_previous_location,
615                } => Some(format!(
616                    r#"There can be only one operation named "{name_at_previous_location}"."#
617                )),
618                ExecutableBuildError::FragmentNameCollision {
619                    name_at_previous_location,
620                } => Some(format!(
621                    r#"There can be only one fragment named "{name_at_previous_location}"."#
622                )),
623                ExecutableBuildError::UndefinedRootOperation { operation_type } => Some(format!(
624                    // no period unlike other messages :zany_face:
625                    r#"The schema has no "{operation_type}" root type defined"#
626                )),
627                ExecutableBuildError::UndefinedTypeInNamedFragmentTypeCondition {
628                    type_name,
629                    ..
630                }
631                | ExecutableBuildError::UndefinedTypeInInlineFragmentTypeCondition {
632                    type_name,
633                    ..
634                } => Some(format!(r#"Unknown type "{type_name}"."#)),
635                ExecutableBuildError::SubselectionOnScalarType { type_name, path }
636                | ExecutableBuildError::SubselectionOnEnumType { type_name, path } => {
637                    #[allow(clippy::manual_map)]
638                    if let Some(field) = path.nested_fields.last() {
639                        Some(format!(
640                            r#"Field "{field}" must not have a selection since type "{type_name}" has no subfields"#
641                        ))
642                    } else {
643                        None // Can this happen?
644                    }
645                }
646                ExecutableBuildError::SubscriptionUsesMultipleFields { name, .. } => {
647                    if let Some(name) = name {
648                        Some(format!(
649                            r#"Subscription "{name}" must select only one top level field."#
650                        ))
651                    } else {
652                        Some(
653                            "Anonymous Subscription must select only one top level field."
654                                .to_string(),
655                        )
656                    }
657                }
658                ExecutableBuildError::SubscriptionUsesIntrospection { name, .. } => {
659                    if let Some(name) = name {
660                        Some(format!(
661                            r#"Subscription "{name}" must not select an introspection top level field."#
662                        ))
663                    } else {
664                        Some("Anonymous Subscription must not select an introspection top level field."
665                            .to_string())
666                    }
667                }
668                ExecutableBuildError::SubscriptionUsesConditionalSelection { name, .. } => {
669                    if let Some(name) = name {
670                        Some(format!(
671                            r#"Subscription "{name}" can not specify @skip or @include on root fields."#
672                        ))
673                    } else {
674                        Some(
675                            "Anonymous Subscription can not specify @skip or @include on root fields."
676                                .to_string(),
677                        )
678                    }
679                }
680                ExecutableBuildError::ConflictingFieldType(inner) => {
681                    let ConflictingFieldType {
682                        alias,
683                        original_type,
684                        conflicting_type,
685                        ..
686                    } = &**inner;
687                    Some(format!(
688                        r#"Fields "{alias}" conflict because they return conflicting types "{original_type} and "{conflicting_type}". Use different aliases on the fields to fetch both if this was intentional."#
689                    ))
690                }
691                ExecutableBuildError::ConflictingFieldName(inner) => {
692                    let ConflictingFieldName {
693                        alias,
694                        original_selection,
695                        conflicting_selection,
696                        ..
697                    } = &**inner;
698                    Some(format!(
699                        r#"Fields "{alias}" conflict because "{}" and "{}" are different fields. Use different aliases on the fields to fetch both if this was intentional."#,
700                        original_selection.attribute, conflicting_selection.attribute
701                    ))
702                }
703                ExecutableBuildError::ConflictingFieldArgument(inner) => {
704                    let ConflictingFieldArgument { alias, .. } = &**inner;
705                    Some(format!(
706                        r#"Fields "{alias}" conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional."#
707                    ))
708                }
709                ExecutableBuildError::DuplicateDeferLabel { .. } => Some(
710                    r#"Value for arguments "defer(label:)" and "stream(label:)" must be unique across all Defer/Stream directive usages."#
711                        .to_string(),
712                ),
713                ExecutableBuildError::DeferLabelMustNotBeVariable => Some(
714                    r#"Argument "@defer(label:)" must be a static string."#.to_string(),
715                ),
716                ExecutableBuildError::DeferOnRootMutationOrSubscriptionField {
717                    operation_type,
718                } => Some(format!(
719                    r#"Defer directive cannot be used on root {} type."#,
720                    operation_type.name()
721                )),
722                ExecutableBuildError::DeferInSubscriptionMustBeConditional => Some(
723                    "Defer directive not supported on subscription operations. Disable `@defer` by setting the `if` argument to `false`.".to_string(),
724                ),
725            },
726            _ => None,
727        }
728    }
729}
730
731impl ToCliReport for DiagnosticData {
732    fn location(&self) -> Option<SourceSpan> {
733        self.location
734    }
735
736    fn report(&self, report: &mut CliReport) {
737        if let Details::CompilerDiagnostic(diagnostic) = &self.details {
738            diagnostic.report(self.location, report);
739            return;
740        }
741
742        // Every case should also have a label at the main location
743        // (preferably saying something not completely redundant with the main message)
744        // and may have additional labels.
745        // Labels are always optional because locations are always optional,
746        // so essential information should be in the main message.
747        match &self.details {
748            Details::CompilerDiagnostic(_) => unreachable!(),
749            Details::ParserLimit { message, .. } => report.with_label_opt(self.location, message),
750            Details::SyntaxError { message, .. } => report.with_label_opt(self.location, message),
751            Details::SchemaBuildError(err) => match err {
752                SchemaBuildError::ExecutableDefinition { .. } => report.with_label_opt(
753                    self.location,
754                    "remove this definition, or use `parse_mixed()`",
755                ),
756                SchemaBuildError::SchemaDefinitionCollision {
757                    previous_location, ..
758                } => {
759                    report.with_label_opt(*previous_location, "previous `schema` definition here");
760                    report.with_label_opt(self.location, "`schema` redefined here");
761                    report.with_help(
762                        "merge this definition with the previous one, or use `extend schema`",
763                    );
764                }
765                SchemaBuildError::DirectiveDefinitionCollision {
766                    previous_location,
767                    name,
768                    ..
769                } => {
770                    report.with_label_opt(
771                        *previous_location,
772                        format_args!("previous definition of `@{name}` here"),
773                    );
774                    report.with_label_opt(self.location, format_args!("`@{name}` redefined here"));
775                    report.with_help("remove or rename one of the definitions");
776                }
777                SchemaBuildError::TypeDefinitionCollision {
778                    previous_location,
779                    name,
780                    ..
781                } => {
782                    report.with_label_opt(
783                        *previous_location,
784                        format_args!("previous definition of `{name}` here"),
785                    );
786                    report.with_label_opt(self.location, format_args!("`{name}` redefined here"));
787                    report.with_help("remove or rename one of the definitions, or use `extend`");
788                }
789                SchemaBuildError::BuiltInScalarTypeRedefinition => {
790                    report.with_label_opt(self.location, "remove this scalar definition");
791                }
792                SchemaBuildError::OrphanSchemaExtension => {
793                    report.with_label_opt(self.location, "extension here")
794                }
795                SchemaBuildError::OrphanTypeExtension { .. } => {
796                    report.with_label_opt(self.location, "extension here")
797                }
798                SchemaBuildError::TypeExtensionKindMismatch { def_location, .. } => {
799                    report.with_label_opt(*def_location, "type definition");
800                    report.with_label_opt(self.location, "extension here")
801                }
802                SchemaBuildError::DuplicateRootOperation {
803                    previous_location,
804                    operation_type,
805                    ..
806                } => {
807                    report.with_label_opt(
808                        *previous_location,
809                        format_args!("previous definition of `{operation_type}` here"),
810                    );
811                    report.with_label_opt(
812                        self.location,
813                        format_args!("`{operation_type}` redefined here"),
814                    );
815                }
816                SchemaBuildError::DuplicateImplementsInterfaceInObject {
817                    name_at_previous_location,
818                    ..
819                }
820                | SchemaBuildError::DuplicateImplementsInterfaceInInterface {
821                    name_at_previous_location,
822                    ..
823                } => {
824                    let previous_location = &name_at_previous_location.location();
825                    let name = name_at_previous_location;
826                    report.with_label_opt(
827                        *previous_location,
828                        format_args!("previous implementation of `{name}` here"),
829                    );
830                    report.with_label_opt(
831                        self.location,
832                        format_args!("`{name}` implemented again here"),
833                    );
834                }
835                SchemaBuildError::ObjectFieldNameCollision {
836                    name_at_previous_location,
837                    ..
838                }
839                | SchemaBuildError::InterfaceFieldNameCollision {
840                    name_at_previous_location,
841                    ..
842                }
843                | SchemaBuildError::EnumValueNameCollision {
844                    name_at_previous_location,
845                    ..
846                }
847                | SchemaBuildError::UnionMemberNameCollision {
848                    name_at_previous_location,
849                    ..
850                }
851                | SchemaBuildError::InputFieldNameCollision {
852                    name_at_previous_location,
853                    ..
854                } => {
855                    let previous_location = &name_at_previous_location.location();
856                    let name = name_at_previous_location;
857                    report.with_label_opt(
858                        *previous_location,
859                        format_args!("previous definition of `{name}` here"),
860                    );
861                    report.with_label_opt(self.location, format_args!("`{name}` redefined here"));
862                }
863            },
864            Details::ExecutableBuildError(err) => match err {
865                ExecutableBuildError::TypeSystemDefinition { .. } => report.with_label_opt(
866                    self.location,
867                    "remove this definition, or use `parse_mixed()`",
868                ),
869                ExecutableBuildError::AmbiguousAnonymousOperation => {
870                    report.with_label_opt(self.location, "provide a name for this definition");
871                    report.with_help(
872                        "GraphQL requires operations to be named if the document has more than one",
873                    );
874                }
875                ExecutableBuildError::OperationNameCollision {
876                    name_at_previous_location,
877                    ..
878                }
879                | ExecutableBuildError::FragmentNameCollision {
880                    name_at_previous_location,
881                    ..
882                } => {
883                    let previous_location = &name_at_previous_location.location();
884                    let name = name_at_previous_location;
885                    report.with_label_opt(
886                        *previous_location,
887                        format_args!("previous definition of `{name}` here"),
888                    );
889                    report.with_label_opt(self.location, format_args!("`{name}` redefined here"));
890                }
891                ExecutableBuildError::UndefinedRootOperation { operation_type, .. } => {
892                    report.with_label_opt(
893                        self.location,
894                        format_args!(
895                            "`{operation_type}` is not defined in the schema and is therefore not supported"
896                        ),
897                    );
898                    report.with_help(format_args!(
899                        "consider defining a `{operation_type}` root operation type in your schema"
900                    ))
901                }
902                ExecutableBuildError::UndefinedTypeInNamedFragmentTypeCondition { .. } => {
903                    report.with_label_opt(self.location, "type condition here")
904                }
905                ExecutableBuildError::UndefinedTypeInInlineFragmentTypeCondition {
906                    path, ..
907                } => {
908                    report.with_label_opt(self.location, "type condition here");
909                    report.with_note(format_args!("path to the inline fragment: `{path} → ...`"))
910                }
911                ExecutableBuildError::SubselectionOnScalarType { path, .. }
912                | ExecutableBuildError::SubselectionOnEnumType { path, .. } => {
913                    report.with_label_opt(self.location, "remove subselections here");
914                    report.with_note(format_args!("path to the field: `{path}`"))
915                }
916                ExecutableBuildError::UndefinedField {
917                    field_name,
918                    type_name,
919                    path,
920                    ..
921                } => {
922                    report.with_label_opt(
923                        self.location,
924                        format_args!("field `{field_name}` selected here"),
925                    );
926                    report.with_label_opt(
927                        type_name.location(),
928                        format_args!("type `{type_name}` defined here"),
929                    );
930                    report.with_note(format_args!("path to the field: `{path}`"))
931                }
932                ExecutableBuildError::SubscriptionUsesMultipleFields { fields, .. } => {
933                    report.with_label_opt(
934                        self.location,
935                        format_args!("subscription with {} root fields", fields.len()),
936                    );
937                    report.with_help(format_args!(
938                        "There are {} root fields: {}. This is not allowed.",
939                        fields.len(),
940                        CommaSeparated(fields)
941                    ));
942                }
943                ExecutableBuildError::SubscriptionUsesIntrospection { field, .. } => {
944                    report.with_label_opt(
945                        self.location,
946                        format_args!("{field} is an introspection field"),
947                    );
948                }
949                ExecutableBuildError::SubscriptionUsesConditionalSelection { .. } => {
950                    report.with_label_opt(self.location, "conditional directive used here");
951                }
952                ExecutableBuildError::DuplicateDeferLabel {
953                    original_location, ..
954                } => {
955                    report.with_label_opt(*original_location, "label first used here");
956                    report.with_label_opt(self.location, "duplicate label used here");
957                }
958                ExecutableBuildError::DeferLabelMustNotBeVariable => {
959                    report.with_label_opt(self.location, "variable used as label here");
960                }
961                ExecutableBuildError::DeferOnRootMutationOrSubscriptionField { operation_type } => {
962                    report.with_label_opt(
963                        self.location,
964                        format_args!("`@defer` on a root {} selection", operation_type.name()),
965                    );
966                }
967                ExecutableBuildError::DeferInSubscriptionMustBeConditional => {
968                    report.with_label_opt(
969                        self.location,
970                        "`@defer` in a subscription must use `if: false` or `if: $variable`",
971                    );
972                }
973                ExecutableBuildError::ConflictingFieldType(inner) => {
974                    let ConflictingFieldType {
975                        alias,
976                        original_location,
977                        original_coordinate,
978                        original_type,
979                        conflicting_location,
980                        conflicting_coordinate,
981                        conflicting_type,
982                    } = &**inner;
983                    report.with_label_opt(
984                        *original_location,
985                        format_args!(
986                        "`{alias}` is selected from `{original_coordinate}: {original_type}` here"
987                    ),
988                    );
989                    report.with_label_opt(
990                    *conflicting_location,
991                    format_args!("`{alias}` is selected from `{conflicting_coordinate}: {conflicting_type}` here"),
992                );
993                }
994                ExecutableBuildError::ConflictingFieldArgument(inner) => {
995                    let ConflictingFieldArgument {
996                        alias,
997                        original_location,
998                        original_coordinate,
999                        original_value,
1000                        conflicting_location,
1001                        conflicting_coordinate: _,
1002                        conflicting_value,
1003                    } = &**inner;
1004                    let argument = &original_coordinate.argument;
1005                    match (original_value, conflicting_value) {
1006                        (Some(_), Some(_)) => {
1007                            report.with_label_opt(
1008                                *original_location,
1009                                format_args!(
1010                                    "`{original_coordinate}` is used with one argument value here"
1011                                ),
1012                            );
1013                            report.with_label_opt(
1014                                *conflicting_location,
1015                                "but a different value here",
1016                            );
1017                        }
1018                        (Some(_), None) => {
1019                            report.with_label_opt(
1020                                *original_location,
1021                                format!("`{alias}` is selected with argument `{argument}` here",),
1022                            );
1023                            report.with_label_opt(
1024                                *conflicting_location,
1025                                format!("but argument `{argument}` is not provided here"),
1026                            );
1027                        }
1028                        (None, Some(_)) => {
1029                            report.with_label_opt(
1030                                *conflicting_location,
1031                                format!("`{alias}` is selected with argument `{argument}` here",),
1032                            );
1033                            report.with_label_opt(
1034                                *original_location,
1035                                format!("but argument `{argument}` is not provided here"),
1036                            );
1037                        }
1038                        (None, None) => unreachable!(),
1039                    }
1040                    report.with_help("The same name cannot be selected multiple times with different arguments, because it's not clear which set of arguments should be used to fill the response. If you intend to use diverging arguments, consider adding an alias to differentiate");
1041                }
1042                ExecutableBuildError::ConflictingFieldName(inner) => {
1043                    let ConflictingFieldName {
1044                        alias: field,
1045                        original_selection,
1046                        original_location,
1047                        conflicting_selection,
1048                        conflicting_location,
1049                    } = &**inner;
1050                    report.with_label_opt(
1051                        *original_location,
1052                        format_args!("`{field}` is selected from `{original_selection}` here"),
1053                    );
1054                    report.with_label_opt(
1055                        *conflicting_location,
1056                        format_args!("`{field}` is selected from `{conflicting_selection}` here"),
1057                    );
1058
1059                    report.with_help("Both fields may be present on the schema type, so it's not clear which one should be used to fill the response");
1060                }
1061            },
1062            Details::RecursionLimitError => {}
1063        }
1064    }
1065}
1066
1067impl Diagnostic<'_, DiagnosticData> {
1068    /// Get a [`serde`]-serializable version of the current diagnostic. This method mimicks the
1069    /// shape and message of errors produced by graphql-js.
1070    ///
1071    /// This is only for use by the Apollo Router, any other consumer is not supported.
1072    #[doc(hidden)]
1073    pub fn unstable_to_json_compat(&self) -> GraphQLError {
1074        GraphQLError::new(
1075            self.error
1076                .unstable_compat_message()
1077                .unwrap_or_else(|| self.error.to_string()),
1078            self.error.location(),
1079            self.sources,
1080        )
1081    }
1082}
1083
1084impl DiagnosticList {
1085    /// Creates an empty diagnostic list with the given source map.
1086    pub fn new(sources: SourceMap) -> Self {
1087        Self {
1088            sources,
1089            diagnostics_data: Vec::new(),
1090        }
1091    }
1092
1093    pub fn is_empty(&self) -> bool {
1094        self.diagnostics_data.is_empty()
1095    }
1096
1097    pub fn len(&self) -> usize {
1098        self.diagnostics_data.len()
1099    }
1100
1101    pub fn iter(
1102        &self,
1103    ) -> impl DoubleEndedIterator<Item = Diagnostic<'_, DiagnosticData>> + ExactSizeIterator {
1104        self.diagnostics_data
1105            .iter()
1106            .map(|data| data.to_diagnostic(&self.sources))
1107    }
1108
1109    pub(crate) fn push(&mut self, location: Option<SourceSpan>, details: impl Into<Details>) {
1110        self.diagnostics_data.push(DiagnosticData {
1111            location,
1112            details: details.into(),
1113        })
1114    }
1115
1116    /// Concatenate an `other` list of diagnostics into `self`, and sort them together.
1117    pub fn merge(&mut self, other: Self) {
1118        if !Arc::ptr_eq(&self.sources, &other.sources) {
1119            let sources = Arc::make_mut(&mut self.sources);
1120            for (&k, v) in &*other.sources {
1121                sources.entry(k).or_insert_with(|| v.clone());
1122            }
1123        }
1124        self.diagnostics_data.extend(other.diagnostics_data);
1125        self.sort()
1126    }
1127
1128    fn sort(&mut self) {
1129        self.diagnostics_data
1130            .sort_by_key(|err| err.location.map(|loc| (loc.file_id(), loc.offset())));
1131    }
1132
1133    pub(crate) fn into_result(mut self) -> Result<(), Self> {
1134        if self.diagnostics_data.is_empty() {
1135            Ok(())
1136        } else {
1137            self.sort();
1138            Err(self)
1139        }
1140    }
1141
1142    pub(crate) fn into_result_with<T>(self, value: T) -> Result<T, WithErrors<T>> {
1143        match self.into_result() {
1144            Ok(()) => Ok(value),
1145            Err(errors) => Err(WithErrors {
1146                partial: value,
1147                errors,
1148            }),
1149        }
1150    }
1151
1152    pub(crate) fn into_valid_result<T>(self, value: T) -> Result<Valid<T>, WithErrors<T>> {
1153        match self.into_result() {
1154            Ok(()) => Ok(Valid(value)),
1155            Err(errors) => Err(WithErrors {
1156                partial: value,
1157                errors,
1158            }),
1159        }
1160    }
1161}
1162
1163/// Use Display formatting to output without colors: `format!("{diagnostics}")`
1164impl fmt::Display for DiagnosticList {
1165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1166        for diagnostic in self.iter() {
1167            fmt::Display::fmt(&diagnostic, f)?
1168        }
1169        Ok(())
1170    }
1171}
1172
1173/// Use Debug formatting to output with colors: `format!("{diagnostics:?}")`
1174impl fmt::Debug for DiagnosticList {
1175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1176        for diagnostic in self.iter() {
1177            fmt::Debug::fmt(&diagnostic, f)?
1178        }
1179        Ok(())
1180    }
1181}
1182
1183impl From<SchemaBuildError> for Details {
1184    fn from(value: SchemaBuildError) -> Self {
1185        Details::SchemaBuildError(value)
1186    }
1187}
1188
1189impl From<ExecutableBuildError> for Details {
1190    fn from(value: ExecutableBuildError) -> Self {
1191        Details::ExecutableBuildError(value)
1192    }
1193}
1194
1195impl From<diagnostics::DiagnosticData> for Details {
1196    fn from(value: diagnostics::DiagnosticData) -> Self {
1197        Details::CompilerDiagnostic(value)
1198    }
1199}
1200
1201const DEFAULT_RECURSION_LIMIT: usize = 32;
1202
1203#[derive(Debug, Clone, thiserror::Error)]
1204#[error("Recursion limit reached")]
1205#[non_exhaustive]
1206struct RecursionLimitError {}
1207
1208/// Track recursion depth to prevent stack overflow.
1209#[derive(Debug)]
1210struct DepthCounter {
1211    value: usize,
1212    high: usize,
1213    limit: usize,
1214}
1215
1216impl DepthCounter {
1217    fn new() -> Self {
1218        Self {
1219            value: 0,
1220            high: 0,
1221            limit: DEFAULT_RECURSION_LIMIT,
1222        }
1223    }
1224
1225    fn with_limit(mut self, limit: usize) -> Self {
1226        self.limit = limit;
1227        self
1228    }
1229
1230    /// Return the actual API for tracking recursive uses.
1231    pub(crate) fn guard(&mut self) -> DepthGuard<'_> {
1232        DepthGuard(self)
1233    }
1234}
1235
1236/// Track call depth in a recursive function.
1237///
1238/// Pass the result of `guard.increment()` to recursive calls. When a guard is dropped,
1239/// its value is decremented.
1240struct DepthGuard<'a>(&'a mut DepthCounter);
1241
1242impl DepthGuard<'_> {
1243    /// Mark that we are recursing. If we reached the limit, return an error.
1244    fn increment(&mut self) -> Result<DepthGuard<'_>, RecursionLimitError> {
1245        self.0.value += 1;
1246        self.0.high = self.0.high.max(self.0.value);
1247        if self.0.value > self.0.limit {
1248            Err(RecursionLimitError {})
1249        } else {
1250            Ok(DepthGuard(self.0))
1251        }
1252    }
1253}
1254
1255impl Drop for DepthGuard<'_> {
1256    fn drop(&mut self) {
1257        // This may already be 0 if it's the original `counter.guard()` result, but that's fine
1258        self.0.value = self.0.value.saturating_sub(1);
1259    }
1260}
1261
1262/// Track used names in a recursive function.
1263#[derive(Debug)]
1264struct RecursionStack {
1265    seen: IndexSet<Name>,
1266    high: usize,
1267    limit: usize,
1268}
1269
1270impl RecursionStack {
1271    fn new() -> Self {
1272        Self {
1273            seen: IndexSet::with_hasher(Default::default()),
1274            high: 0,
1275            limit: DEFAULT_RECURSION_LIMIT,
1276        }
1277    }
1278
1279    fn with_root(root: Name) -> Self {
1280        let mut stack = Self::new();
1281        stack.seen.insert(root);
1282        stack
1283    }
1284
1285    fn with_limit(mut self, limit: usize) -> Self {
1286        self.limit = limit;
1287        self
1288    }
1289
1290    /// Return the actual API for tracking recursive uses.
1291    pub(crate) fn guard(&mut self) -> RecursionGuard<'_> {
1292        RecursionGuard(self)
1293    }
1294}
1295
1296/// Track used names in a recursive function.
1297///
1298/// Pass the result of `guard.push(name)` to recursive calls. Use `guard.contains(name)` to check
1299/// if the name was used somewhere up the call stack. When a guard is dropped, its name is removed
1300/// from the list.
1301struct RecursionGuard<'a>(&'a mut RecursionStack);
1302
1303impl RecursionGuard<'_> {
1304    /// Mark that we saw a name. If there are too many names, return an error.
1305    fn push(&mut self, name: &Name) -> Result<RecursionGuard<'_>, RecursionLimitError> {
1306        let new = self.0.seen.insert(name.clone());
1307        debug_assert!(
1308            new,
1309            "cannot push the same name twice to RecursionGuard, check contains() first"
1310        );
1311        self.0.high = self.0.high.max(self.0.seen.len());
1312        if self.0.seen.len() > self.0.limit {
1313            Err(RecursionLimitError {})
1314        } else {
1315            Ok(RecursionGuard(self.0))
1316        }
1317    }
1318
1319    /// Check if we saw a name somewhere up the call stack.
1320    fn contains(&self, name: &Name) -> bool {
1321        self.0.seen.contains(name)
1322    }
1323
1324    /// Return the name where we started.
1325    fn first(&self) -> Option<&Name> {
1326        self.0.seen.first()
1327    }
1328}
1329
1330impl Drop for RecursionGuard<'_> {
1331    fn drop(&mut self) {
1332        // This may already be empty if it's the original `stack.guard()` result, but that's fine
1333        let _ = self.0.seen.pop();
1334    }
1335}
1336
1337/// Errors that can happen when chasing potentially cyclical references.
1338#[derive(Debug, Clone, thiserror::Error)]
1339enum CycleError<T> {
1340    /// Detected a cycle, value contains the path from the offending node back to the node where we
1341    /// started.
1342    #[error("Cycle detected")]
1343    Recursed(Vec<Node<T>>),
1344    /// Ran into recursion limit before a cycle could be detected.
1345    #[error(transparent)]
1346    Limit(#[from] RecursionLimitError),
1347}
1348
1349impl<T> CycleError<T> {
1350    fn trace(mut self, node: &Node<T>) -> Self {
1351        if let Self::Recursed(trace) = &mut self {
1352            trace.push(node.clone());
1353        }
1354        self
1355    }
1356}
1357
1358struct CommaSeparated<'a, It>(&'a It);
1359impl<'a, T, It> fmt::Display for CommaSeparated<'a, It>
1360where
1361    T: fmt::Display,
1362    &'a It: IntoIterator<Item = T>,
1363{
1364    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1365        let mut it = self.0.into_iter();
1366        if let Some(element) = it.next() {
1367            element.fmt(f)?;
1368        }
1369        for element in it {
1370            f.write_str(", ")?;
1371            element.fmt(f)?;
1372        }
1373        Ok(())
1374    }
1375}