use std::collections::{hash_map::Entry, HashMap};
use crate::diagnostics::{ApolloDiagnostic, DiagnosticData, Label};
use crate::validation::{FileId, ValidationDatabase};
use crate::{ast, schema, Node};
use super::operation::OperationValidationConfig;
#[derive(Debug, Clone, Copy)]
pub(crate) struct FieldAgainstType<'a> {
pub against_type: &'a ast::NamedType,
pub field: &'a Node<ast::Field>,
}
pub(crate) fn operation_fields<'a>(
named_fragments: &'a HashMap<ast::Name, Node<ast::FragmentDefinition>>,
against_type: &'a ast::NamedType,
selections: &'a [ast::Selection],
) -> Vec<FieldAgainstType<'a>> {
fn inner<'a>(
named_fragments: &'a HashMap<ast::Name, Node<ast::FragmentDefinition>>,
seen: &mut std::collections::HashSet<ast::Name>,
against_type: &'a ast::NamedType,
selections: &'a [ast::Selection],
) -> Vec<FieldAgainstType<'a>> {
selections
.iter()
.flat_map(|selection| match selection {
ast::Selection::Field(field) => vec![FieldAgainstType {
against_type,
field,
}],
ast::Selection::InlineFragment(inline) => inner(
named_fragments,
seen,
inline.type_condition.as_ref().unwrap_or(against_type),
&inline.selection_set,
),
ast::Selection::FragmentSpread(spread) => {
if seen.contains(&spread.fragment_name) {
return vec![];
}
seen.insert(spread.fragment_name.clone());
named_fragments
.get(&spread.fragment_name)
.map(|fragment| {
inner(
named_fragments,
seen,
&fragment.type_condition,
&fragment.selection_set,
)
})
.unwrap_or_default()
}
})
.collect()
}
inner(
named_fragments,
&mut Default::default(),
against_type,
selections,
)
}
pub(crate) fn same_response_shape(
db: &dyn ValidationDatabase,
file_id: FileId,
field_a: FieldAgainstType<'_>,
field_b: FieldAgainstType<'_>,
) -> Result<(), ApolloDiagnostic> {
let schema = db.schema();
let Ok(full_type_a) = schema.type_field(field_a.against_type, &field_a.field.name) else {
return Ok(()); };
let mut type_a = &full_type_a.ty;
let Ok(full_type_b) = schema.type_field(field_b.against_type, &field_b.field.name) else {
return Ok(()); };
let mut type_b = &full_type_b.ty;
let mismatching_type_diagnostic = || {
ApolloDiagnostic::new(
db,
field_b.field.location(),
DiagnosticData::ConflictingField {
field: field_a.field.name.to_string(),
original_selection: (field_a.field.location()),
redefined_selection: (field_b.field.location()),
},
)
.label(Label::new(
field_a.field.location(),
format!(
"`{}` has type `{}` here",
field_a.field.response_name(),
full_type_a.ty,
),
))
.label(Label::new(
field_b.field.location(),
format!("but the same field name has type `{}` here", full_type_b.ty),
))
};
while !type_a.is_named() || !type_b.is_named() {
(type_a, type_b) = match (type_a, type_b) {
(ast::Type::List(type_a), ast::Type::List(type_b))
| (ast::Type::NonNullList(type_a), ast::Type::NonNullList(type_b)) => {
(type_a.as_ref(), type_b.as_ref())
}
(ast::Type::List(_), _)
| (_, ast::Type::List(_))
| (ast::Type::NonNullList(_), _)
| (_, ast::Type::NonNullList(_)) => return Err(mismatching_type_diagnostic()),
(type_a, type_b) => (type_a, type_b),
};
}
let (type_a, type_b) = match (type_a, type_b) {
(ast::Type::NonNullNamed(a), ast::Type::NonNullNamed(b)) => (a, b),
(ast::Type::Named(a), ast::Type::Named(b)) => (a, b),
_ => return Err(mismatching_type_diagnostic()),
};
let (Some(def_a), Some(def_b)) = (schema.types.get(type_a), schema.types.get(type_b)) else {
return Ok(()); };
fn is_composite(ty: &schema::ExtendedType) -> bool {
type T = schema::ExtendedType;
matches!(ty, T::Object(_) | T::Interface(_) | T::Union(_))
}
match (def_a, def_b) {
(
def_a @ (schema::ExtendedType::Scalar(_) | schema::ExtendedType::Enum(_)),
def_b @ (schema::ExtendedType::Scalar(_) | schema::ExtendedType::Enum(_)),
) => {
if def_a == def_b {
Ok(())
} else {
Err(mismatching_type_diagnostic())
}
}
(def_a, def_b) if is_composite(def_a) && is_composite(def_b) => {
let Ok(subfield_a_type) = schema.type_field(field_a.against_type, &field_a.field.name)
else {
return Ok(());
};
let Ok(subfield_b_type) = schema.type_field(field_b.against_type, &field_b.field.name)
else {
return Ok(());
};
let named_fragments = db.ast_named_fragments(file_id);
let mut merged_set = operation_fields(
&named_fragments,
&subfield_a_type.name,
&field_a.field.selection_set,
);
merged_set.extend(operation_fields(
&named_fragments,
&subfield_b_type.name,
&field_b.field.selection_set,
));
let grouped_by_name = group_fields_by_name(merged_set);
for (_, fields_for_name) in grouped_by_name {
let Some((subfield_a, rest)) = fields_for_name.split_first() else {
continue;
};
for subfield_b in rest {
same_response_shape(db, file_id, *subfield_a, *subfield_b)?;
}
}
Ok(())
}
(_, _) => Ok(()),
}
}
fn group_fields_by_name(
fields: Vec<FieldAgainstType<'_>>,
) -> HashMap<ast::Name, Vec<FieldAgainstType<'_>>> {
let mut map = HashMap::<ast::Name, Vec<FieldAgainstType<'_>>>::new();
for field in fields {
match map.entry(field.field.response_name().clone()) {
Entry::Occupied(mut entry) => {
entry.get_mut().push(field);
}
Entry::Vacant(entry) => {
entry.insert(vec![field]);
}
}
}
map
}
fn identical_arguments(
db: &dyn ValidationDatabase,
field_a: &Node<ast::Field>,
field_b: &Node<ast::Field>,
) -> Result<(), ApolloDiagnostic> {
let args_a = &field_a.arguments;
let args_b = &field_b.arguments;
let loc_a = field_a.location();
let loc_b = field_b.location();
for arg in args_a {
let Some(other_arg) = args_b.iter().find(|other_arg| other_arg.name == arg.name) else {
return Err(
ApolloDiagnostic::new(
db,
loc_b,
DiagnosticData::ConflictingField {
field: field_a.name.to_string(),
original_selection: loc_a,
redefined_selection: loc_b,
},
)
.label(Label::new(arg.location(), format!("field `{}` is selected with argument `{}` here", field_a.name, arg.name)))
.label(Label::new(loc_b, format!("but argument `{}` is not provided here", arg.name)))
.help("Fields with the same response name must provide the same set of arguments. Consider adding an alias if you need to select fields with different arguments.")
);
};
if other_arg.value != arg.value {
return Err(
ApolloDiagnostic::new(
db,
loc_b,
DiagnosticData::ConflictingField {
field: field_a.name.to_string(),
original_selection: loc_a,
redefined_selection: loc_b,
},
)
.label(Label::new(arg.location(), format!("field `{}` provides one argument value here", field_a.name)))
.label(Label::new(other_arg.location(), "but a different value here"))
.help("Fields with the same response name must provide the same set of arguments. Consider adding an alias if you need to select fields with different arguments.")
);
}
}
for arg in args_b {
if !args_a.iter().any(|other_arg| other_arg.name == arg.name) {
return Err(
ApolloDiagnostic::new(
db,
loc_b,
DiagnosticData::ConflictingField {
field: field_a.name.to_string(),
original_selection: loc_a,
redefined_selection: loc_b,
},
)
.label(Label::new(arg.location(), format!("field `{}` is selected with argument `{}` here", field_b.name, arg.name)))
.label(Label::new(loc_a, format!("but argument `{}` is not provided here", arg.name)))
.help("Fields with the same response name must provide the same set of arguments. Consider adding an alias if you need to select fields with different arguments.")
);
};
}
Ok(())
}
pub(crate) fn fields_in_set_can_merge(
db: &dyn ValidationDatabase,
file_id: FileId,
named_fragments: &HashMap<ast::Name, Node<ast::FragmentDefinition>>,
against_type: &ast::NamedType,
selection_set: &[ast::Selection],
) -> Result<(), Vec<ApolloDiagnostic>> {
let schema = db.schema();
let fields = operation_fields(named_fragments, against_type, selection_set);
let grouped_by_name = group_fields_by_name(fields);
let mut diagnostics = vec![];
for (_, fields_for_name) in grouped_by_name {
let Some((field_a, rest)) = fields_for_name.split_first() else {
continue; };
let Ok(parent_a) = schema.type_field(field_a.against_type, &field_a.field.name) else {
continue; };
for field_b in rest {
if let Err(diagnostic) = same_response_shape(db, file_id, *field_a, *field_b) {
diagnostics.push(diagnostic);
continue;
}
if field_a.against_type == field_b.against_type {
if field_a.field.name != field_b.field.name {
diagnostics.push(
ApolloDiagnostic::new(
db,
field_b.field.location(),
DiagnosticData::ConflictingField {
field: field_b.field.name.to_string(),
original_selection: (field_a.field.location()),
redefined_selection: (field_b.field.location()),
},
)
.label(Label::new(
field_a.field.location(),
format!(
"field `{}` is selected from field `{}` here",
field_a.field.response_name(),
field_a.field.name
),
))
.label(Label::new(
field_b.field.location(),
format!(
"but the same field `{}` is also selected from field `{}` here",
field_b.field.response_name(),
field_b.field.name
),
))
.help("Alias is already used for a different field"),
);
continue;
}
if let Err(diagnostic) = identical_arguments(db, field_a.field, field_b.field) {
diagnostics.push(diagnostic);
continue;
}
let mut merged_set = field_a.field.selection_set.clone();
merged_set.extend_from_slice(&field_b.field.selection_set);
if let Err(sub_diagnostics) = fields_in_set_can_merge(
db,
file_id,
named_fragments,
parent_a.ty.inner_named_type(),
&merged_set,
) {
diagnostics.extend(sub_diagnostics);
continue;
}
}
}
}
if diagnostics.is_empty() {
Ok(())
} else {
Err(diagnostics)
}
}
pub(crate) fn validate_selection_set(
db: &dyn ValidationDatabase,
file_id: FileId,
against_type: Option<&ast::NamedType>,
selection_set: &[ast::Selection],
context: OperationValidationConfig<'_>,
) -> Vec<ApolloDiagnostic> {
let mut diagnostics = vec![];
let named_fragments = Some(db.ast_named_fragments(file_id));
if let (Some(named_fragments), Some(against_type)) = (named_fragments, against_type) {
if let Err(diagnostic) =
fields_in_set_can_merge(db, file_id, &named_fragments, against_type, selection_set)
{
diagnostics.extend(diagnostic);
}
}
diagnostics.extend(validate_selections(
db,
file_id,
against_type,
selection_set,
context,
));
diagnostics
}
pub(crate) fn validate_selections(
db: &dyn ValidationDatabase,
file_id: FileId,
against_type: Option<&ast::NamedType>,
selection_set: &[ast::Selection],
context: OperationValidationConfig<'_>,
) -> Vec<ApolloDiagnostic> {
let mut diagnostics = vec![];
for selection in selection_set {
match selection {
ast::Selection::Field(field) => diagnostics.extend(super::field::validate_field(
db,
file_id,
against_type,
field.clone(),
context.clone(),
)),
ast::Selection::FragmentSpread(fragment) => {
diagnostics.extend(super::fragment::validate_fragment_spread(
db,
file_id,
against_type,
fragment.clone(),
context.clone(),
))
}
ast::Selection::InlineFragment(inline) => {
diagnostics.extend(super::fragment::validate_inline_fragment(
db,
file_id,
against_type,
inline.clone(),
context.clone(),
))
}
}
}
diagnostics
}