Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions crates/apollo-compiler/src/executable/validation.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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(
Expand Down
71 changes: 32 additions & 39 deletions crates/apollo-compiler/src/validation/fragment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -376,49 +378,40 @@ pub(crate) fn validate_fragment_type_condition(
}
}

pub(crate) fn validate_fragment_used(
diagnostics: &mut DiagnosticList,
fn collect_used_fragments(
document: &ExecutableDocument,
fragment: &Node<executable::Fragment>,
) {
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(),
},
)
) -> Result<HashSet<&Name>, 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(),
},
)
}
}
}
54 changes: 54 additions & 0 deletions crates/apollo-compiler/src/validation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This struct really is just an integer with extra steps, but I wanted to mimick the pattern that we have with RecursionStack in the other recursion-limited functions.

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()` to recursive calls. When a guard is dropped,
/// its value is decremented.
struct DepthGuard<'a>(&'a mut DepthCounter);

impl DepthGuard<'_> {
/// Mark that we are recursing. If we reached the limit, return an error.
fn increment(&mut self) -> Result<DepthGuard<'_>, 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 {
Expand Down
26 changes: 24 additions & 2 deletions crates/apollo-compiler/src/validation/operation.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {
Expand All @@ -45,19 +48,38 @@ 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(
Expand Down
35 changes: 31 additions & 4 deletions crates/apollo-compiler/src/validation/variable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -88,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),
Expand All @@ -97,13 +99,20 @@ 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);
Expand All @@ -118,19 +127,37 @@ 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<Item = &Name> + '_ {
Expand Down
Loading