apollo_compiler/validation/
selection.rs

1use crate::ast;
2use crate::ast::NamedType;
3use crate::collections::HashMap;
4use crate::collections::HashSet;
5use crate::collections::IndexMap;
6use crate::coordinate::TypeAttributeCoordinate;
7use crate::executable;
8use crate::executable::BuildError;
9use crate::executable::ConflictingFieldArgument;
10use crate::executable::ConflictingFieldName;
11use crate::executable::ConflictingFieldType;
12use crate::executable::SelectionSet;
13use crate::schema;
14use crate::validation::DiagnosticList;
15use crate::validation::OperationValidationContext;
16use crate::ExecutableDocument;
17use crate::Name;
18use crate::Node;
19use apollo_parser::LimitTracker;
20use std::cell::OnceCell;
21use std::collections::VecDeque;
22use std::hash::Hash;
23use std::rc::Rc;
24use std::sync::OnceLock;
25
26type Arena<'doc> = typed_arena::Arena<Vec<FieldSelection<'doc>>>;
27
28/// Represents a field selected against a parent type.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub(crate) struct FieldSelection<'a> {
31    /// Cached hash: field selections are always hashed at least once, and often multiple times.
32    /// Hashing Node<Field> is not that cheap, so we do it eagerly.
33    hash: u64,
34    /// The type of the selection set this field selection is part of.
35    pub parent_type: &'a NamedType,
36    pub field: &'a Node<executable::Field>,
37}
38
39impl Hash for FieldSelection<'_> {
40    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
41        state.write_u64(self.hash)
42    }
43}
44
45impl<'a> FieldSelection<'a> {
46    fn new(parent_type: &'a NamedType, field: &'a Node<executable::Field>) -> Self {
47        static SHARED_RANDOM: OnceLock<ahash::RandomState> = OnceLock::new();
48        let hash = SHARED_RANDOM
49            .get_or_init(Default::default)
50            .hash_one((parent_type, field));
51        Self {
52            hash,
53            parent_type,
54            field,
55        }
56    }
57
58    pub(crate) fn coordinate(&self) -> TypeAttributeCoordinate {
59        TypeAttributeCoordinate {
60            ty: self.parent_type.clone(),
61            attribute: self.field.name.clone(),
62        }
63    }
64}
65
66/// Expand one or more selection sets to a list of all fields selected.
67fn expand_selections<'doc>(
68    fragments: &'doc IndexMap<Name, Node<executable::Fragment>>,
69    selection_sets: impl Iterator<Item = &'doc executable::SelectionSet>,
70) -> Vec<FieldSelection<'doc>> {
71    let mut selections = vec![];
72    let mut queue: VecDeque<&executable::SelectionSet> = selection_sets.collect();
73    let mut seen_fragments = HashSet::with_hasher(ahash::RandomState::new());
74
75    while let Some(next_set) = queue.pop_front() {
76        for selection in &next_set.selections {
77            match selection {
78                executable::Selection::Field(field) => {
79                    selections.push(FieldSelection::new(&next_set.ty, field))
80                }
81                executable::Selection::InlineFragment(spread) => {
82                    queue.push_back(&spread.selection_set)
83                }
84                executable::Selection::FragmentSpread(spread)
85                    if seen_fragments.insert(&spread.fragment_name) =>
86                {
87                    if let Some(fragment) = fragments.get(&spread.fragment_name) {
88                        queue.push_back(&fragment.selection_set);
89                    }
90                }
91                executable::Selection::FragmentSpread(_) => {
92                    // Already seen
93                }
94            }
95        }
96    }
97
98    selections
99}
100
101fn is_composite(ty: &schema::ExtendedType) -> bool {
102    use schema::ExtendedType::*;
103    matches!(ty, Object(_) | Interface(_) | Union(_))
104}
105
106/// A temporary index for frequent argument lookups by name, using a hash map if there are many
107/// arguments.
108enum ArgumentLookup<'a> {
109    Map(HashMap<&'a Name, &'a Node<ast::Argument>>),
110    List(&'a [Node<ast::Argument>]),
111}
112impl<'a> ArgumentLookup<'a> {
113    fn new(list: &'a [Node<ast::Argument>]) -> Self {
114        if list.len() > 20 {
115            Self::Map(list.iter().map(|arg| (&arg.name, arg)).collect())
116        } else {
117            Self::List(list)
118        }
119    }
120
121    fn by_name(&self, name: &Name) -> Option<&'a Node<ast::Argument>> {
122        match self {
123            Self::Map(map) => map.get(name).copied(),
124            Self::List(list) => list.iter().find(|arg| arg.name == *name),
125        }
126    }
127}
128
129/// Check if two field selections from the overlapping types are the same, so the fields can be merged.
130fn same_name_and_arguments(
131    field_a: FieldSelection<'_>,
132    field_b: FieldSelection<'_>,
133) -> Result<(), BuildError> {
134    // 2bi. fieldA and fieldB must have identical field names.
135    if field_a.field.name != field_b.field.name {
136        return Err(BuildError::ConflictingFieldName(Box::new(
137            ConflictingFieldName {
138                alias: field_a.field.response_key().clone(),
139                original_location: field_a.field.location(),
140                original_selection: field_a.coordinate(),
141                conflicting_location: field_b.field.location(),
142                conflicting_selection: field_b.coordinate(),
143            },
144        )));
145    }
146
147    // 2bii. fieldA and fieldB must have identical sets of arguments.
148    let conflicting_field_argument =
149        |original_arg: Option<&Node<ast::Argument>>,
150         redefined_arg: Option<&Node<ast::Argument>>| {
151            debug_assert!(
152                    original_arg.is_some() || redefined_arg.is_some(),
153                    "a conflicting field argument error can only exist when at least one field has the argument",
154                );
155
156            // We can take the name from either one of the arguments as they are necessarily the same.
157            let arg = original_arg.or(redefined_arg).unwrap();
158
159            BuildError::ConflictingFieldArgument(Box::new(ConflictingFieldArgument {
160                // field_a and field_b have the same name so we can use either one.
161                alias: field_b.field.name.clone(),
162                original_location: field_a.field.location(),
163                original_coordinate: field_a.coordinate().with_argument(arg.name.clone()),
164                original_value: original_arg.map(|arg| (*arg.value).clone()),
165                conflicting_location: field_b.field.location(),
166                conflicting_coordinate: field_b.coordinate().with_argument(arg.name.clone()),
167                conflicting_value: redefined_arg.map(|arg| (*arg.value).clone()),
168            }))
169        };
170
171    // Check if fieldB provides the same argument names and values as fieldA (order-independent).
172    let self_args = ArgumentLookup::new(&field_a.field.arguments);
173    let other_args = ArgumentLookup::new(&field_b.field.arguments);
174    for arg in &field_a.field.arguments {
175        let Some(other_arg) = other_args.by_name(&arg.name) else {
176            return Err(conflicting_field_argument(Some(arg), None));
177        };
178
179        if !same_value(&other_arg.value, &arg.value) {
180            return Err(conflicting_field_argument(Some(arg), Some(other_arg)));
181        }
182    }
183    // Check if fieldB provides any arguments that fieldA does not provide.
184    for arg in &field_b.field.arguments {
185        if self_args.by_name(&arg.name).is_none() {
186            return Err(conflicting_field_argument(None, Some(arg)));
187        };
188    }
189
190    Ok(())
191}
192
193/// Compare two input values, with two special cases for objects: assuming no duplicate keys,
194/// and order-independence.
195fn same_value(left: &ast::Value, right: &ast::Value) -> bool {
196    match (left, right) {
197        (ast::Value::Null, ast::Value::Null) => true,
198        (ast::Value::Enum(left), ast::Value::Enum(right)) => left == right,
199        (ast::Value::Variable(left), ast::Value::Variable(right)) => left == right,
200        (ast::Value::String(left), ast::Value::String(right)) => left == right,
201        (ast::Value::Float(left), ast::Value::Float(right)) => left == right,
202        (ast::Value::Int(left), ast::Value::Int(right)) => left == right,
203        (ast::Value::Boolean(left), ast::Value::Boolean(right)) => left == right,
204        (ast::Value::List(left), ast::Value::List(right)) => left
205            .iter()
206            .zip(right.iter())
207            .all(|(left, right)| same_value(left, right)),
208        (ast::Value::Object(left), ast::Value::Object(right)) if left.len() == right.len() => {
209            // This check could miss out on keys that exist in `right`, but not in `left`, if `left` contains duplicate keys.
210            // We assume that that doesn't happen. GraphQL does not support duplicate keys and
211            // that is checked elsewhere in validation.
212            left.iter().all(|(key, value)| {
213                right
214                    .iter()
215                    .find(|(other_key, _)| key == other_key)
216                    .is_some_and(|(_, other_value)| same_value(value, other_value))
217            })
218        }
219        _ => false,
220    }
221}
222
223fn same_output_type_shape(
224    schema: &schema::Schema,
225    selection_a: FieldSelection<'_>,
226    selection_b: FieldSelection<'_>,
227) -> Result<(), BuildError> {
228    let field_a = &selection_a.field.definition;
229    let field_b = &selection_b.field.definition;
230
231    let mut type_a = &field_a.ty;
232    let mut type_b = &field_b.ty;
233
234    let mismatching_type_diagnostic = || {
235        BuildError::ConflictingFieldType(Box::new(ConflictingFieldType {
236            alias: selection_a.field.response_key().clone(),
237            original_location: selection_a.field.location(),
238            original_coordinate: selection_a.coordinate(),
239            original_type: field_a.ty.clone(),
240            conflicting_location: selection_b.field.location(),
241            conflicting_coordinate: selection_b.coordinate(),
242            conflicting_type: field_b.ty.clone(),
243        }))
244    };
245
246    // Steps 3 and 4 of the spec text unwrap both types simultaneously down to the named type.
247    // The apollo-rs representation of NonNull and Lists makes it tricky to follow the steps
248    // exactly.
249    //
250    // Instead we unwrap lists and non-null lists first, which leaves just a named type or a
251    // non-null named type...
252    while !type_a.is_named() || !type_b.is_named() {
253        // 4. If typeA or typeB is List.
254        // 4a. If typeA or typeB is not List, return false.
255        // 4b. Let typeA be the item type of typeA
256        // 4c. Let typeB be the item type of typeB
257        (type_a, type_b) = match (type_a, type_b) {
258            (ast::Type::List(type_a), ast::Type::List(type_b))
259            | (ast::Type::NonNullList(type_a), ast::Type::NonNullList(type_b)) => {
260                (type_a.as_ref(), type_b.as_ref())
261            }
262            (ast::Type::List(_), _)
263            | (_, ast::Type::List(_))
264            | (ast::Type::NonNullList(_), _)
265            | (_, ast::Type::NonNullList(_)) => return Err(mismatching_type_diagnostic()),
266            // Now it's a named type.
267            (type_a, type_b) => (type_a, type_b),
268        };
269    }
270
271    // Now we are down to two named types, we can check that they have the same nullability...
272    let (type_a, type_b) = match (type_a, type_b) {
273        (ast::Type::NonNullNamed(a), ast::Type::NonNullNamed(b)) => (a, b),
274        (ast::Type::Named(a), ast::Type::Named(b)) => (a, b),
275        _ => return Err(mismatching_type_diagnostic()),
276    };
277
278    let (Some(def_a), Some(def_b)) = (schema.types.get(type_a), schema.types.get(type_b)) else {
279        return Ok(()); // Cannot do much if we don't know the type
280    };
281
282    match (def_a, def_b) {
283        // 5. If typeA or typeB is Scalar or Enum.
284        (
285            def_a @ (schema::ExtendedType::Scalar(_) | schema::ExtendedType::Enum(_)),
286            def_b @ (schema::ExtendedType::Scalar(_) | schema::ExtendedType::Enum(_)),
287        ) => {
288            // 5a. If typeA and typeB are the same type return true, otherwise return false.
289            if def_a == def_b {
290                Ok(())
291            } else {
292                Err(mismatching_type_diagnostic())
293            }
294        }
295        // 6. If typeA or typeB is not a composite type, return false.
296        (def_a, def_b) if is_composite(def_a) && is_composite(def_b) => Ok(()),
297        _ => Err(mismatching_type_diagnostic()),
298    }
299}
300
301/// A boolean that turns on after the first check.
302struct OnceBool(std::cell::Cell<bool>);
303impl OnceBool {
304    fn new() -> Self {
305        Self(false.into())
306    }
307
308    /// Returns `false` the first time it is called, then returns `true` forever.
309    fn already_done(&self) -> bool {
310        self.0.replace(true)
311    }
312}
313
314/// Represents a merged field set that may or may not be valid.
315struct MergedFieldSet<'alloc, 'doc> {
316    selections: &'alloc [FieldSelection<'doc>],
317    grouped_by_output_names: OnceCell<IndexMap<Name, &'alloc [FieldSelection<'doc>]>>,
318    grouped_by_common_parents: OnceCell<Vec<&'alloc [FieldSelection<'doc>]>>,
319    same_response_shape_guard: OnceBool,
320    same_for_common_parents_guard: OnceBool,
321}
322
323impl<'alloc, 'doc> MergedFieldSet<'alloc, 'doc> {
324    fn new(selections: &'alloc [FieldSelection<'doc>]) -> Self {
325        Self {
326            selections,
327            grouped_by_output_names: Default::default(),
328            grouped_by_common_parents: Default::default(),
329            same_response_shape_guard: OnceBool::new(),
330            same_for_common_parents_guard: OnceBool::new(),
331        }
332    }
333
334    /// Given a set of fields, do all the fields that contribute to 1 output name have the same
335    /// shape?
336    ///
337    /// This prevents leaf output fields from having an inconsistent type.
338    fn same_response_shape_by_name(
339        &self,
340        validator: &mut FieldsInSetCanMerge<'alloc, '_, 'doc>,
341        diagnostics: &mut DiagnosticList,
342    ) {
343        // No need to do this if this field set has been checked before.
344        if self.same_response_shape_guard.already_done() {
345            return;
346        }
347
348        for fields_for_name in self.group_by_output_name(validator.alloc).values() {
349            let Some((field_a, rest)) = fields_for_name.split_first() else {
350                continue;
351            };
352            for field_b in rest {
353                // Covers steps 3-5 of the spec algorithm.
354                if let Err(err) = same_output_type_shape(validator.schema, *field_a, *field_b) {
355                    diagnostics.push(field_b.field.location(), err);
356                    continue;
357                }
358            }
359
360            let mut nested_selection_sets = fields_for_name
361                .iter()
362                .map(|selection| &selection.field.selection_set)
363                .filter(|set| !set.selections.is_empty())
364                .peekable();
365            if nested_selection_sets.peek().is_some() {
366                let merged_set = validator.expand_selections(nested_selection_sets);
367                validator.same_response_shape_by_name(merged_set, diagnostics);
368            }
369        }
370    }
371
372    /// Given a set of fields, do all the fields selecting from potentially overlapping types
373    /// select from the same thing?
374    ///
375    /// This prevents selecting two different fields from the same type into the same name. That
376    /// would be a contradiction because there would be no way to know which field takes precedence.
377    fn same_for_common_parents_by_name(
378        &self,
379        validator: &mut FieldsInSetCanMerge<'alloc, '_, 'doc>,
380        diagnostics: &mut DiagnosticList,
381    ) {
382        // No need to do this if this field set has been checked before.
383        if self.same_for_common_parents_guard.already_done() {
384            return;
385        }
386
387        for fields_for_name in self.group_by_output_name(validator.alloc).values() {
388            let selection_for_name = validator.lookup(fields_for_name);
389            for fields_for_parents in
390                selection_for_name.group_by_common_parents(validator.alloc, validator.schema)
391            {
392                // 2bi. fieldA and fieldB must have identical field names.
393                // 2bii. fieldA and fieldB must have identical sets of arguments.
394                // The same arguments check is reflexive so we don't need to check all
395                // combinations.
396                let Some((field_a, rest)) = fields_for_parents.split_first() else {
397                    continue;
398                };
399                for field_b in rest {
400                    if let Err(diagnostic) = same_name_and_arguments(*field_a, *field_b) {
401                        diagnostics.push(field_b.field.location(), diagnostic);
402                        continue;
403                    }
404                }
405
406                let mut nested_selection_sets = fields_for_parents
407                    .iter()
408                    .map(|selection| &selection.field.selection_set)
409                    .filter(|set| !set.selections.is_empty())
410                    .peekable();
411                if nested_selection_sets.peek().is_some() {
412                    let merged_set = validator.expand_selections(nested_selection_sets);
413                    validator.same_for_common_parents_by_name(merged_set, diagnostics);
414                }
415            }
416        }
417    }
418
419    fn group_by_output_name(
420        &self,
421        alloc: &'alloc Arena<'doc>,
422    ) -> &IndexMap<schema::Name, &'alloc [FieldSelection<'doc>]> {
423        self.grouped_by_output_names.get_or_init(|| {
424            let mut map = IndexMap::<_, Vec<_>>::with_hasher(Default::default());
425            for selection in self.selections {
426                map.entry(selection.field.response_key().clone())
427                    .or_default()
428                    .push(*selection);
429            }
430            map.into_iter()
431                .map(|(key, value)| (key, alloc.alloc(value).as_slice()))
432                .collect()
433        })
434    }
435
436    /// Returns potentially overlapping groups of fields. Fields overlap if they are selected from
437    /// the same concrete type or if they are selected from an abstract type (future schema changes
438    /// can make any abstract type overlap with any other type).
439    fn group_by_common_parents(
440        &self,
441        alloc: &'alloc Arena<'doc>,
442        schema: &schema::Schema,
443    ) -> &Vec<&'alloc [FieldSelection<'doc>]> {
444        self.grouped_by_common_parents.get_or_init(|| {
445            let mut abstract_parents = vec![];
446            let mut concrete_parents = IndexMap::<_, Vec<_>>::with_hasher(Default::default());
447            for selection in self.selections {
448                match schema.types.get(selection.parent_type) {
449                    Some(schema::ExtendedType::Object(object)) => {
450                        concrete_parents
451                            .entry(object.name.clone())
452                            .or_default()
453                            .push(*selection);
454                    }
455                    Some(schema::ExtendedType::Interface(_) | schema::ExtendedType::Union(_)) => {
456                        abstract_parents.push(*selection);
457                    }
458                    _ => {}
459                }
460            }
461
462            if concrete_parents.is_empty() {
463                vec![alloc.alloc(abstract_parents)]
464            } else {
465                concrete_parents
466                    .into_values()
467                    .map(|mut group| {
468                        group.extend(abstract_parents.iter().copied());
469                        alloc.alloc(group).as_slice()
470                    })
471                    .collect()
472            }
473        })
474    }
475}
476
477// The field depth in the field merging validation matches the nesting level in the resulting
478// data. It makes sense to use the same limit as serde_json.
479const FIELD_DEPTH_LIMIT: usize = 128;
480
481/// Implements the `FieldsInSetCanMerge()` validation.
482/// https://spec.graphql.org/draft/#sec-Field-Selection-Merging
483///
484/// This uses the [validation algorithm described by XING][0] ([archived][1]), which
485/// scales much better with larger selection sets that may have many overlapping fields,
486/// and with widespread use of fragments.
487///
488/// [0]: https://tech.new-work.se/graphql-overlapping-fields-can-be-merged-fast-ea6e92e0a01
489/// [1]: https://web.archive.org/web/20240208084612/https://tech.new-work.se/graphql-overlapping-fields-can-be-merged-fast-ea6e92e0a01
490pub(crate) struct FieldsInSetCanMerge<'alloc, 's, 'doc> {
491    alloc: &'alloc Arena<'doc>,
492    schema: &'s schema::Schema,
493    document: &'doc executable::ExecutableDocument,
494    /// Stores merged field sets.
495    ///
496    /// The value is an Rc because it needs to have an independent lifetime from `self`,
497    /// so the cache can be updated while a field set is borrowed.
498    cache: HashMap<&'alloc [FieldSelection<'doc>], Rc<MergedFieldSet<'alloc, 'doc>>>,
499    // The recursion limit is used for two separate recursions, but they are not interleaved,
500    // so the effective limit does apply to field nesting levels in both cases.
501    recursion_limit: LimitTracker,
502}
503
504impl<'alloc, 's, 'doc> FieldsInSetCanMerge<'alloc, 's, 'doc> {
505    pub(crate) fn new(
506        alloc: &'alloc Arena<'doc>,
507        schema: &'s schema::Schema,
508        document: &'doc executable::ExecutableDocument,
509    ) -> Self {
510        Self {
511            alloc,
512            schema,
513            document,
514            cache: Default::default(),
515            recursion_limit: LimitTracker::new(FIELD_DEPTH_LIMIT),
516        }
517    }
518
519    fn expand_selections(
520        &self,
521        selection_sets: impl Iterator<Item = &'doc executable::SelectionSet>,
522    ) -> &'alloc [FieldSelection<'doc>] {
523        self.alloc
524            .alloc(expand_selections(&self.document.fragments, selection_sets))
525    }
526
527    pub(crate) fn validate_operation(
528        &mut self,
529        operation: &'doc Node<executable::Operation>,
530        diagnostics: &mut DiagnosticList,
531    ) {
532        let fields = self.expand_selections(std::iter::once(&operation.selection_set));
533        let set = self.lookup(fields);
534        set.same_response_shape_by_name(self, diagnostics);
535        set.same_for_common_parents_by_name(self, diagnostics);
536
537        if self.recursion_limit.high > self.recursion_limit.limit {
538            diagnostics.push(operation.location(), super::Details::RecursionLimitError);
539        }
540    }
541
542    fn lookup(
543        &mut self,
544        selections: &'alloc [FieldSelection<'doc>],
545    ) -> Rc<MergedFieldSet<'alloc, 'doc>> {
546        self.cache
547            .entry(selections)
548            .or_insert_with(|| Rc::new(MergedFieldSet::new(selections)))
549            .clone()
550    }
551
552    fn same_for_common_parents_by_name(
553        &mut self,
554        selections: &'alloc [FieldSelection<'doc>],
555        diagnostics: &mut DiagnosticList,
556    ) {
557        if self.recursion_limit.check_and_increment() {
558            return;
559        }
560        let field_set = self.lookup(selections);
561        field_set.same_for_common_parents_by_name(self, diagnostics);
562        self.recursion_limit.decrement();
563    }
564
565    fn same_response_shape_by_name(
566        &mut self,
567        selections: &'alloc [FieldSelection<'doc>],
568        diagnostics: &mut DiagnosticList,
569    ) {
570        if self.recursion_limit.check_and_increment() {
571            return;
572        }
573        let field_set = self.lookup(selections);
574        field_set.same_response_shape_by_name(self, diagnostics);
575        self.recursion_limit.decrement();
576    }
577}
578
579pub(crate) fn validate_selection_set(
580    diagnostics: &mut DiagnosticList,
581    document: &ExecutableDocument,
582    against_type: Option<(&crate::Schema, &NamedType)>,
583    selection_set: &SelectionSet,
584    context: &mut OperationValidationContext<'_>,
585) {
586    for selection in &selection_set.selections {
587        match selection {
588            executable::Selection::Field(field) => {
589                super::field::validate_field(diagnostics, document, against_type, field, context)
590            }
591            executable::Selection::FragmentSpread(fragment) => {
592                super::fragment::validate_fragment_spread(
593                    diagnostics,
594                    document,
595                    against_type,
596                    fragment,
597                    context,
598                )
599            }
600            executable::Selection::InlineFragment(inline) => {
601                super::fragment::validate_inline_fragment(
602                    diagnostics,
603                    document,
604                    against_type,
605                    inline,
606                    context,
607                )
608            }
609        }
610    }
611}