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