From 733bc5ec0317026c7ec9f4d7bef76da5602e146d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Tue, 8 Apr 2025 13:35:45 +0200 Subject: [PATCH 1/9] Add failing tests for stack overflow --- .../tests/validation/recursion.rs | 120 +++++++++++++++++- 1 file changed, 119 insertions(+), 1 deletion(-) diff --git a/crates/apollo-compiler/tests/validation/recursion.rs b/crates/apollo-compiler/tests/validation/recursion.rs index ba67a989..db82e294 100644 --- a/crates/apollo-compiler/tests/validation/recursion.rs +++ b/crates/apollo-compiler/tests/validation/recursion.rs @@ -1,6 +1,8 @@ use apollo_compiler::parser::Parser; use expect_test::expect; +/// Build a chain of `size` fragments where each fragment recurses into a field and applies the +/// next fragment in the chain. fn build_fragment_chain(size: usize) -> String { let mut query = r#" query Introspection{ @@ -36,6 +38,39 @@ fn build_fragment_chain(size: usize) -> String { query } +/// Build a chain of `size` fragments where each fragment applies the next fragment in the chain, +/// without going into nested fields. +fn build_flat_fragment_chain(size: usize) -> String { + let mut query = r#" + query Introspection{ + __schema { + types { + ...typeFragment1 + } + } + } + "# + .to_string(); + + for i in 1..size { + query.push_str(&format!( + " + fragment typeFragment{i} on __Type {{ + ...typeFragment{} + }}", + i + 1 + )); + } + query.push_str(&format!( + " + fragment typeFragment{size} on __Type {{ + name + }}" + )); + + query +} + fn build_directive_chain(size: usize) -> String { let mut schema = r#" type Query { @@ -103,7 +138,7 @@ fn build_nested_selection(depth: usize) -> String { } #[test] -fn long_fragment_chains_do_not_overflow_stack() { +fn long_nested_fragment_chains_do_not_overflow_stack() { // Build a query that applies 1K fragments // Validating it would take a lot of recursion and blow the stack let query = build_fragment_chain(1_000); @@ -119,6 +154,7 @@ fn long_fragment_chains_do_not_overflow_stack() { .expect_err("must have recursion errors"); let expected = expect_test::expect![[r#" + Error: too much recursion Error: too much recursion Error: `typeFragment1` contains too much nesting ╭─[ overflow.graphql:11:11 ] @@ -131,6 +167,88 @@ fn long_fragment_chains_do_not_overflow_stack() { expected.assert_eq(&errors.to_string()); } +#[test] +fn long_flat_fragment_chains_do_not_overflow_stack() { + // Build a query that applies 1K fragments + // Validating it would take a lot of recursion and blow the stack + let query = build_flat_fragment_chain(1_000); + + let errors = Parser::new() + .parse_mixed_validate( + format!( + "type Query {{ a: Int }} + {query}" + ), + "overflow.graphql", + ) + .expect_err("must have recursion errors"); + + let expected = expect_test::expect![[r#" + Error: too much recursion + Error: `typeFragment1` contains too much nesting + ╭─[ overflow.graphql:11:11 ] + │ + 11 │ fragment typeFragment1 on __Type { + │ ───────────┬────────── + │ ╰──────────── references a very long chain of fragments in its definition + ────╯ + "#]]; + expected.assert_eq(&errors.to_string()); +} + +#[test] +fn long_flat_fragment_chains_do_not_overflow_stack_in_subscriptions() { + // Build a subscription that applies 1K fragments + // Validating it would take a lot of recursion and blow the stack + let size = 10_000; + let mut query = r#" + subscription { + ...subscriptionFragment1 + } + "# + .to_string(); + + for i in 1..size { + query.push_str(&format!( + " + fragment subscriptionFragment{i} on Subscription {{ + ...subscriptionFragment{} + }}", + i + 1 + )); + } + query.push_str(&format!( + " + fragment subscriptionFragment{size} on Subscription {{ + a + }}" + )); + + let errors = Parser::new() + .parse_mixed_validate( + format!( + " + type Query {{ notUsed: Int }} + type Subscription {{ a: Int }} + {query}" + ), + "overflow.graphql", + ) + .expect_err("must have recursion errors"); + + let expected = expect_test::expect![[r#" + Error: too much recursion + Error: `subscriptionFragment1` contains too much nesting + ╭─[ overflow.graphql:9:11 ] + │ + 9 │ fragment subscriptionFragment1 on Subscription { + │ ───────────────┬────────────── + │ ╰──────────────── references a very long chain of fragments in its definition + ───╯ + "#]]; + expected.assert_eq(&errors.to_string()); +} + #[test] fn not_long_enough_fragment_chain_applies_correctly() { // Stay just under the recursion limit From b76889736311b56b8f9513f98e1eede968574911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Tue, 8 Apr 2025 13:55:51 +0200 Subject: [PATCH 2/9] Add overflow protection to variable validation --- crates/apollo-compiler/src/validation/mod.rs | 54 +++++++++++++++++++ .../src/validation/variable.rs | 15 ++++-- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/crates/apollo-compiler/src/validation/mod.rs b/crates/apollo-compiler/src/validation/mod.rs index 78bb8789..e6c45be0 100644 --- a/crates/apollo-compiler/src/validation/mod.rs +++ b/crates/apollo-compiler/src/validation/mod.rs @@ -1089,6 +1089,60 @@ const DEFAULT_RECURSION_LIMIT: usize = 32; #[non_exhaustive] struct RecursionLimitError {} +/// Track recursion depth to prevent stack overflow. +#[derive(Debug)] +struct DepthCounter { + value: usize, + high: usize, + limit: usize, +} + +impl DepthCounter { + fn new() -> Self { + Self { + value: 0, + high: 0, + limit: DEFAULT_RECURSION_LIMIT, + } + } + + fn with_limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } + + /// Return the actual API for tracking recursive uses. + pub(crate) fn guard(&mut self) -> DepthGuard<'_> { + DepthGuard(self) + } +} + +/// Track call depth in a recursive function. +/// +/// Pass the result of `guard.increment(name)` to recursive calls. When a guard is dropped, +/// its value is decremented. +struct DepthGuard<'a>(&'a mut DepthCounter); + +impl DepthGuard<'_> { + /// Mark that we saw a name. If there are too many names, return an error. + fn increment(&mut self) -> Result, RecursionLimitError> { + self.0.value += 1; + self.0.high = self.0.high.max(self.0.value); + if self.0.value > self.0.limit { + Err(RecursionLimitError {}) + } else { + Ok(DepthGuard(self.0)) + } + } +} + +impl Drop for DepthGuard<'_> { + fn drop(&mut self) { + // This may already be 0 if it's the original `counter.guard()` result, but that's fine + self.0.value = self.0.value.saturating_sub(1); + } +} + /// Track used names in a recursive function. #[derive(Debug)] struct RecursionStack { diff --git a/crates/apollo-compiler/src/validation/variable.rs b/crates/apollo-compiler/src/validation/variable.rs index 53212c3d..023d2b20 100644 --- a/crates/apollo-compiler/src/validation/variable.rs +++ b/crates/apollo-compiler/src/validation/variable.rs @@ -4,6 +4,8 @@ use crate::collections::HashSet; use crate::executable; use crate::validation::diagnostics::DiagnosticData; use crate::validation::value::value_of_correct_type; +use crate::validation::DepthCounter; +use crate::validation::DepthGuard; use crate::validation::DiagnosticList; use crate::validation::RecursionLimitError; use crate::validation::SourceSpan; @@ -97,13 +99,14 @@ fn walk_selections_with_deduped_fragments<'doc>( document: &'doc ExecutableDocument, selection_set: &'doc executable::SelectionSet, seen: &mut HashSet<&'doc Name>, + mut guard: DepthGuard<'_>, f: &mut dyn FnMut(&'doc executable::Selection), ) -> Result<(), RecursionLimitError> { for selection in &selection_set.selections { f(selection); match selection { executable::Selection::Field(field) => { - walk_selections_inner(document, &field.selection_set, seen, f)?; + walk_selections_inner(document, &field.selection_set, seen, guard.increment()?, f)?; } executable::Selection::FragmentSpread(fragment) => { let new = seen.insert(&fragment.fragment_name); @@ -118,19 +121,25 @@ fn walk_selections_with_deduped_fragments<'doc>( document, &fragment_definition.selection_set, seen, + guard.increment()?, f, )?; } } executable::Selection::InlineFragment(fragment) => { - walk_selections_inner(document, &fragment.selection_set, seen, f)?; + walk_selections_inner(document, &fragment.selection_set, seen, guard.increment()?, f)?; } } } Ok(()) } - walk_selections_inner(document, selections, &mut HashSet::default(), &mut f) + // This has a much higher limit than comparable recursive walks, like the one in + // `validate_fragment_cycles`, despite doing similar work. This is because this limit + // was introduced later and should not break (reasonable) existing queries that are + // under that pre-existing limit. Luckily the existing limit was very conservative. + let mut depth = DepthCounter::new().with_limit(500); + walk_selections_inner(document, selections, &mut HashSet::default(), depth.guard(), &mut f) } fn variables_in_value(value: &ast::Value) -> impl Iterator + '_ { From b183cea0a9d90a69d2c8557360dbfa92b59da651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Wed, 9 Apr 2025 10:42:34 +0200 Subject: [PATCH 3/9] You need a lot to blow the stack here --- crates/apollo-compiler/tests/validation/recursion.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/apollo-compiler/tests/validation/recursion.rs b/crates/apollo-compiler/tests/validation/recursion.rs index db82e294..ad27a425 100644 --- a/crates/apollo-compiler/tests/validation/recursion.rs +++ b/crates/apollo-compiler/tests/validation/recursion.rs @@ -169,9 +169,9 @@ fn long_nested_fragment_chains_do_not_overflow_stack() { #[test] fn long_flat_fragment_chains_do_not_overflow_stack() { - // Build a query that applies 1K fragments + // Build a query that applies 10K fragments // Validating it would take a lot of recursion and blow the stack - let query = build_flat_fragment_chain(1_000); + let query = build_flat_fragment_chain(10_000); let errors = Parser::new() .parse_mixed_validate( @@ -198,7 +198,7 @@ fn long_flat_fragment_chains_do_not_overflow_stack() { #[test] fn long_flat_fragment_chains_do_not_overflow_stack_in_subscriptions() { - // Build a subscription that applies 1K fragments + // Build a subscription that applies 10K fragments // Validating it would take a lot of recursion and blow the stack let size = 10_000; let mut query = r#" From 594e009440b42a83f209ad213f9332718c286d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Wed, 9 Apr 2025 10:55:35 +0200 Subject: [PATCH 4/9] Lint --- .../src/validation/variable.rs | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/apollo-compiler/src/validation/variable.rs b/crates/apollo-compiler/src/validation/variable.rs index 023d2b20..2f0f79ed 100644 --- a/crates/apollo-compiler/src/validation/variable.rs +++ b/crates/apollo-compiler/src/validation/variable.rs @@ -106,7 +106,13 @@ fn walk_selections_with_deduped_fragments<'doc>( f(selection); match selection { executable::Selection::Field(field) => { - walk_selections_inner(document, &field.selection_set, seen, guard.increment()?, f)?; + walk_selections_inner( + document, + &field.selection_set, + seen, + guard.increment()?, + f, + )?; } executable::Selection::FragmentSpread(fragment) => { let new = seen.insert(&fragment.fragment_name); @@ -127,7 +133,13 @@ fn walk_selections_with_deduped_fragments<'doc>( } } executable::Selection::InlineFragment(fragment) => { - walk_selections_inner(document, &fragment.selection_set, seen, guard.increment()?, f)?; + walk_selections_inner( + document, + &fragment.selection_set, + seen, + guard.increment()?, + f, + )?; } } } @@ -139,7 +151,13 @@ fn walk_selections_with_deduped_fragments<'doc>( // was introduced later and should not break (reasonable) existing queries that are // under that pre-existing limit. Luckily the existing limit was very conservative. let mut depth = DepthCounter::new().with_limit(500); - walk_selections_inner(document, selections, &mut HashSet::default(), depth.guard(), &mut f) + walk_selections_inner( + document, + selections, + &mut HashSet::default(), + depth.guard(), + &mut f, + ) } fn variables_in_value(value: &ast::Value) -> impl Iterator + '_ { From 4bd2189af0ae66316a4607762c387a8518071fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Tue, 15 Apr 2025 11:02:11 +0200 Subject: [PATCH 5/9] Add overflow protection in subscription validation --- crates/apollo-compiler/src/validation/operation.rs | 14 ++++++++++++-- .../apollo-compiler/tests/validation/recursion.rs | 1 + 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/crates/apollo-compiler/src/validation/operation.rs b/crates/apollo-compiler/src/validation/operation.rs index a598e47c..66987e21 100644 --- a/crates/apollo-compiler/src/validation/operation.rs +++ b/crates/apollo-compiler/src/validation/operation.rs @@ -1,6 +1,8 @@ use crate::collections::HashSet; use crate::executable; use crate::validation::diagnostics::DiagnosticData; +use crate::validation::DepthCounter; +use crate::validation::DepthGuard; use crate::validation::DiagnosticList; use crate::validation::ExecutableValidationContext; use crate::validation::RecursionLimitError; @@ -23,6 +25,7 @@ fn walk_selections<'doc>( document: &'doc ExecutableDocument, selection_set: &'doc executable::SelectionSet, seen: &mut HashSet<&'doc Name>, + mut guard: DepthGuard<'_>, f: &mut dyn FnMut(&'doc executable::Selection), ) -> Result<(), RecursionLimitError> { for selection in &selection_set.selections { @@ -45,19 +48,26 @@ fn walk_selections<'doc>( document, &fragment_definition.selection_set, seen, + guard.increment()?, f, )?; } } executable::Selection::InlineFragment(fragment) => { - walk_selections_inner(document, &fragment.selection_set, seen, f)?; + walk_selections_inner(document, &fragment.selection_set, seen, guard.increment()?, f)?; } } } Ok(()) } - walk_selections_inner(document, selections, &mut HashSet::default(), &mut f) + // This has a much higher limit than comparable recursive walks, like the one in + // `validate_fragment_cycles`, despite doing similar work. This is because this limit + // was introduced later and should not break (reasonable) existing queries that are + // under that pre-existing limit. Luckily the existing limit was very conservative. + let mut depth = DepthCounter::new().with_limit(500); + + walk_selections_inner(document, selections, &mut HashSet::default(), depth.guard(), &mut f) } pub(crate) fn validate_subscription( diff --git a/crates/apollo-compiler/tests/validation/recursion.rs b/crates/apollo-compiler/tests/validation/recursion.rs index ad27a425..f88ea039 100644 --- a/crates/apollo-compiler/tests/validation/recursion.rs +++ b/crates/apollo-compiler/tests/validation/recursion.rs @@ -237,6 +237,7 @@ fn long_flat_fragment_chains_do_not_overflow_stack_in_subscriptions() { .expect_err("must have recursion errors"); let expected = expect_test::expect![[r#" + Error: too much recursion Error: too much recursion Error: `subscriptionFragment1` contains too much nesting ╭─[ overflow.graphql:9:11 ] From eea9ead97267d12c133238e4488a98c03959a831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Tue, 15 Apr 2025 11:34:57 +0200 Subject: [PATCH 6/9] Respect recursion limit in Fragment Is Used validation Also speed it up significantly by only walking the entire AST once, instead of doing it again for every fragment definition. --- .../src/executable/validation.rs | 6 +- .../src/validation/fragment.rs | 73 +++++++++---------- .../src/validation/operation.rs | 16 +++- .../src/validation/variable.rs | 2 +- 4 files changed, 50 insertions(+), 47 deletions(-) diff --git a/crates/apollo-compiler/src/executable/validation.rs b/crates/apollo-compiler/src/executable/validation.rs index 11e8fd09..f757b406 100644 --- a/crates/apollo-compiler/src/executable/validation.rs +++ b/crates/apollo-compiler/src/executable/validation.rs @@ -1,5 +1,5 @@ use super::FieldSet; -use crate::validation::fragment::validate_fragment_used; +use crate::validation::fragment::validate_fragments_used; use crate::validation::operation::validate_operation_definitions; use crate::validation::selection::FieldsInSetCanMerge; use crate::validation::DiagnosticList; @@ -44,9 +44,7 @@ pub(crate) fn validate_with_or_without_schema( ) { let context = ExecutableValidationContext::new(schema); validate_operation_definitions(errors, document, &context); - for def in document.fragments.values() { - validate_fragment_used(errors, document, def); - } + validate_fragments_used(errors, document); } pub(crate) fn validate_field_set( diff --git a/crates/apollo-compiler/src/validation/fragment.rs b/crates/apollo-compiler/src/validation/fragment.rs index f6e7117c..acbd18d0 100644 --- a/crates/apollo-compiler/src/validation/fragment.rs +++ b/crates/apollo-compiler/src/validation/fragment.rs @@ -7,10 +7,12 @@ use crate::executable; use crate::schema; use crate::schema::Implementers; use crate::validation::diagnostics::DiagnosticData; +use crate::validation::variable::walk_selections_with_deduped_fragments; use crate::validation::CycleError; use crate::validation::DiagnosticList; use crate::validation::OperationValidationContext; use crate::validation::RecursionGuard; +use crate::validation::RecursionLimitError; use crate::validation::RecursionStack; use crate::validation::SourceSpan; use crate::ExecutableDocument; @@ -376,49 +378,40 @@ pub(crate) fn validate_fragment_type_condition( } } -pub(crate) fn validate_fragment_used( - diagnostics: &mut DiagnosticList, - document: &ExecutableDocument, - fragment: &Node, -) { - let fragment_name = &fragment.name; - - let mut all_selections = document - .operations - .iter() - .map(|operation| &operation.selection_set) - .chain( - document - .fragments - .values() - .map(|fragment| &fragment.selection_set), - ) - .flat_map(|set| &set.selections); - - let is_used = all_selections.any(|sel| selection_uses_fragment(sel, fragment_name)); - - // Fragments must be used within the schema - // - // Returns Unused Fragment error. - if !is_used { - diagnostics.push( - fragment.location(), - DiagnosticData::UnusedFragment { - name: fragment_name.clone(), - }, - ) +fn collect_used_fragments<'doc>( + document: &'doc ExecutableDocument, +) -> Result, RecursionLimitError> { + let mut names = HashSet::default(); + for operation in document.operations.iter() { + walk_selections_with_deduped_fragments(document, &operation.selection_set, |selection| { + if let executable::Selection::FragmentSpread(spread) = selection { + names.insert(&spread.fragment_name); + } + })?; } + Ok(names) } -fn selection_uses_fragment(sel: &executable::Selection, name: &str) -> bool { - let sub_selections = match sel { - executable::Selection::FragmentSpread(fragment) => return fragment.fragment_name == name, - executable::Selection::Field(field) => &field.selection_set, - executable::Selection::InlineFragment(inline) => &inline.selection_set, +pub(crate) fn validate_fragments_used( + diagnostics: &mut DiagnosticList, + document: &ExecutableDocument, +) { + let Ok(used_fragments) = collect_used_fragments(document) else { + diagnostics.push(None, super::Details::RecursionLimitError); + return; }; - sub_selections - .selections - .iter() - .any(|sel| selection_uses_fragment(sel, name)) + for fragment in document.fragments.values() { + // Fragments must be used within the schema + // + // Returns Unused Fragment error. + if !used_fragments.contains(&fragment.name) { + diagnostics.push( + fragment.location(), + DiagnosticData::UnusedFragment { + name: fragment.name.clone(), + }, + ) + } + } } diff --git a/crates/apollo-compiler/src/validation/operation.rs b/crates/apollo-compiler/src/validation/operation.rs index 66987e21..42df8969 100644 --- a/crates/apollo-compiler/src/validation/operation.rs +++ b/crates/apollo-compiler/src/validation/operation.rs @@ -54,7 +54,13 @@ fn walk_selections<'doc>( } } executable::Selection::InlineFragment(fragment) => { - walk_selections_inner(document, &fragment.selection_set, seen, guard.increment()?, f)?; + walk_selections_inner( + document, + &fragment.selection_set, + seen, + guard.increment()?, + f, + )?; } } } @@ -67,7 +73,13 @@ fn walk_selections<'doc>( // under that pre-existing limit. Luckily the existing limit was very conservative. let mut depth = DepthCounter::new().with_limit(500); - walk_selections_inner(document, selections, &mut HashSet::default(), depth.guard(), &mut f) + walk_selections_inner( + document, + selections, + &mut HashSet::default(), + depth.guard(), + &mut f, + ) } pub(crate) fn validate_subscription( diff --git a/crates/apollo-compiler/src/validation/variable.rs b/crates/apollo-compiler/src/validation/variable.rs index 2f0f79ed..6d5d25cd 100644 --- a/crates/apollo-compiler/src/validation/variable.rs +++ b/crates/apollo-compiler/src/validation/variable.rs @@ -90,7 +90,7 @@ pub(crate) fn validate_variable_definitions( /// /// Named fragments are "deduplicated": only visited once even if spread multiple times *in /// different locations*. This is only appropriate for certain kinds of validations, so reuser beware. -fn walk_selections_with_deduped_fragments<'doc>( +pub(super) fn walk_selections_with_deduped_fragments<'doc>( document: &'doc ExecutableDocument, selections: &'doc executable::SelectionSet, mut f: impl FnMut(&'doc executable::Selection), From 53e57b70178d5abc3e1576d1235a93e2ee3bea82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Tue, 15 Apr 2025 11:37:31 +0200 Subject: [PATCH 7/9] lint --- crates/apollo-compiler/src/validation/fragment.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/apollo-compiler/src/validation/fragment.rs b/crates/apollo-compiler/src/validation/fragment.rs index acbd18d0..de628724 100644 --- a/crates/apollo-compiler/src/validation/fragment.rs +++ b/crates/apollo-compiler/src/validation/fragment.rs @@ -378,9 +378,9 @@ pub(crate) fn validate_fragment_type_condition( } } -fn collect_used_fragments<'doc>( - document: &'doc ExecutableDocument, -) -> Result, RecursionLimitError> { +fn collect_used_fragments( + document: &ExecutableDocument, +) -> Result, RecursionLimitError> { let mut names = HashSet::default(); for operation in document.operations.iter() { walk_selections_with_deduped_fragments(document, &operation.selection_set, |selection| { From c4b3853bd646f9adbc4d149cb7aceb8fd4e89d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Tue, 15 Apr 2025 11:38:20 +0200 Subject: [PATCH 8/9] comment tweaks --- crates/apollo-compiler/src/validation/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/apollo-compiler/src/validation/mod.rs b/crates/apollo-compiler/src/validation/mod.rs index e6c45be0..ab514a4e 100644 --- a/crates/apollo-compiler/src/validation/mod.rs +++ b/crates/apollo-compiler/src/validation/mod.rs @@ -1119,12 +1119,12 @@ impl DepthCounter { /// Track call depth in a recursive function. /// -/// Pass the result of `guard.increment(name)` to recursive calls. When a guard is dropped, +/// Pass the result of `guard.increment()` to recursive calls. When a guard is dropped, /// its value is decremented. struct DepthGuard<'a>(&'a mut DepthCounter); impl DepthGuard<'_> { - /// Mark that we saw a name. If there are too many names, return an error. + /// Mark that we are recursing. If we reached the limit, return an error. fn increment(&mut self) -> Result, RecursionLimitError> { self.0.value += 1; self.0.high = self.0.high.max(self.0.value); From 3f7eaf01a908946505b2f77751369d682cecc2ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Tue, 15 Apr 2025 11:42:40 +0200 Subject: [PATCH 9/9] Update snapshots --- crates/apollo-compiler/tests/validation/recursion.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/apollo-compiler/tests/validation/recursion.rs b/crates/apollo-compiler/tests/validation/recursion.rs index f88ea039..8ea63dfe 100644 --- a/crates/apollo-compiler/tests/validation/recursion.rs +++ b/crates/apollo-compiler/tests/validation/recursion.rs @@ -154,6 +154,7 @@ fn long_nested_fragment_chains_do_not_overflow_stack() { .expect_err("must have recursion errors"); let expected = expect_test::expect![[r#" + Error: too much recursion Error: too much recursion Error: too much recursion Error: `typeFragment1` contains too much nesting @@ -184,6 +185,7 @@ fn long_flat_fragment_chains_do_not_overflow_stack() { .expect_err("must have recursion errors"); let expected = expect_test::expect![[r#" + Error: too much recursion Error: too much recursion Error: `typeFragment1` contains too much nesting ╭─[ overflow.graphql:11:11 ] @@ -237,6 +239,7 @@ fn long_flat_fragment_chains_do_not_overflow_stack_in_subscriptions() { .expect_err("must have recursion errors"); let expected = expect_test::expect![[r#" + Error: too much recursion Error: too much recursion Error: too much recursion Error: `subscriptionFragment1` contains too much nesting