use indexmap::IndexMap;
use std::sync::Arc;
use crate::{
diagnostics::{ApolloDiagnostic, DiagnosticData, Label},
hir,
validation::ValidationDatabase,
};
use super::operation::OperationValidationConfig;
pub(crate) fn same_response_shape(
db: &dyn ValidationDatabase,
field_a: Arc<hir::Field>,
field_b: Arc<hir::Field>,
) -> Result<(), ApolloDiagnostic> {
let Some(full_type_a) = field_a.ty(db.upcast()) else {
return Ok(()); };
let Some(full_type_b) = field_b.ty(db.upcast()) else {
return Ok(()); };
let mut type_a = &full_type_a;
let mut type_b = &full_type_b;
let mismatching_type_diagnostic = || {
ApolloDiagnostic::new(
db,
field_b.loc().into(),
DiagnosticData::ConflictingField {
field: field_a.name().to_string(),
original_selection: field_a.loc().into(),
redefined_selection: field_b.loc().into(),
},
)
.label(Label::new(
field_a.loc(),
format!(
"`{}` has type `{}` here",
field_a.response_name(),
full_type_a
),
))
.label(Label::new(
field_b.loc(),
format!("but the same field name has type `{full_type_b}` here"),
))
};
while !type_a.is_named() || !type_b.is_named() {
(type_a, type_b) = match (type_a, type_b) {
(hir::Type::NonNull { ty: type_a, .. }, hir::Type::NonNull { ty: type_b, .. }) => {
(type_a.as_ref(), type_b.as_ref())
}
(hir::Type::NonNull { .. }, _) | (_, hir::Type::NonNull { .. }) => {
return Err(mismatching_type_diagnostic())
}
(type_a, type_b) => (type_a, type_b),
};
(type_a, type_b) = match (type_a, type_b) {
(hir::Type::List { ty: type_a, .. }, hir::Type::List { ty: type_b, .. }) => {
(type_a.as_ref(), type_b.as_ref())
}
(hir::Type::List { .. }, _) | (_, hir::Type::List { .. }) => {
return Err(mismatching_type_diagnostic())
}
(type_a, type_b) => (type_a, type_b),
};
}
let (Some(def_a), Some(def_b)) = (type_a.type_def(db.upcast()), type_b.type_def(db.upcast()))
else {
return Ok(()); };
match (def_a, def_b) {
(
def_a @ (hir::TypeDefinition::ScalarTypeDefinition(_)
| hir::TypeDefinition::EnumTypeDefinition(_)),
def_b @ (hir::TypeDefinition::ScalarTypeDefinition(_)
| hir::TypeDefinition::EnumTypeDefinition(_)),
) => {
if def_a == def_b {
Ok(())
} else {
Err(mismatching_type_diagnostic())
}
}
(def_a, def_b) if def_a.is_composite_definition() && def_b.is_composite_definition() => {
let merged_set = field_a.selection_set().concat(field_b.selection_set());
let fields = db.flattened_operation_fields(merged_set);
let grouped_by_name = group_fields_by_name(fields);
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 {
db.same_response_shape(Arc::clone(subfield_a), Arc::clone(subfield_b))?;
}
}
Ok(())
}
(_, _) => Ok(()),
}
}
fn group_fields_by_name(fields: Vec<Arc<hir::Field>>) -> IndexMap<String, Vec<Arc<hir::Field>>> {
let mut map = IndexMap::<String, Vec<Arc<hir::Field>>>::new();
for field in fields {
match map.entry(field.response_name().to_string()) {
indexmap::map::Entry::Occupied(mut entry) => {
entry.get_mut().push(field);
}
indexmap::map::Entry::Vacant(entry) => {
entry.insert(vec![field]);
}
}
}
map
}
fn identical_arguments(
db: &dyn ValidationDatabase,
field_a: &hir::Field,
field_b: &hir::Field,
) -> Result<(), ApolloDiagnostic> {
let args_a = field_a.arguments();
let args_b = field_b.arguments();
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,
field_b.loc().into(),
DiagnosticData::ConflictingField {
field: field_a.name().to_string(),
original_selection: field_a.loc().into(),
redefined_selection: field_b.loc().into(),
},
)
.label(Label::new(arg.loc(), format!("field `{}` is selected with argument `{}` here", field_a.name(), arg.name())))
.label(Label::new(field_b.loc(), 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.is_same_value(&arg.value) {
return Err(
ApolloDiagnostic::new(
db,
field_b.loc().into(),
DiagnosticData::ConflictingField {
field: field_a.name().to_string(),
original_selection: field_a.loc().into(),
redefined_selection: field_b.loc().into(),
},
)
.label(Label::new(arg.loc(), format!("field `{}` provides one argument value here", field_a.name())))
.label(Label::new(other_arg.loc(), "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,
field_b.loc().into(),
DiagnosticData::ConflictingField {
field: field_a.name().to_string(),
original_selection: field_a.loc().into(),
redefined_selection: field_b.loc().into(),
},
)
.label(Label::new(arg.loc(), format!("field `{}` is selected with argument `{}` here", field_b.name(), arg.name())))
.label(Label::new(field_a.loc(), 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,
selection_set: hir::SelectionSet,
) -> Result<(), Vec<ApolloDiagnostic>> {
let fields = db.flattened_operation_fields(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 Some(parent_a) = field_a.parent_type(db.upcast()) else {
continue; };
for field_b in rest {
if let Err(diagnostic) =
db.same_response_shape(Arc::clone(field_a), Arc::clone(field_b))
{
diagnostics.push(diagnostic);
continue;
}
let Some(parent_b) = field_b.parent_type(db.upcast()) else {
continue; };
if parent_a == parent_b {
if field_a.name() != field_b.name() {
diagnostics.push(
ApolloDiagnostic::new(
db,
field_b.loc().into(),
DiagnosticData::ConflictingField {
field: field_b.name().to_string(),
original_selection: field_a.loc().into(),
redefined_selection: field_b.loc().into(),
},
)
.label(Label::new(
field_a.loc(),
format!(
"field `{}` is selected from field `{}` here",
field_a.response_name(),
field_a.name()
),
))
.label(Label::new(
field_b.loc(),
format!(
"but the same field `{}` is also selected from field `{}` here",
field_b.response_name(),
field_b.name()
),
))
.help("Alias is already used for a different field"),
);
continue;
}
if let Err(diagnostic) = identical_arguments(db, field_a, field_b) {
diagnostics.push(diagnostic);
continue;
}
let merged_set = field_a.selection_set().concat(field_b.selection_set());
if let Err(sub_diagnostics) = db.fields_in_set_can_merge(merged_set) {
diagnostics.extend(sub_diagnostics);
continue;
}
}
}
}
if diagnostics.is_empty() {
Ok(())
} else {
Err(diagnostics)
}
}
pub fn validate_selection(
db: &dyn ValidationDatabase,
selection: Arc<Vec<hir::Selection>>,
context: OperationValidationConfig,
) -> Vec<ApolloDiagnostic> {
let mut diagnostics = Vec::new();
for sel in selection.iter() {
match sel {
hir::Selection::Field(field) => {
diagnostics.extend(db.validate_field(field.clone(), context.clone()));
}
hir::Selection::FragmentSpread(spread) => {
diagnostics
.extend(db.validate_fragment_spread(Arc::clone(spread), context.clone()));
}
hir::Selection::InlineFragment(inline) => {
diagnostics
.extend(db.validate_inline_fragment(Arc::clone(inline), context.clone()));
}
}
}
diagnostics
}
pub fn validate_selection_set(
db: &dyn ValidationDatabase,
selection_set: hir::SelectionSet,
context: OperationValidationConfig,
) -> Vec<ApolloDiagnostic> {
let mut diagnostics = Vec::new();
if let Err(diagnostic) = db.fields_in_set_can_merge(selection_set.clone()) {
diagnostics.extend(diagnostic);
}
diagnostics.extend(db.validate_selection(selection_set.selection, context));
diagnostics
}