use crate::ast;
use crate::ast::Name;
use crate::ast::NamedType;
use crate::executable;
use crate::schema;
use crate::schema::Implementers;
use crate::validation::diagnostics::DiagnosticData;
use crate::validation::operation::OperationValidationConfig;
use crate::validation::DiagnosticList;
use crate::validation::{CycleError, NodeLocation, RecursionGuard, RecursionStack};
use crate::ExecutableDocument;
use crate::Node;
use std::borrow::Cow;
use std::collections::HashMap;
use std::collections::HashSet;
fn get_possible_types<'a>(
type_definition: &schema::ExtendedType,
implementers_map: &'a HashMap<Name, Implementers>,
) -> Cow<'a, HashSet<NamedType>> {
match type_definition {
schema::ExtendedType::Object(object) => {
let mut set = HashSet::new();
set.insert(object.name.clone());
Cow::Owned(set)
}
schema::ExtendedType::Interface(interface) => implementers_map
.get(&interface.name)
.map(|implementers| Cow::Borrowed(&implementers.objects))
.unwrap_or_default(),
schema::ExtendedType::Union(union_) => Cow::Owned(
union_
.members
.iter()
.map(|component| component.name.clone())
.collect(),
),
_ => Default::default(),
}
}
fn validate_fragment_spread_type(
diagnostics: &mut DiagnosticList,
schema: &crate::Schema,
document: &ExecutableDocument,
against_type: &NamedType,
type_condition: &NamedType,
selection: &executable::Selection,
) {
let Some(type_condition_definition) = schema.types.get(type_condition) else {
return;
};
let Some(against_type_definition) = schema.types.get(against_type) else {
return;
};
let implementers_map = schema.implementers_map();
let concrete_parent_types = get_possible_types(against_type_definition, &implementers_map);
let concrete_condition_types = get_possible_types(type_condition_definition, &implementers_map);
let mut applicable_types = concrete_parent_types.intersection(&concrete_condition_types);
if applicable_types.next().is_none() {
match selection {
executable::Selection::Field(_) => unreachable!(),
executable::Selection::FragmentSpread(spread) => {
let fragment_definition = document.fragments.get(&spread.fragment_name).unwrap();
diagnostics.push(
spread.location(),
DiagnosticData::InvalidFragmentSpread {
name: Some(spread.fragment_name.clone()),
type_name: against_type.clone(),
type_condition: type_condition.clone(),
fragment_location: fragment_definition.location(),
type_location: against_type_definition.location(),
},
)
}
executable::Selection::InlineFragment(inline) => diagnostics.push(
inline.location(),
DiagnosticData::InvalidFragmentSpread {
name: None,
type_name: against_type.clone(),
type_condition: type_condition.clone(),
fragment_location: inline.location(),
type_location: against_type_definition.location(),
},
),
};
}
}
pub(crate) fn validate_inline_fragment(
diagnostics: &mut DiagnosticList,
document: &ExecutableDocument,
against_type: Option<(&crate::Schema, &ast::NamedType)>,
inline: &Node<executable::InlineFragment>,
context: OperationValidationConfig<'_>,
) {
super::directive::validate_directives(
diagnostics,
context.schema,
inline.directives.iter(),
ast::DirectiveLocation::InlineFragment,
context.variables,
);
let previous = diagnostics.len();
if let Some(schema) = context.schema {
if let Some(t) = &inline.type_condition {
validate_fragment_type_condition(diagnostics, schema, None, t, inline.location())
}
}
let has_type_error = diagnostics.len() > previous;
if !has_type_error {
if let (Some((schema, against_type)), Some(type_condition)) =
(against_type, &inline.type_condition)
{
validate_fragment_spread_type(
diagnostics,
schema,
document,
against_type,
type_condition,
&executable::Selection::InlineFragment(inline.clone()),
);
}
super::selection::validate_selection_set(
diagnostics,
document,
if let (Some(schema), Some(ty)) = (&context.schema, &inline.type_condition) {
Some((schema, ty))
} else {
against_type
},
&inline.selection_set,
context,
);
}
}
pub(crate) fn validate_fragment_spread(
diagnostics: &mut DiagnosticList,
document: &ExecutableDocument,
against_type: Option<(&crate::Schema, &NamedType)>,
spread: &Node<executable::FragmentSpread>,
context: OperationValidationConfig<'_>,
) {
super::directive::validate_directives(
diagnostics,
context.schema,
spread.directives.iter(),
ast::DirectiveLocation::FragmentSpread,
context.variables,
);
match document.fragments.get(&spread.fragment_name) {
Some(def) => {
if let Some((schema, against_type)) = against_type {
validate_fragment_spread_type(
diagnostics,
schema,
document,
against_type,
def.type_condition(),
&executable::Selection::FragmentSpread(spread.clone()),
);
}
validate_fragment_definition(diagnostics, document, def, context);
}
None => {
diagnostics.push(
spread.location(),
DiagnosticData::UndefinedFragment {
name: spread.fragment_name.clone(),
},
);
}
}
}
pub(crate) fn validate_fragment_definition(
diagnostics: &mut DiagnosticList,
document: &ExecutableDocument,
fragment: &Node<executable::Fragment>,
context: OperationValidationConfig<'_>,
) {
super::directive::validate_directives(
diagnostics,
context.schema,
fragment.directives.iter(),
ast::DirectiveLocation::FragmentDefinition,
context.variables,
);
let previous = diagnostics.len();
if let Some(schema) = context.schema {
validate_fragment_type_condition(
diagnostics,
schema,
Some(fragment.name.clone()),
fragment.type_condition(),
fragment.location(),
);
}
let has_type_error = diagnostics.len() > previous;
let previous = diagnostics.len();
validate_fragment_cycles(diagnostics, document, fragment);
let has_cycles = diagnostics.len() > previous;
if !has_type_error && !has_cycles {
let type_condition = context.schema.and_then(|schema| {
schema
.types
.contains_key(fragment.type_condition())
.then_some((schema, fragment.type_condition()))
});
super::selection::validate_selection_set(
diagnostics,
document,
type_condition,
&fragment.selection_set,
context,
);
}
}
pub(crate) fn validate_fragment_cycles(
diagnostics: &mut DiagnosticList,
document: &ExecutableDocument,
def: &Node<executable::Fragment>,
) {
fn detect_fragment_cycles(
document: &ExecutableDocument,
selection_set: &executable::SelectionSet,
visited: &mut RecursionGuard<'_>,
) -> Result<(), CycleError<executable::FragmentSpread>> {
for selection in &selection_set.selections {
match selection {
executable::Selection::FragmentSpread(spread) => {
if visited.contains(&spread.fragment_name) {
if visited.first() == Some(&spread.fragment_name) {
return Err(CycleError::Recursed(vec![spread.clone()]));
}
continue;
}
if let Some(fragment) = document.fragments.get(&spread.fragment_name) {
detect_fragment_cycles(
document,
&fragment.selection_set,
&mut visited.push(&fragment.name)?,
)
.map_err(|error| error.trace(spread))?;
}
}
executable::Selection::InlineFragment(inline) => {
detect_fragment_cycles(document, &inline.selection_set, visited)?;
}
executable::Selection::Field(field) => {
detect_fragment_cycles(document, &field.selection_set, visited)?;
}
}
}
Ok(())
}
let mut visited = RecursionStack::with_root(def.name.clone()).with_limit(100);
match detect_fragment_cycles(document, &def.selection_set, &mut visited.guard()) {
Ok(_) => {}
Err(CycleError::Recursed(trace)) => {
let head_location = NodeLocation::recompose(def.location(), def.name.location());
diagnostics.push(
def.location(),
DiagnosticData::RecursiveFragmentDefinition {
head_location,
name: def.name.clone(),
trace,
},
);
}
Err(CycleError::Limit(_)) => {
let head_location = NodeLocation::recompose(def.location(), def.name.location());
diagnostics.push(
head_location,
DiagnosticData::DeeplyNestedType {
name: def.name.clone(),
describe_type: "fragment",
},
);
}
};
}
pub(crate) fn validate_fragment_type_condition(
diagnostics: &mut DiagnosticList,
schema: &crate::Schema,
fragment_name: Option<Name>,
type_cond: &NamedType,
fragment_location: Option<NodeLocation>,
) {
let type_def = schema.types.get(type_cond);
let is_composite = type_def
.as_ref()
.map_or(false, |ty| {
matches!(
ty,
schema::ExtendedType::Object(_)
| schema::ExtendedType::Interface(_)
| schema::ExtendedType::Union(_)
)
});
if !is_composite {
diagnostics.push(
fragment_location,
DiagnosticData::InvalidFragmentTarget {
name: fragment_name,
ty: type_cond.clone(),
},
);
}
}
pub(crate) fn validate_fragment_used(
diagnostics: &mut DiagnosticList,
document: &ExecutableDocument,
fragment: &Node<executable::Fragment>,
) {
let fragment_name = &fragment.name;
let mut all_selections = document
.all_operations()
.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));
if !is_used {
diagnostics.push(
fragment.location(),
DiagnosticData::UnusedFragment {
name: fragment_name.clone(),
},
)
}
}
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,
};
sub_selections
.selections
.iter()
.any(|sel| selection_uses_fragment(sel, name))
}