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