|
| 1 | +use crate::collections::HashSet; |
1 | 2 | use crate::executable;
|
| 3 | +use crate::validation::diagnostics::DiagnosticData; |
2 | 4 | use crate::validation::DiagnosticList;
|
3 | 5 | use crate::validation::ExecutableValidationContext;
|
| 6 | +use crate::validation::RecursionLimitError; |
4 | 7 | use crate::ExecutableDocument;
|
| 8 | +use crate::Name; |
5 | 9 | use crate::Node;
|
6 | 10 |
|
| 11 | +/// Iterate all selections in the selection set. |
| 12 | +/// |
| 13 | +/// This includes fields, fragment spreads, and inline fragments. For fragments, both the spread |
| 14 | +/// and the fragment's nested selections are reported. |
| 15 | +/// |
| 16 | +/// Does not recurse into nested fields. |
| 17 | +fn walk_selections<'doc>( |
| 18 | + document: &'doc ExecutableDocument, |
| 19 | + selections: &'doc executable::SelectionSet, |
| 20 | + mut f: impl FnMut(&'doc executable::Selection), |
| 21 | +) -> Result<(), RecursionLimitError> { |
| 22 | + fn walk_selections_inner<'doc>( |
| 23 | + document: &'doc ExecutableDocument, |
| 24 | + selection_set: &'doc executable::SelectionSet, |
| 25 | + seen: &mut HashSet<&'doc Name>, |
| 26 | + f: &mut dyn FnMut(&'doc executable::Selection), |
| 27 | + ) -> Result<(), RecursionLimitError> { |
| 28 | + for selection in &selection_set.selections { |
| 29 | + f(selection); |
| 30 | + match selection { |
| 31 | + executable::Selection::Field(_) => { |
| 32 | + // Nothing to do |
| 33 | + } |
| 34 | + executable::Selection::FragmentSpread(fragment) => { |
| 35 | + let new = seen.insert(&fragment.fragment_name); |
| 36 | + if !new { |
| 37 | + continue; |
| 38 | + } |
| 39 | + |
| 40 | + // If the fragment doesn't exist, that error is reported elsewhere. |
| 41 | + if let Some(fragment_definition) = |
| 42 | + document.fragments.get(&fragment.fragment_name) |
| 43 | + { |
| 44 | + walk_selections_inner( |
| 45 | + document, |
| 46 | + &fragment_definition.selection_set, |
| 47 | + seen, |
| 48 | + f, |
| 49 | + )?; |
| 50 | + } |
| 51 | + } |
| 52 | + executable::Selection::InlineFragment(fragment) => { |
| 53 | + walk_selections_inner(document, &fragment.selection_set, seen, f)?; |
| 54 | + } |
| 55 | + } |
| 56 | + } |
| 57 | + Ok(()) |
| 58 | + } |
| 59 | + |
| 60 | + walk_selections_inner(document, selections, &mut HashSet::default(), &mut f) |
| 61 | +} |
| 62 | + |
7 | 63 | pub(crate) fn validate_subscription(
|
8 | 64 | document: &executable::ExecutableDocument,
|
9 | 65 | operation: &Node<executable::Operation>,
|
10 | 66 | diagnostics: &mut DiagnosticList,
|
11 | 67 | ) {
|
12 |
| - if operation.is_subscription() { |
13 |
| - let fields = super::selection::expand_selections( |
14 |
| - &document.fragments, |
15 |
| - std::iter::once(&operation.selection_set), |
16 |
| - Some((operation, diagnostics)), |
17 |
| - ); |
| 68 | + if !operation.is_subscription() { |
| 69 | + return; |
| 70 | + } |
18 | 71 |
|
19 |
| - if fields.len() > 1 { |
20 |
| - diagnostics.push( |
21 |
| - operation.location(), |
22 |
| - executable::BuildError::SubscriptionUsesMultipleFields { |
23 |
| - name: operation.name.clone(), |
24 |
| - fields: fields |
25 |
| - .iter() |
26 |
| - .map(|field| field.field.name.clone()) |
27 |
| - .collect(), |
28 |
| - }, |
29 |
| - ); |
| 72 | + let mut field_names = vec![]; |
| 73 | + |
| 74 | + let walked = walk_selections(document, &operation.selection_set, |selection| { |
| 75 | + if let executable::Selection::Field(field) = selection { |
| 76 | + field_names.push(field.name.clone()); |
| 77 | + if matches!(field.name.as_str(), "__type" | "__schema" | "__typename") { |
| 78 | + diagnostics.push( |
| 79 | + field.location(), |
| 80 | + executable::BuildError::SubscriptionUsesIntrospection { |
| 81 | + name: operation.name.clone(), |
| 82 | + field: field.name.clone(), |
| 83 | + }, |
| 84 | + ); |
| 85 | + } |
30 | 86 | }
|
31 | 87 |
|
32 |
| - let has_introspection_fields = fields |
| 88 | + if let Some(conditional_directive) = selection |
| 89 | + .directives() |
33 | 90 | .iter()
|
34 |
| - .find(|field| { |
35 |
| - matches!( |
36 |
| - field.field.name.as_str(), |
37 |
| - "__type" | "__schema" | "__typename" |
38 |
| - ) |
39 |
| - }) |
40 |
| - .map(|field| &field.field); |
41 |
| - if let Some(field) = has_introspection_fields { |
| 91 | + .find(|d| matches!(d.name.as_str(), "skip" | "include")) |
| 92 | + { |
42 | 93 | diagnostics.push(
|
43 |
| - field.location(), |
44 |
| - executable::BuildError::SubscriptionUsesIntrospection { |
| 94 | + conditional_directive.location(), |
| 95 | + executable::BuildError::SubscriptionUsesConditionalSelection { |
45 | 96 | name: operation.name.clone(),
|
46 |
| - field: field.name.clone(), |
47 | 97 | },
|
48 | 98 | );
|
49 | 99 | }
|
| 100 | + }); |
| 101 | + |
| 102 | + if walked.is_err() { |
| 103 | + diagnostics.push(None, DiagnosticData::RecursionError {}); |
| 104 | + return; |
| 105 | + } |
| 106 | + |
| 107 | + if field_names.len() > 1 { |
| 108 | + diagnostics.push( |
| 109 | + operation.location(), |
| 110 | + executable::BuildError::SubscriptionUsesMultipleFields { |
| 111 | + name: operation.name.clone(), |
| 112 | + fields: field_names, |
| 113 | + }, |
| 114 | + ); |
50 | 115 | }
|
51 | 116 | }
|
52 | 117 |
|
|
0 commit comments