Skip to main content

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