Skip to content

Commit 35f280c

Browse files
sachindshindeSimonSapingoto-bus-stop
authored
Avoid reprocessing named fragments (#952)
* Skip already-visited named fragments for finding unused variables * Skip already-visited named fragment for cycle detection * Only validate fragment definitions once per operation * Skip already-visited named fragments in introspection::check_max_depth * Deduplicate named fragments in `Operation::root_fields` and `named_fields` * Remember the depth of already-checked fragments --------- Co-authored-by: Simon Sapin <[email protected]> Co-authored-by: Renée Kooi <[email protected]>
1 parent 65f14e1 commit 35f280c

File tree

12 files changed

+151
-83
lines changed

12 files changed

+151
-83
lines changed

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

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ pub use crate::ast::OperationType;
7979
pub use crate::ast::Type;
8080
pub use crate::ast::Value;
8181
pub use crate::ast::VariableDefinition;
82+
use crate::collections::HashSet;
8283
use crate::request::RequestError;
8384
pub use crate::Name;
8485

@@ -540,17 +541,18 @@ impl Operation {
540541
///
541542
/// `document` is used to look up fragment definitions.
542543
///
543-
/// This does **not** perform [field merging] nor fragment spreads de-duplication,
544-
/// so multiple items in this iterator may have the same response key,
545-
/// point to the same field definition,
546-
/// or even be the same field selection (reached through different fragments).
544+
/// This does **not** perform [field merging],
545+
/// so multiple items in this iterator may have the same response key
546+
/// or point to the same field definition.
547+
/// Named fragments however are only traversed once even if spread multiple times.
547548
///
548549
/// [field merging]: https://spec.graphql.org/draft/#sec-Field-Selection-Merging
549550
pub fn root_fields<'doc>(
550551
&'doc self,
551552
document: &'doc ExecutableDocument,
552553
) -> impl Iterator<Item = &'doc Node<Field>> {
553554
let mut stack = vec![self.selection_set.selections.iter()];
555+
let mut fragments_seen = HashSet::default();
554556
std::iter::from_fn(move || {
555557
while let Some(selection_set_iter) = stack.last_mut() {
556558
match selection_set_iter.next() {
@@ -564,7 +566,10 @@ impl Operation {
564566
}
565567
Some(Selection::FragmentSpread(spread)) => {
566568
if let Some(def) = document.fragments.get(&spread.fragment_name) {
567-
stack.push(def.selection_set.selections.iter())
569+
let new = fragments_seen.insert(&spread.fragment_name);
570+
if new {
571+
stack.push(def.selection_set.selections.iter())
572+
}
568573
} else {
569574
// Undefined fragments are silently ignored.
570575
// They should never happen in a valid document.
@@ -587,16 +592,18 @@ impl Operation {
587592
///
588593
/// `document` is used to look up fragment definitions.
589594
///
590-
/// This does **not** perform [field merging] nor fragment spreads de-duplication,
591-
/// so multiple items in this iterator may have the same response key,
592-
/// point to the same field definition, or even be the same field selection.
595+
/// This does **not** perform [field merging],
596+
/// so multiple items in this iterator may have the same response key
597+
/// or point to the same field definition.
598+
/// Named fragments however are only traversed once even if spread multiple times.
593599
///
594600
/// [field merging]: https://spec.graphql.org/draft/#sec-Field-Selection-Merging
595601
pub fn all_fields<'doc>(
596602
&'doc self,
597603
document: &'doc ExecutableDocument,
598604
) -> impl Iterator<Item = &'doc Node<Field>> {
599605
let mut stack = vec![self.selection_set.selections.iter()];
606+
let mut fragments_seen = HashSet::default();
600607
std::iter::from_fn(move || {
601608
while let Some(selection_set_iter) = stack.last_mut() {
602609
match selection_set_iter.next() {
@@ -613,7 +620,10 @@ impl Operation {
613620
}
614621
Some(Selection::FragmentSpread(spread)) => {
615622
if let Some(def) = document.fragments.get(&spread.fragment_name) {
616-
stack.push(def.selection_set.selections.iter())
623+
let new = fragments_seen.insert(&spread.fragment_name);
624+
if new {
625+
stack.push(def.selection_set.selections.iter())
626+
}
617627
} else {
618628
// Undefined fragments are silently ignored.
619629
// They should never happen in a valid document.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,6 @@ pub(crate) fn validate_field_set(
6161
document,
6262
Some((schema, &field_set.selection_set.ty)),
6363
&field_set.selection_set,
64-
context.operation_context(&[]),
64+
&mut context.operation_context(&[]),
6565
)
6666
}
Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,57 @@
1+
use crate::collections::HashMap;
12
use crate::executable::Selection;
23
use crate::executable::SelectionSet;
34
use crate::request::RequestError;
45
use crate::validation::Valid;
56
use crate::ExecutableDocument;
7+
use crate::Name;
68

79
const MAX_LISTS_DEPTH: u32 = 3;
810

9-
pub(super) fn check_selection_set(
10-
document: &Valid<ExecutableDocument>,
11+
pub(super) fn check_selection_set<'doc>(
12+
document: &'doc Valid<ExecutableDocument>,
13+
fragment_depths: &mut HashMap<&'doc Name, u32>,
1114
depth_so_far: u32,
12-
selection_set: &SelectionSet,
13-
) -> Result<(), RequestError> {
15+
selection_set: &'doc SelectionSet,
16+
) -> Result<u32, RequestError> {
17+
let mut max_depth = depth_so_far;
1418
for selection in &selection_set.selections {
1519
match selection {
1620
Selection::InlineFragment(inline) => {
17-
check_selection_set(document, depth_so_far, &inline.selection_set)?
21+
max_depth = max_depth.max(check_selection_set(
22+
document,
23+
fragment_depths,
24+
depth_so_far,
25+
&inline.selection_set,
26+
)?)
1827
}
1928
Selection::FragmentSpread(spread) => {
20-
// Validation ensures that `Valid<ExecutableDocument>` does not contain fragment cycles
21-
if let Some(def) = document.fragments.get(&spread.fragment_name) {
22-
check_selection_set(document, depth_so_far, &def.selection_set)?
29+
let Some(def) = document.fragments.get(&spread.fragment_name) else {
30+
continue;
31+
};
32+
// Avoiding the entry API because we may have to modify the map in-between this `.get()`
33+
// and the `.insert()`.
34+
if let Some(fragment_depth) = fragment_depths.get(&spread.fragment_name) {
35+
if depth_so_far + *fragment_depth > MAX_LISTS_DEPTH {
36+
return Err(RequestError {
37+
message: "Maximum introspection depth exceeded".into(),
38+
location: spread.location(),
39+
is_suspected_validation_bug: false,
40+
});
41+
}
42+
} else {
43+
// Recursing without marking our fragment spread as used is fine,
44+
// because validation guarantees that we do not have a self-referential
45+
// fragment chain.
46+
let post_fragment_depth = check_selection_set(
47+
document,
48+
fragment_depths,
49+
depth_so_far,
50+
&def.selection_set,
51+
)?;
52+
fragment_depths
53+
.insert(&spread.fragment_name, post_fragment_depth - depth_so_far);
54+
max_depth = max_depth.max(post_fragment_depth);
2355
}
2456
}
2557
Selection::Field(field) => {
@@ -37,9 +69,14 @@ pub(super) fn check_selection_set(
3769
});
3870
}
3971
}
40-
check_selection_set(document, depth, &field.selection_set)?
72+
max_depth = max_depth.max(check_selection_set(
73+
document,
74+
fragment_depths,
75+
depth,
76+
&field.selection_set,
77+
)?)
4178
}
4279
}
4380
}
44-
Ok(())
81+
Ok(max_depth)
4582
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,13 @@ pub fn check_max_depth(
4242
operation: &Operation,
4343
) -> Result<(), RequestError> {
4444
let initial_depth = 0;
45-
max_depth::check_selection_set(document, initial_depth, &operation.selection_set)
45+
max_depth::check_selection_set(
46+
document,
47+
&mut HashMap::default(),
48+
initial_depth,
49+
&operation.selection_set,
50+
)
51+
.map(drop)
4652
}
4753

4854
/// Excecutes the [schema introspection](https://spec.graphql.org/draft/#sec-Schema-Introspection)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub(crate) fn validate_field(
1919
// May be None if a parent selection was invalid
2020
against_type: Option<(&crate::Schema, &ast::NamedType)>,
2121
field: &Node<executable::Field>,
22-
context: OperationValidationContext<'_>,
22+
context: &mut OperationValidationContext<'_>,
2323
) {
2424
// First do all the validation that we can without knowing the type of the field.
2525

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

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::ast;
22
use crate::ast::NamedType;
33
use crate::collections::HashMap;
4+
use crate::collections::HashSet;
45
use crate::collections::IndexSet;
56
use crate::executable;
67
use crate::schema;
@@ -55,7 +56,7 @@ fn validate_fragment_spread_type(
5556
against_type: &NamedType,
5657
type_condition: &NamedType,
5758
selection: &executable::Selection,
58-
context: OperationValidationContext<'_>,
59+
context: &mut OperationValidationContext<'_>,
5960
) {
6061
// Treat a spread that's just literally on the parent type as always valid:
6162
// by spec text, it shouldn't be, but graphql-{js,java,go} and others all do this.
@@ -118,7 +119,7 @@ pub(crate) fn validate_inline_fragment(
118119
document: &ExecutableDocument,
119120
against_type: Option<(&crate::Schema, &ast::NamedType)>,
120121
inline: &Node<executable::InlineFragment>,
121-
context: OperationValidationContext<'_>,
122+
context: &mut OperationValidationContext<'_>,
122123
) {
123124
super::directive::validate_directives(
124125
diagnostics,
@@ -171,7 +172,7 @@ pub(crate) fn validate_fragment_spread(
171172
document: &ExecutableDocument,
172173
against_type: Option<(&crate::Schema, &NamedType)>,
173174
spread: &Node<executable::FragmentSpread>,
174-
context: OperationValidationContext<'_>,
175+
context: &mut OperationValidationContext<'_>,
175176
) {
176177
super::directive::validate_directives(
177178
diagnostics,
@@ -194,7 +195,12 @@ pub(crate) fn validate_fragment_spread(
194195
context,
195196
);
196197
}
197-
validate_fragment_definition(diagnostics, document, def, context);
198+
let new = context
199+
.validated_fragments
200+
.insert(spread.fragment_name.clone());
201+
if new {
202+
validate_fragment_definition(diagnostics, document, def, context);
203+
}
198204
}
199205
None => {
200206
diagnostics.push(
@@ -211,7 +217,7 @@ pub(crate) fn validate_fragment_definition(
211217
diagnostics: &mut DiagnosticList,
212218
document: &ExecutableDocument,
213219
fragment: &Node<executable::Fragment>,
214-
context: OperationValidationContext<'_>,
220+
context: &mut OperationValidationContext<'_>,
215221
) {
216222
super::directive::validate_directives(
217223
diagnostics,
@@ -265,35 +271,43 @@ pub(crate) fn validate_fragment_cycles(
265271
) {
266272
/// If a fragment spread is recursive, returns a vec containing the spread that refers back to
267273
/// the original fragment, and a trace of each fragment spread back to the original fragment.
268-
fn detect_fragment_cycles(
269-
document: &ExecutableDocument,
270-
selection_set: &executable::SelectionSet,
271-
visited: &mut RecursionGuard<'_>,
274+
fn detect_fragment_cycles<'doc>(
275+
document: &'doc ExecutableDocument,
276+
selection_set: &'doc executable::SelectionSet,
277+
path_from_root: &mut RecursionGuard<'_>,
278+
seen: &mut HashSet<&'doc Name>,
272279
) -> Result<(), CycleError<executable::FragmentSpread>> {
273280
for selection in &selection_set.selections {
274281
match selection {
275282
executable::Selection::FragmentSpread(spread) => {
276-
if visited.contains(&spread.fragment_name) {
277-
if visited.first() == Some(&spread.fragment_name) {
283+
if path_from_root.contains(&spread.fragment_name) {
284+
if path_from_root.first() == Some(&spread.fragment_name) {
278285
return Err(CycleError::Recursed(vec![spread.clone()]));
279286
}
280287
continue;
281288
}
282289

290+
let new = seen.insert(&spread.fragment_name);
291+
if !new {
292+
// We already recursively traversed that fragment and didn’t find a cycle then
293+
continue;
294+
}
295+
283296
if let Some(fragment) = document.fragments.get(&spread.fragment_name) {
284297
detect_fragment_cycles(
285298
document,
286299
&fragment.selection_set,
287-
&mut visited.push(&fragment.name)?,
300+
&mut path_from_root.push(&fragment.name)?,
301+
seen,
288302
)
289303
.map_err(|error| error.trace(spread))?;
290304
}
291305
}
292306
executable::Selection::InlineFragment(inline) => {
293-
detect_fragment_cycles(document, &inline.selection_set, visited)?;
307+
detect_fragment_cycles(document, &inline.selection_set, path_from_root, seen)?;
294308
}
295309
executable::Selection::Field(field) => {
296-
detect_fragment_cycles(document, &field.selection_set, visited)?;
310+
detect_fragment_cycles(document, &field.selection_set, path_from_root, seen)?;
297311
}
298312
}
299313
}
@@ -303,7 +317,12 @@ pub(crate) fn validate_fragment_cycles(
303317

304318
let mut visited = RecursionStack::with_root(def.name.clone()).with_limit(100);
305319

306-
match detect_fragment_cycles(document, &def.selection_set, &mut visited.guard()) {
320+
match detect_fragment_cycles(
321+
document,
322+
&def.selection_set,
323+
&mut visited.guard(),
324+
&mut HashSet::default(),
325+
) {
307326
Ok(_) => {}
308327
Err(CycleError::Recursed(trace)) => {
309328
let head_location = SourceSpan::recompose(def.location(), def.name.location());

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub(crate) mod value;
2424
pub(crate) mod variable;
2525

2626
use crate::collections::HashMap;
27+
use crate::collections::HashSet;
2728
use crate::collections::IndexSet;
2829
use crate::diagnostic::CliReport;
2930
use crate::diagnostic::Diagnostic;
@@ -160,18 +161,20 @@ impl<'a> ExecutableValidationContext<'a> {
160161
OperationValidationContext {
161162
executable: self,
162163
variables,
164+
validated_fragments: HashSet::default(),
163165
}
164166
}
165167
}
166168

167169
/// Shared context when validating things inside an operation.
168-
#[derive(Debug, Clone, Copy)]
170+
#[derive(Debug)]
169171
pub(crate) struct OperationValidationContext<'a> {
170172
/// Parent context. Using a reference so the `OnceLock` is shared between all operation
171173
/// contexts.
172174
executable: &'a ExecutableValidationContext<'a>,
173175
/// The variables defined for this operation.
174-
pub variables: &'a [Node<VariableDefinition>],
176+
pub(crate) variables: &'a [Node<VariableDefinition>],
177+
pub(crate) validated_fragments: HashSet<Name>,
175178
}
176179

177180
impl<'a> OperationValidationContext<'a> {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ pub(crate) fn validate_operation(
8282
document,
8383
against_type,
8484
&operation.selection_set,
85-
context.operation_context(&operation.variables),
85+
&mut context.operation_context(&operation.variables),
8686
);
8787
}
8888

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,7 @@ pub(crate) fn validate_selection_set(
581581
document: &ExecutableDocument,
582582
against_type: Option<(&crate::Schema, &NamedType)>,
583583
selection_set: &SelectionSet,
584-
context: OperationValidationContext<'_>,
584+
context: &mut OperationValidationContext<'_>,
585585
) {
586586
for selection in &selection_set.selections {
587587
match selection {

0 commit comments

Comments
 (0)