Skip to content

Commit be2f362

Browse files
committed
Use a single special-purpose walk in subscription validation
1 parent 7c66e7f commit be2f362

File tree

7 files changed

+451
-56
lines changed

7 files changed

+451
-56
lines changed

crates/apollo-compiler/src/validation/mod.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
//! Supporting APIs for [GraphQL validation](https://spec.graphql.org/October2021/#sec-Validation)
22
//! and other kinds of errors.
33
4-
use crate::coordinate::SchemaCoordinate;
5-
#[cfg(doc)]
6-
use crate::ExecutableDocument;
7-
use crate::Schema;
8-
94
pub(crate) mod argument;
105
pub(crate) mod diagnostics;
116
pub(crate) mod directive;
@@ -26,6 +21,7 @@ pub(crate) mod variable;
2621
use crate::collections::HashMap;
2722
use crate::collections::HashSet;
2823
use crate::collections::IndexSet;
24+
use crate::coordinate::SchemaCoordinate;
2925
use crate::diagnostic::CliReport;
3026
use crate::diagnostic::Diagnostic;
3127
use crate::diagnostic::ToCliReport;
@@ -41,6 +37,7 @@ use crate::schema::BuildError as SchemaBuildError;
4137
use crate::schema::Implementers;
4238
use crate::Name;
4339
use crate::Node;
40+
use crate::Schema;
4441
use std::fmt;
4542
use std::sync::Arc;
4643
use std::sync::OnceLock;

crates/apollo-compiler/src/validation/operation.rs

Lines changed: 94 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,117 @@
1+
use crate::collections::HashSet;
12
use crate::executable;
3+
use crate::validation::diagnostics::DiagnosticData;
24
use crate::validation::DiagnosticList;
35
use crate::validation::ExecutableValidationContext;
6+
use crate::validation::RecursionLimitError;
47
use crate::ExecutableDocument;
8+
use crate::Name;
59
use crate::Node;
610

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+
763
pub(crate) fn validate_subscription(
864
document: &executable::ExecutableDocument,
965
operation: &Node<executable::Operation>,
1066
diagnostics: &mut DiagnosticList,
1167
) {
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+
}
1871

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+
}
3086
}
3187

32-
let has_introspection_fields = fields
88+
if let Some(conditional_directive) = selection
89+
.directives()
3390
.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+
{
4293
diagnostics.push(
43-
field.location(),
44-
executable::BuildError::SubscriptionUsesIntrospection {
94+
conditional_directive.location(),
95+
executable::BuildError::SubscriptionUsesConditionalSelection {
4596
name: operation.name.clone(),
46-
field: field.name.clone(),
4797
},
4898
);
4999
}
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+
);
50115
}
51116
}
52117

crates/apollo-compiler/src/validation/selection.rs

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -64,31 +64,16 @@ impl<'a> FieldSelection<'a> {
6464
}
6565

6666
/// Expand one or more selection sets to a list of all fields selected.
67-
pub(crate) fn expand_selections<'doc>(
67+
fn expand_selections<'doc>(
6868
fragments: &'doc IndexMap<Name, Node<executable::Fragment>>,
6969
selection_sets: impl Iterator<Item = &'doc executable::SelectionSet>,
70-
mut for_subscription_top_level: Option<(&executable::Operation, &mut DiagnosticList)>,
7170
) -> Vec<FieldSelection<'doc>> {
7271
let mut selections = vec![];
7372
let mut queue: VecDeque<&executable::SelectionSet> = selection_sets.collect();
7473
let mut seen_fragments = HashSet::with_hasher(ahash::RandomState::new());
7574

7675
while let Some(next_set) = queue.pop_front() {
7776
for selection in &next_set.selections {
78-
if let Some((operation, diagnostics)) = &mut for_subscription_top_level {
79-
if let Some(conditional_directive) = selection
80-
.directives()
81-
.iter()
82-
.find(|d| matches!(d.name.as_str(), "skip" | "include"))
83-
{
84-
diagnostics.push(
85-
conditional_directive.location(),
86-
executable::BuildError::SubscriptionUsesConditionalSelection {
87-
name: operation.name.clone(),
88-
},
89-
);
90-
}
91-
}
9277
match selection {
9378
executable::Selection::Field(field) => {
9479
selections.push(FieldSelection::new(&next_set.ty, field))
@@ -535,11 +520,8 @@ impl<'alloc, 's, 'doc> FieldsInSetCanMerge<'alloc, 's, 'doc> {
535520
&self,
536521
selection_sets: impl Iterator<Item = &'doc executable::SelectionSet>,
537522
) -> &'alloc [FieldSelection<'doc>] {
538-
self.alloc.alloc(expand_selections(
539-
&self.document.fragments,
540-
selection_sets,
541-
None,
542-
))
523+
self.alloc
524+
.alloc(expand_selections(&self.document.fragments, selection_sets))
543525
}
544526

545527
pub(crate) fn validate_operation(

crates/apollo-compiler/src/validation/variable.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,14 @@ pub(crate) fn validate_variable_definitions(
8080
}
8181
}
8282

83-
/// Named fragments are "deduplicated": only visited once even if spread multiple times
83+
/// Call a function for every selection that is reachable from the given selection set.
84+
///
85+
/// This includes fields, fragment spreads, and inline fragments. For fragments, both the spread
86+
/// and the fragment's nested selections are reported. For fields, nested selections are also
87+
/// reported.
88+
///
89+
/// Named fragments are "deduplicated": only visited once even if spread multiple times *in
90+
/// different locations*. This is only appropriate for certain kinds of validations, so reuser beware.
8491
fn walk_selections_with_deduped_fragments<'doc>(
8592
document: &'doc ExecutableDocument,
8693
selections: &'doc executable::SelectionSet,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
subscription ConditionalSub($includeContent: Boolean = true, $small: Boolean = true) {
2+
messages {
3+
username
4+
text @include(if: $includeContent)
5+
avatar @skip(if: $small)
6+
}
7+
}
8+
9+
type Query {
10+
hello: String
11+
}
12+
13+
type Message {
14+
username: String
15+
text: String
16+
avatar: String
17+
}
18+
19+
type Subscription {
20+
messages: Message
21+
}

0 commit comments

Comments
 (0)