Skip to main content

apollo_compiler/validation/
mod.rs

1//! Supporting APIs for [GraphQL validation](https://spec.graphql.org/October2021/#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/October2021/#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                    InvalidImplementationFieldType { .. } => "InvalidImplementationFieldType",
319                    MissingInterfaceFieldArgument { .. } => "MissingInterfaceFieldArgument",
320                    InvalidImplementationFieldArgumentType { .. } => {
321                        "InvalidImplementationFieldArgumentType"
322                    }
323                    ExtraRequiredImplementationFieldArgument { .. } => {
324                        "ExtraRequiredImplementationFieldArgument"
325                    }
326                })
327            }
328            Details::ExecutableBuildError(error) => Some(match error {
329                ExecutableBuildError::UndefinedField { .. } => "UndefinedField",
330                ExecutableBuildError::TypeSystemDefinition { .. } => "TypeSystemDefinition",
331                ExecutableBuildError::AmbiguousAnonymousOperation => "AmbiguousAnonymousOperation",
332                ExecutableBuildError::OperationNameCollision { .. } => "OperationNameCollision",
333                ExecutableBuildError::FragmentNameCollision { .. } => "FragmentNameCollision",
334                ExecutableBuildError::UndefinedRootOperation { .. } => "UndefinedRootOperation",
335                ExecutableBuildError::UndefinedTypeInNamedFragmentTypeCondition { .. } => {
336                    "UndefinedTypeInNamedFragmentTypeCondition"
337                }
338                ExecutableBuildError::UndefinedTypeInInlineFragmentTypeCondition { .. } => {
339                    "UndefinedTypeInInlineFragmentTypeCondition"
340                }
341                ExecutableBuildError::SubselectionOnScalarType { .. } => "SubselectionOnScalarType",
342                ExecutableBuildError::SubselectionOnEnumType { .. } => "SubselectionOnEnumType",
343                ExecutableBuildError::SubscriptionUsesMultipleFields { .. } => {
344                    "SubscriptionUsesMultipleFields"
345                }
346                ExecutableBuildError::SubscriptionUsesIntrospection { .. } => {
347                    "SubscriptionUsesIntrospection"
348                }
349                ExecutableBuildError::SubscriptionUsesConditionalSelection { .. } => {
350                    "SubscriptionUsesConditionalSelection"
351                }
352                ExecutableBuildError::DuplicateDeferLabel { .. } => "DuplicateDeferLabel",
353                ExecutableBuildError::DeferLabelMustNotBeVariable => "DeferLabelMustNotBeVariable",
354                ExecutableBuildError::DeferOnRootMutationOrSubscriptionField { .. } => {
355                    "DeferOnRootMutationOrSubscriptionField"
356                }
357                ExecutableBuildError::DeferInSubscriptionMustBeConditional => {
358                    "DeferInSubscriptionMustBeConditional"
359                }
360                ExecutableBuildError::ConflictingFieldType(_) => "ConflictingFieldType",
361                ExecutableBuildError::ConflictingFieldName(_) => "ConflictingFieldName",
362                ExecutableBuildError::ConflictingFieldArgument(_) => "ConflictingFieldArgument",
363            }),
364            Details::RecursionLimitError => Some("RecursionLimitError"),
365            _ => None,
366        }
367    }
368
369    /// Returns an error message for this diagnostic, mimicking the graphql-js format.
370    ///
371    /// This is meant as a migration path for the Apollo Router, and use by other consumers
372    /// is not supported.
373    #[doc(hidden)]
374    pub fn unstable_compat_message(&self) -> Option<String> {
375        match &self.details {
376            Details::CompilerDiagnostic(diagnostic) => {
377                use diagnostics::DiagnosticData::*;
378                match diagnostic {
379                    RecursionError { .. } => None,
380                    UniqueVariable { name, .. } => Some(format!(
381                        r#"There can be only one variable named "${name}"."#
382                    )),
383                    UniqueArgument { name, .. } => {
384                        Some(format!(r#"There can be only one argument named "{name}"."#))
385                    }
386                    UniqueInputValue { .. } => None,
387                    UndefinedArgument {
388                        name, coordinate, ..
389                    } => Some(format!(
390                        r#"Unknown argument "{name}" on field "{coordinate}"."#
391                    )),
392                    UndefinedDefinition { name } => Some(format!(r#"Unknown type "{name}"."#)),
393                    UndefinedDirective { name } => Some(format!(r#"Unknown directive "@{name}"."#)),
394                    UndefinedVariable { name } => {
395                        Some(format!(r#"Variable "${name}" is not defined."#))
396                    }
397                    UndefinedFragment { name } => Some(format!(r#"Unknown fragment "{name}"."#)),
398                    UndefinedEnumValue {
399                        value, definition, ..
400                    } => Some(format!(
401                        r#"Value "{value}" does not exist in "{definition}" enum."#
402                    )),
403                    UndefinedInputValue {
404                        value, definition, ..
405                    } => Some(format!(
406                        r#"Field "{value}" is not defined by type "{definition}"."#
407                    )),
408                    MissingInterfaceField { .. } => None,
409                    RequiredArgument {
410                        name,
411                        coordinate,
412                        expected_type,
413                        ..
414                    } => match coordinate {
415                        SchemaCoordinate::FieldArgument(coordinate) => Some(format!(
416                            r#"Field "{}" argument "{name}" of type "{expected_type}" is required, but it was not provided."#,
417                            coordinate.field,
418                        )),
419                        SchemaCoordinate::DirectiveArgument(coordinate) => Some(format!(
420                            r#"Directive "@{}" argument "{name}" of type "{expected_type}" is required, but it was not provided."#,
421                            coordinate.directive,
422                        )),
423                        // It's always an argument coordinate so we don't need to handle other cases.
424                        _ => None,
425                    },
426                    RequiredField {
427                        coordinate,
428                        expected_type,
429                        ..
430                    } => Some(format!(
431                        r#"Field "{coordinate}" of required type "{expected_type}" was not provided."#
432                    )),
433                    TransitiveImplementedInterfaces { .. } => None,
434                    OutputType { .. } => None,
435                    InputType { .. } => None,
436                    VariableInputType { name, ty, .. } => Some(format!(
437                        r#"Variable "${name}" cannot be non-input type "{ty}"."#
438                    )),
439                    QueryRootOperationType => None,
440                    UnusedVariable { name } => {
441                        Some(format!(r#"Variable "${name}" is never used."#))
442                    }
443                    RootOperationObjectType { .. } => None,
444                    DuplicateRootOperationType { .. } => None,
445                    UnionMemberObjectType { .. } => None,
446                    UnsupportedLocation { name, location, .. } => Some(format!(
447                        r#"Directive "@{name}" may not be used on {location}."#
448                    )),
449                    UnsupportedValueType { ty, value, .. } => Some(format!(
450                        r#"{} cannot represent value: {value}"#,
451                        ty.inner_named_type()
452                    )),
453                    IntCoercionError { value } => {
454                        let is_integer = value
455                            .chars()
456                            // The possible characters in "-1e+100"
457                            .all(|c| matches!(c, '-' | '+' | 'e' | '0'..='9'));
458                        if is_integer {
459                            Some(format!(
460                                r#"Int cannot represent non 32-bit signed integer value: {value}"#
461                            ))
462                        } else {
463                            Some(format!(
464                                r#"Int cannot represent non-integer value: {value}"#
465                            ))
466                        }
467                    }
468                    FloatCoercionError { value } => Some(format!(
469                        r#"Float cannot represent non numeric value: {value}"#
470                    )),
471                    UniqueDirective { name, .. } => Some(format!(
472                        r#"The directive "@{name}" can only be used once at this location."#
473                    )),
474                    MissingSubselection { coordinate, .. } => Some(format!(
475                        r#"Field "{field}" of type "{ty}" must have a selection of subfields. Did you mean "{field} {{ ... }}"?"#,
476                        ty = coordinate.ty,
477                        field = coordinate.attribute,
478                    )),
479                    InvalidFragmentTarget { name, ty } => {
480                        if let Some(name) = name {
481                            Some(format!(
482                                r#"Fragment "{name}" cannot condition on non composite type "{ty}"."#
483                            ))
484                        } else {
485                            Some(format!(
486                                r#"Fragment cannot condition on non composite type "{ty}"."#
487                            ))
488                        }
489                    }
490                    InvalidFragmentSpread {
491                        name,
492                        type_name,
493                        type_condition,
494                        ..
495                    } => {
496                        if let Some(name) = name {
497                            Some(format!(
498                                r#"Fragment "{name}" cannot be spread here as objects of type "{type_name}" can never be of type "{type_condition}"."#
499                            ))
500                        } else {
501                            Some(format!(
502                                r#"Fragment cannot be spread here as objects of type "{type_name}" can never be of type "{type_condition}"."#
503                            ))
504                        }
505                    }
506                    UnusedFragment { name } => Some(format!(r#"Fragment "{name}" is never used."#)),
507                    DisallowedVariableUsage {
508                        variable,
509                        variable_type,
510                        argument_type,
511                        ..
512                    } => Some(format!(
513                        r#"Variable "${variable}" of type "{variable_type}" used in position expecting type "{argument_type}"."#
514                    )),
515                    RecursiveDirectiveDefinition { .. } => None,
516                    RecursiveInterfaceDefinition { .. } => None,
517                    RecursiveInputObjectDefinition { .. } => None,
518                    RecursiveFragmentDefinition { name, trace, .. } => Some(format!(
519                        r#"Cannot spread fragment "{name}" within itself via {}"#,
520                        // Some inefficient allocation but :shrug:, not a big deal here
521                        trace
522                            .iter()
523                            .map(|spread| format!(r#""{}""#, spread.fragment_name))
524                            .collect::<Vec<_>>()
525                            .join(", "),
526                    )),
527                    DeeplyNestedType { .. } => None,
528                    EmptyFieldSet { .. } => None,
529                    EmptyValueSet { .. } => None,
530                    EmptyMemberSet { .. } => None,
531                    EmptyInputValueSet { .. } => None,
532                    ReservedName { .. } => None,
533                    InvalidImplementationFieldType {
534                        name,
535                        interface,
536                        field,
537                        interface_type,
538                        actual_type,
539                        ..
540                    } => Some(format!(
541                        r#"Interface field {interface}.{field} expects type {interface_type} but {name}.{field} of type {actual_type} is not a proper subtype."#
542                    )),
543                    MissingInterfaceFieldArgument {
544                        name,
545                        interface,
546                        field,
547                        argument,
548                        ..
549                    } => Some(format!(
550                        r#"Interface field argument {interface}.{field}({argument}:) expected but {name}.{field} does not provide it."#
551                    )),
552                    InvalidImplementationFieldArgumentType {
553                        name,
554                        interface,
555                        field,
556                        argument,
557                        interface_type,
558                        actual_type,
559                        ..
560                    } => Some(format!(
561                        r#"Interface field {interface}.{field}({argument}:) expects type {interface_type} but {name}.{field}({argument}:) is type {actual_type}."#
562                    )),
563                    ExtraRequiredImplementationFieldArgument {
564                        name,
565                        interface,
566                        field,
567                        argument,
568                        ..
569                    } => Some(format!(
570                        r#"Object field {name}.{field} includes required argument {argument} that is missing from the Interface field {interface}.{field}."#
571                    )),
572                }
573            }
574            Details::ExecutableBuildError(error) => match error {
575                ExecutableBuildError::UndefinedField {
576                    type_name,
577                    field_name,
578                    ..
579                } => Some(format!(
580                    r#"Cannot query field "{field_name}" on type "{type_name}"."#
581                )),
582                ExecutableBuildError::TypeSystemDefinition { name, .. } => {
583                    if let Some(name) = name {
584                        Some(format!(r#"The "{name}" definition is not executable."#))
585                    } else {
586                        // Among type system definitions, only schema definitions do have a name
587                        Some("The schema definition is not executable.".to_string())
588                    }
589                }
590                ExecutableBuildError::AmbiguousAnonymousOperation => {
591                    Some("This anonymous operation must be the only defined operation.".to_string())
592                }
593                ExecutableBuildError::OperationNameCollision {
594                    name_at_previous_location,
595                } => Some(format!(
596                    r#"There can be only one operation named "{name_at_previous_location}"."#
597                )),
598                ExecutableBuildError::FragmentNameCollision {
599                    name_at_previous_location,
600                } => Some(format!(
601                    r#"There can be only one fragment named "{name_at_previous_location}"."#
602                )),
603                ExecutableBuildError::UndefinedRootOperation { operation_type } => Some(format!(
604                    // no period unlike other messages :zany_face:
605                    r#"The schema has no "{operation_type}" root type defined"#
606                )),
607                ExecutableBuildError::UndefinedTypeInNamedFragmentTypeCondition {
608                    type_name,
609                    ..
610                }
611                | ExecutableBuildError::UndefinedTypeInInlineFragmentTypeCondition {
612                    type_name,
613                    ..
614                } => Some(format!(r#"Unknown type "{type_name}"."#)),
615                ExecutableBuildError::SubselectionOnScalarType { type_name, path }
616                | ExecutableBuildError::SubselectionOnEnumType { type_name, path } => {
617                    #[allow(clippy::manual_map)]
618                    if let Some(field) = path.nested_fields.last() {
619                        Some(format!(
620                            r#"Field "{field}" must not have a selection since type "{type_name}" has no subfields"#
621                        ))
622                    } else {
623                        None // Can this happen?
624                    }
625                }
626                ExecutableBuildError::SubscriptionUsesMultipleFields { name, .. } => {
627                    if let Some(name) = name {
628                        Some(format!(
629                            r#"Subscription "{name}" must select only one top level field."#
630                        ))
631                    } else {
632                        Some(
633                            "Anonymous Subscription must select only one top level field."
634                                .to_string(),
635                        )
636                    }
637                }
638                ExecutableBuildError::SubscriptionUsesIntrospection { name, .. } => {
639                    if let Some(name) = name {
640                        Some(format!(
641                            r#"Subscription "{name}" must not select an introspection top level field."#
642                        ))
643                    } else {
644                        Some("Anonymous Subscription must not select an introspection top level field."
645                            .to_string())
646                    }
647                }
648                ExecutableBuildError::SubscriptionUsesConditionalSelection { name, .. } => {
649                    if let Some(name) = name {
650                        Some(format!(
651                            r#"Subscription "{name}" can not specify @skip or @include on root fields."#
652                        ))
653                    } else {
654                        Some(
655                            "Anonymous Subscription can not specify @skip or @include on root fields."
656                                .to_string(),
657                        )
658                    }
659                }
660                ExecutableBuildError::ConflictingFieldType(inner) => {
661                    let ConflictingFieldType {
662                        alias,
663                        original_type,
664                        conflicting_type,
665                        ..
666                    } = &**inner;
667                    Some(format!(
668                        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."#
669                    ))
670                }
671                ExecutableBuildError::ConflictingFieldName(inner) => {
672                    let ConflictingFieldName {
673                        alias,
674                        original_selection,
675                        conflicting_selection,
676                        ..
677                    } = &**inner;
678                    Some(format!(
679                        r#"Fields "{alias}" conflict because "{}" and "{}" are different fields. Use different aliases on the fields to fetch both if this was intentional."#,
680                        original_selection.attribute, conflicting_selection.attribute
681                    ))
682                }
683                ExecutableBuildError::ConflictingFieldArgument(inner) => {
684                    let ConflictingFieldArgument { alias, .. } = &**inner;
685                    Some(format!(
686                        r#"Fields "{alias}" conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional."#
687                    ))
688                }
689                ExecutableBuildError::DuplicateDeferLabel { .. } => Some(
690                    r#"Value for arguments "defer(label:)" and "stream(label:)" must be unique across all Defer/Stream directive usages."#
691                        .to_string(),
692                ),
693                ExecutableBuildError::DeferLabelMustNotBeVariable => Some(
694                    r#"Argument "@defer(label:)" must be a static string."#.to_string(),
695                ),
696                ExecutableBuildError::DeferOnRootMutationOrSubscriptionField {
697                    operation_type,
698                } => Some(format!(
699                    r#"Defer directive cannot be used on root {} type."#,
700                    operation_type.name()
701                )),
702                ExecutableBuildError::DeferInSubscriptionMustBeConditional => Some(
703                    "Defer directive not supported on subscription operations. Disable `@defer` by setting the `if` argument to `false`.".to_string(),
704                ),
705            },
706            _ => None,
707        }
708    }
709}
710
711impl ToCliReport for DiagnosticData {
712    fn location(&self) -> Option<SourceSpan> {
713        self.location
714    }
715
716    fn report(&self, report: &mut CliReport) {
717        if let Details::CompilerDiagnostic(diagnostic) = &self.details {
718            diagnostic.report(self.location, report);
719            return;
720        }
721
722        // Every case should also have a label at the main location
723        // (preferably saying something not completely redundant with the main message)
724        // and may have additional labels.
725        // Labels are always optional because locations are always optional,
726        // so essential information should be in the main message.
727        match &self.details {
728            Details::CompilerDiagnostic(_) => unreachable!(),
729            Details::ParserLimit { message, .. } => report.with_label_opt(self.location, message),
730            Details::SyntaxError { message, .. } => report.with_label_opt(self.location, message),
731            Details::SchemaBuildError(err) => match err {
732                SchemaBuildError::ExecutableDefinition { .. } => report.with_label_opt(
733                    self.location,
734                    "remove this definition, or use `parse_mixed()`",
735                ),
736                SchemaBuildError::SchemaDefinitionCollision {
737                    previous_location, ..
738                } => {
739                    report.with_label_opt(*previous_location, "previous `schema` definition here");
740                    report.with_label_opt(self.location, "`schema` redefined here");
741                    report.with_help(
742                        "merge this definition with the previous one, or use `extend schema`",
743                    );
744                }
745                SchemaBuildError::DirectiveDefinitionCollision {
746                    previous_location,
747                    name,
748                    ..
749                } => {
750                    report.with_label_opt(
751                        *previous_location,
752                        format_args!("previous definition of `@{name}` here"),
753                    );
754                    report.with_label_opt(self.location, format_args!("`@{name}` redefined here"));
755                    report.with_help("remove or rename one of the definitions");
756                }
757                SchemaBuildError::TypeDefinitionCollision {
758                    previous_location,
759                    name,
760                    ..
761                } => {
762                    report.with_label_opt(
763                        *previous_location,
764                        format_args!("previous definition of `{name}` here"),
765                    );
766                    report.with_label_opt(self.location, format_args!("`{name}` redefined here"));
767                    report.with_help("remove or rename one of the definitions, or use `extend`");
768                }
769                SchemaBuildError::BuiltInScalarTypeRedefinition => {
770                    report.with_label_opt(self.location, "remove this scalar definition");
771                }
772                SchemaBuildError::OrphanSchemaExtension => {
773                    report.with_label_opt(self.location, "extension here")
774                }
775                SchemaBuildError::OrphanTypeExtension { .. } => {
776                    report.with_label_opt(self.location, "extension here")
777                }
778                SchemaBuildError::TypeExtensionKindMismatch { def_location, .. } => {
779                    report.with_label_opt(*def_location, "type definition");
780                    report.with_label_opt(self.location, "extension here")
781                }
782                SchemaBuildError::DuplicateRootOperation {
783                    previous_location,
784                    operation_type,
785                    ..
786                } => {
787                    report.with_label_opt(
788                        *previous_location,
789                        format_args!("previous definition of `{operation_type}` here"),
790                    );
791                    report.with_label_opt(
792                        self.location,
793                        format_args!("`{operation_type}` redefined here"),
794                    );
795                }
796                SchemaBuildError::DuplicateImplementsInterfaceInObject {
797                    name_at_previous_location,
798                    ..
799                }
800                | SchemaBuildError::DuplicateImplementsInterfaceInInterface {
801                    name_at_previous_location,
802                    ..
803                } => {
804                    let previous_location = &name_at_previous_location.location();
805                    let name = name_at_previous_location;
806                    report.with_label_opt(
807                        *previous_location,
808                        format_args!("previous implementation of `{name}` here"),
809                    );
810                    report.with_label_opt(
811                        self.location,
812                        format_args!("`{name}` implemented again here"),
813                    );
814                }
815                SchemaBuildError::ObjectFieldNameCollision {
816                    name_at_previous_location,
817                    ..
818                }
819                | SchemaBuildError::InterfaceFieldNameCollision {
820                    name_at_previous_location,
821                    ..
822                }
823                | SchemaBuildError::EnumValueNameCollision {
824                    name_at_previous_location,
825                    ..
826                }
827                | SchemaBuildError::UnionMemberNameCollision {
828                    name_at_previous_location,
829                    ..
830                }
831                | SchemaBuildError::InputFieldNameCollision {
832                    name_at_previous_location,
833                    ..
834                } => {
835                    let previous_location = &name_at_previous_location.location();
836                    let name = name_at_previous_location;
837                    report.with_label_opt(
838                        *previous_location,
839                        format_args!("previous definition of `{name}` here"),
840                    );
841                    report.with_label_opt(self.location, format_args!("`{name}` redefined here"));
842                }
843            },
844            Details::ExecutableBuildError(err) => match err {
845                ExecutableBuildError::TypeSystemDefinition { .. } => report.with_label_opt(
846                    self.location,
847                    "remove this definition, or use `parse_mixed()`",
848                ),
849                ExecutableBuildError::AmbiguousAnonymousOperation => {
850                    report.with_label_opt(self.location, "provide a name for this definition");
851                    report.with_help(
852                        "GraphQL requires operations to be named if the document has more than one",
853                    );
854                }
855                ExecutableBuildError::OperationNameCollision {
856                    name_at_previous_location,
857                    ..
858                }
859                | ExecutableBuildError::FragmentNameCollision {
860                    name_at_previous_location,
861                    ..
862                } => {
863                    let previous_location = &name_at_previous_location.location();
864                    let name = name_at_previous_location;
865                    report.with_label_opt(
866                        *previous_location,
867                        format_args!("previous definition of `{name}` here"),
868                    );
869                    report.with_label_opt(self.location, format_args!("`{name}` redefined here"));
870                }
871                ExecutableBuildError::UndefinedRootOperation { operation_type, .. } => {
872                    report.with_label_opt(
873                        self.location,
874                        format_args!(
875                            "`{operation_type}` is not defined in the schema and is therefore not supported"
876                        ),
877                    );
878                    report.with_help(format_args!(
879                        "consider defining a `{operation_type}` root operation type in your schema"
880                    ))
881                }
882                ExecutableBuildError::UndefinedTypeInNamedFragmentTypeCondition { .. } => {
883                    report.with_label_opt(self.location, "type condition here")
884                }
885                ExecutableBuildError::UndefinedTypeInInlineFragmentTypeCondition {
886                    path, ..
887                } => {
888                    report.with_label_opt(self.location, "type condition here");
889                    report.with_note(format_args!("path to the inline fragment: `{path} → ...`"))
890                }
891                ExecutableBuildError::SubselectionOnScalarType { path, .. }
892                | ExecutableBuildError::SubselectionOnEnumType { path, .. } => {
893                    report.with_label_opt(self.location, "remove subselections here");
894                    report.with_note(format_args!("path to the field: `{path}`"))
895                }
896                ExecutableBuildError::UndefinedField {
897                    field_name,
898                    type_name,
899                    path,
900                    ..
901                } => {
902                    report.with_label_opt(
903                        self.location,
904                        format_args!("field `{field_name}` selected here"),
905                    );
906                    report.with_label_opt(
907                        type_name.location(),
908                        format_args!("type `{type_name}` defined here"),
909                    );
910                    report.with_note(format_args!("path to the field: `{path}`"))
911                }
912                ExecutableBuildError::SubscriptionUsesMultipleFields { fields, .. } => {
913                    report.with_label_opt(
914                        self.location,
915                        format_args!("subscription with {} root fields", fields.len()),
916                    );
917                    report.with_help(format_args!(
918                        "There are {} root fields: {}. This is not allowed.",
919                        fields.len(),
920                        CommaSeparated(fields)
921                    ));
922                }
923                ExecutableBuildError::SubscriptionUsesIntrospection { field, .. } => {
924                    report.with_label_opt(
925                        self.location,
926                        format_args!("{field} is an introspection field"),
927                    );
928                }
929                ExecutableBuildError::SubscriptionUsesConditionalSelection { .. } => {
930                    report.with_label_opt(self.location, "conditional directive used here");
931                }
932                ExecutableBuildError::DuplicateDeferLabel {
933                    original_location, ..
934                } => {
935                    report.with_label_opt(*original_location, "label first used here");
936                    report.with_label_opt(self.location, "duplicate label used here");
937                }
938                ExecutableBuildError::DeferLabelMustNotBeVariable => {
939                    report.with_label_opt(self.location, "variable used as label here");
940                }
941                ExecutableBuildError::DeferOnRootMutationOrSubscriptionField { operation_type } => {
942                    report.with_label_opt(
943                        self.location,
944                        format_args!("`@defer` on a root {} selection", operation_type.name()),
945                    );
946                }
947                ExecutableBuildError::DeferInSubscriptionMustBeConditional => {
948                    report.with_label_opt(
949                        self.location,
950                        "`@defer` in a subscription must use `if: false` or `if: $variable`",
951                    );
952                }
953                ExecutableBuildError::ConflictingFieldType(inner) => {
954                    let ConflictingFieldType {
955                        alias,
956                        original_location,
957                        original_coordinate,
958                        original_type,
959                        conflicting_location,
960                        conflicting_coordinate,
961                        conflicting_type,
962                    } = &**inner;
963                    report.with_label_opt(
964                        *original_location,
965                        format_args!(
966                        "`{alias}` is selected from `{original_coordinate}: {original_type}` here"
967                    ),
968                    );
969                    report.with_label_opt(
970                    *conflicting_location,
971                    format_args!("`{alias}` is selected from `{conflicting_coordinate}: {conflicting_type}` here"),
972                );
973                }
974                ExecutableBuildError::ConflictingFieldArgument(inner) => {
975                    let ConflictingFieldArgument {
976                        alias,
977                        original_location,
978                        original_coordinate,
979                        original_value,
980                        conflicting_location,
981                        conflicting_coordinate: _,
982                        conflicting_value,
983                    } = &**inner;
984                    let argument = &original_coordinate.argument;
985                    match (original_value, conflicting_value) {
986                        (Some(_), Some(_)) => {
987                            report.with_label_opt(
988                                *original_location,
989                                format_args!(
990                                    "`{original_coordinate}` is used with one argument value here"
991                                ),
992                            );
993                            report.with_label_opt(
994                                *conflicting_location,
995                                "but a different value here",
996                            );
997                        }
998                        (Some(_), None) => {
999                            report.with_label_opt(
1000                                *original_location,
1001                                format!("`{alias}` is selected with argument `{argument}` here",),
1002                            );
1003                            report.with_label_opt(
1004                                *conflicting_location,
1005                                format!("but argument `{argument}` is not provided here"),
1006                            );
1007                        }
1008                        (None, Some(_)) => {
1009                            report.with_label_opt(
1010                                *conflicting_location,
1011                                format!("`{alias}` is selected with argument `{argument}` here",),
1012                            );
1013                            report.with_label_opt(
1014                                *original_location,
1015                                format!("but argument `{argument}` is not provided here"),
1016                            );
1017                        }
1018                        (None, None) => unreachable!(),
1019                    }
1020                    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");
1021                }
1022                ExecutableBuildError::ConflictingFieldName(inner) => {
1023                    let ConflictingFieldName {
1024                        alias: field,
1025                        original_selection,
1026                        original_location,
1027                        conflicting_selection,
1028                        conflicting_location,
1029                    } = &**inner;
1030                    report.with_label_opt(
1031                        *original_location,
1032                        format_args!("`{field}` is selected from `{original_selection}` here"),
1033                    );
1034                    report.with_label_opt(
1035                        *conflicting_location,
1036                        format_args!("`{field}` is selected from `{conflicting_selection}` here"),
1037                    );
1038
1039                    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");
1040                }
1041            },
1042            Details::RecursionLimitError => {}
1043        }
1044    }
1045}
1046
1047impl Diagnostic<'_, DiagnosticData> {
1048    /// Get a [`serde`]-serializable version of the current diagnostic. This method mimicks the
1049    /// shape and message of errors produced by graphql-js.
1050    ///
1051    /// This is only for use by the Apollo Router, any other consumer is not supported.
1052    #[doc(hidden)]
1053    pub fn unstable_to_json_compat(&self) -> GraphQLError {
1054        GraphQLError::new(
1055            self.error
1056                .unstable_compat_message()
1057                .unwrap_or_else(|| self.error.to_string()),
1058            self.error.location(),
1059            self.sources,
1060        )
1061    }
1062}
1063
1064impl DiagnosticList {
1065    /// Creates an empty diagnostic list with the given source map.
1066    pub fn new(sources: SourceMap) -> Self {
1067        Self {
1068            sources,
1069            diagnostics_data: Vec::new(),
1070        }
1071    }
1072
1073    pub fn is_empty(&self) -> bool {
1074        self.diagnostics_data.is_empty()
1075    }
1076
1077    pub fn len(&self) -> usize {
1078        self.diagnostics_data.len()
1079    }
1080
1081    pub fn iter(
1082        &self,
1083    ) -> impl DoubleEndedIterator<Item = Diagnostic<'_, DiagnosticData>> + ExactSizeIterator {
1084        self.diagnostics_data
1085            .iter()
1086            .map(|data| data.to_diagnostic(&self.sources))
1087    }
1088
1089    pub(crate) fn push(&mut self, location: Option<SourceSpan>, details: impl Into<Details>) {
1090        self.diagnostics_data.push(DiagnosticData {
1091            location,
1092            details: details.into(),
1093        })
1094    }
1095
1096    /// Concatenate an `other` list of diagnostics into `self`, and sort them together.
1097    pub fn merge(&mut self, other: Self) {
1098        if !Arc::ptr_eq(&self.sources, &other.sources) {
1099            let sources = Arc::make_mut(&mut self.sources);
1100            for (&k, v) in &*other.sources {
1101                sources.entry(k).or_insert_with(|| v.clone());
1102            }
1103        }
1104        self.diagnostics_data.extend(other.diagnostics_data);
1105        self.sort()
1106    }
1107
1108    fn sort(&mut self) {
1109        self.diagnostics_data
1110            .sort_by_key(|err| err.location.map(|loc| (loc.file_id(), loc.offset())));
1111    }
1112
1113    pub(crate) fn into_result(mut self) -> Result<(), Self> {
1114        if self.diagnostics_data.is_empty() {
1115            Ok(())
1116        } else {
1117            self.sort();
1118            Err(self)
1119        }
1120    }
1121
1122    pub(crate) fn into_result_with<T>(self, value: T) -> Result<T, WithErrors<T>> {
1123        match self.into_result() {
1124            Ok(()) => Ok(value),
1125            Err(errors) => Err(WithErrors {
1126                partial: value,
1127                errors,
1128            }),
1129        }
1130    }
1131
1132    pub(crate) fn into_valid_result<T>(self, value: T) -> Result<Valid<T>, WithErrors<T>> {
1133        match self.into_result() {
1134            Ok(()) => Ok(Valid(value)),
1135            Err(errors) => Err(WithErrors {
1136                partial: value,
1137                errors,
1138            }),
1139        }
1140    }
1141}
1142
1143/// Use Display formatting to output without colors: `format!("{diagnostics}")`
1144impl fmt::Display for DiagnosticList {
1145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1146        for diagnostic in self.iter() {
1147            fmt::Display::fmt(&diagnostic, f)?
1148        }
1149        Ok(())
1150    }
1151}
1152
1153/// Use Debug formatting to output with colors: `format!("{diagnostics:?}")`
1154impl fmt::Debug for DiagnosticList {
1155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1156        for diagnostic in self.iter() {
1157            fmt::Debug::fmt(&diagnostic, f)?
1158        }
1159        Ok(())
1160    }
1161}
1162
1163impl From<SchemaBuildError> for Details {
1164    fn from(value: SchemaBuildError) -> Self {
1165        Details::SchemaBuildError(value)
1166    }
1167}
1168
1169impl From<ExecutableBuildError> for Details {
1170    fn from(value: ExecutableBuildError) -> Self {
1171        Details::ExecutableBuildError(value)
1172    }
1173}
1174
1175impl From<diagnostics::DiagnosticData> for Details {
1176    fn from(value: diagnostics::DiagnosticData) -> Self {
1177        Details::CompilerDiagnostic(value)
1178    }
1179}
1180
1181const DEFAULT_RECURSION_LIMIT: usize = 32;
1182
1183#[derive(Debug, Clone, thiserror::Error)]
1184#[error("Recursion limit reached")]
1185#[non_exhaustive]
1186struct RecursionLimitError {}
1187
1188/// Track recursion depth to prevent stack overflow.
1189#[derive(Debug)]
1190struct DepthCounter {
1191    value: usize,
1192    high: usize,
1193    limit: usize,
1194}
1195
1196impl DepthCounter {
1197    fn new() -> Self {
1198        Self {
1199            value: 0,
1200            high: 0,
1201            limit: DEFAULT_RECURSION_LIMIT,
1202        }
1203    }
1204
1205    fn with_limit(mut self, limit: usize) -> Self {
1206        self.limit = limit;
1207        self
1208    }
1209
1210    /// Return the actual API for tracking recursive uses.
1211    pub(crate) fn guard(&mut self) -> DepthGuard<'_> {
1212        DepthGuard(self)
1213    }
1214}
1215
1216/// Track call depth in a recursive function.
1217///
1218/// Pass the result of `guard.increment()` to recursive calls. When a guard is dropped,
1219/// its value is decremented.
1220struct DepthGuard<'a>(&'a mut DepthCounter);
1221
1222impl DepthGuard<'_> {
1223    /// Mark that we are recursing. If we reached the limit, return an error.
1224    fn increment(&mut self) -> Result<DepthGuard<'_>, RecursionLimitError> {
1225        self.0.value += 1;
1226        self.0.high = self.0.high.max(self.0.value);
1227        if self.0.value > self.0.limit {
1228            Err(RecursionLimitError {})
1229        } else {
1230            Ok(DepthGuard(self.0))
1231        }
1232    }
1233}
1234
1235impl Drop for DepthGuard<'_> {
1236    fn drop(&mut self) {
1237        // This may already be 0 if it's the original `counter.guard()` result, but that's fine
1238        self.0.value = self.0.value.saturating_sub(1);
1239    }
1240}
1241
1242/// Track used names in a recursive function.
1243#[derive(Debug)]
1244struct RecursionStack {
1245    seen: IndexSet<Name>,
1246    high: usize,
1247    limit: usize,
1248}
1249
1250impl RecursionStack {
1251    fn new() -> Self {
1252        Self {
1253            seen: IndexSet::with_hasher(Default::default()),
1254            high: 0,
1255            limit: DEFAULT_RECURSION_LIMIT,
1256        }
1257    }
1258
1259    fn with_root(root: Name) -> Self {
1260        let mut stack = Self::new();
1261        stack.seen.insert(root);
1262        stack
1263    }
1264
1265    fn with_limit(mut self, limit: usize) -> Self {
1266        self.limit = limit;
1267        self
1268    }
1269
1270    /// Return the actual API for tracking recursive uses.
1271    pub(crate) fn guard(&mut self) -> RecursionGuard<'_> {
1272        RecursionGuard(self)
1273    }
1274}
1275
1276/// Track used names in a recursive function.
1277///
1278/// Pass the result of `guard.push(name)` to recursive calls. Use `guard.contains(name)` to check
1279/// if the name was used somewhere up the call stack. When a guard is dropped, its name is removed
1280/// from the list.
1281struct RecursionGuard<'a>(&'a mut RecursionStack);
1282
1283impl RecursionGuard<'_> {
1284    /// Mark that we saw a name. If there are too many names, return an error.
1285    fn push(&mut self, name: &Name) -> Result<RecursionGuard<'_>, RecursionLimitError> {
1286        let new = self.0.seen.insert(name.clone());
1287        debug_assert!(
1288            new,
1289            "cannot push the same name twice to RecursionGuard, check contains() first"
1290        );
1291        self.0.high = self.0.high.max(self.0.seen.len());
1292        if self.0.seen.len() > self.0.limit {
1293            Err(RecursionLimitError {})
1294        } else {
1295            Ok(RecursionGuard(self.0))
1296        }
1297    }
1298
1299    /// Check if we saw a name somewhere up the call stack.
1300    fn contains(&self, name: &Name) -> bool {
1301        self.0.seen.contains(name)
1302    }
1303
1304    /// Return the name where we started.
1305    fn first(&self) -> Option<&Name> {
1306        self.0.seen.first()
1307    }
1308}
1309
1310impl Drop for RecursionGuard<'_> {
1311    fn drop(&mut self) {
1312        // This may already be empty if it's the original `stack.guard()` result, but that's fine
1313        let _ = self.0.seen.pop();
1314    }
1315}
1316
1317/// Errors that can happen when chasing potentially cyclical references.
1318#[derive(Debug, Clone, thiserror::Error)]
1319enum CycleError<T> {
1320    /// Detected a cycle, value contains the path from the offending node back to the node where we
1321    /// started.
1322    #[error("Cycle detected")]
1323    Recursed(Vec<Node<T>>),
1324    /// Ran into recursion limit before a cycle could be detected.
1325    #[error(transparent)]
1326    Limit(#[from] RecursionLimitError),
1327}
1328
1329impl<T> CycleError<T> {
1330    fn trace(mut self, node: &Node<T>) -> Self {
1331        if let Self::Recursed(trace) = &mut self {
1332            trace.push(node.clone());
1333        }
1334        self
1335    }
1336}
1337
1338struct CommaSeparated<'a, It>(&'a It);
1339impl<'a, T, It> fmt::Display for CommaSeparated<'a, It>
1340where
1341    T: fmt::Display,
1342    &'a It: IntoIterator<Item = T>,
1343{
1344    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1345        let mut it = self.0.into_iter();
1346        if let Some(element) = it.next() {
1347            element.fmt(f)?;
1348        }
1349        for element in it {
1350            f.write_str(", ")?;
1351            element.fmt(f)?;
1352        }
1353        Ok(())
1354    }
1355}