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