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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub(crate) struct FieldSelection<'a> {
31 hash: u64,
34 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
66fn 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 }
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
106enum 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
129fn same_name_and_arguments(
131 field_a: FieldSelection<'_>,
132 field_b: FieldSelection<'_>,
133) -> Result<(), BuildError> {
134 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 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 let arg = original_arg.or(redefined_arg).unwrap();
158
159 BuildError::ConflictingFieldArgument(Box::new(ConflictingFieldArgument {
160 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 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 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
193fn 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 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 while !type_a.is_named() || !type_b.is_named() {
253 (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 (type_a, type_b) => (type_a, type_b),
268 };
269 }
270
271 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(()); };
281
282 match (def_a, def_b) {
283 (
285 def_a @ (schema::ExtendedType::Scalar(_) | schema::ExtendedType::Enum(_)),
286 def_b @ (schema::ExtendedType::Scalar(_) | schema::ExtendedType::Enum(_)),
287 ) => {
288 if def_a == def_b {
290 Ok(())
291 } else {
292 Err(mismatching_type_diagnostic())
293 }
294 }
295 (def_a, def_b) if is_composite(def_a) && is_composite(def_b) => Ok(()),
297 _ => Err(mismatching_type_diagnostic()),
298 }
299}
300
301struct OnceBool(std::cell::Cell<bool>);
303impl OnceBool {
304 fn new() -> Self {
305 Self(false.into())
306 }
307
308 fn already_done(&self) -> bool {
310 self.0.replace(true)
311 }
312}
313
314struct 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 fn same_response_shape_by_name(
339 &self,
340 validator: &mut FieldsInSetCanMerge<'alloc, '_, 'doc>,
341 diagnostics: &mut DiagnosticList,
342 ) {
343 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 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 fn same_for_common_parents_by_name(
378 &self,
379 validator: &mut FieldsInSetCanMerge<'alloc, '_, 'doc>,
380 diagnostics: &mut DiagnosticList,
381 ) {
382 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 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 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
477const FIELD_DEPTH_LIMIT: usize = 128;
480
481pub(crate) struct FieldsInSetCanMerge<'alloc, 's, 'doc> {
491 alloc: &'alloc Arena<'doc>,
492 schema: &'s schema::Schema,
493 document: &'doc executable::ExecutableDocument,
494 cache: HashMap<&'alloc [FieldSelection<'doc>], Rc<MergedFieldSet<'alloc, 'doc>>>,
499 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}