async-graphql/src/validation/rules/overlapping_fields_can_be_merged.rs

176 lines
5.4 KiB
Rust
Raw Normal View History

use std::collections::{HashMap, HashSet};
2020-10-15 06:38:10 +00:00
2022-04-19 04:25:11 +00:00
use crate::{
parser::types::{Field, Selection, SelectionSet},
validation::visitor::{Visitor, VisitorContext},
Positioned,
};
2020-03-12 09:11:02 +00:00
#[derive(Default)]
pub struct OverlappingFieldsCanBeMerged;
impl<'a> Visitor<'a> for OverlappingFieldsCanBeMerged {
fn enter_selection_set(
&mut self,
2020-03-22 08:45:59 +00:00
ctx: &mut VisitorContext<'a>,
2020-05-10 02:59:51 +00:00
selection_set: &'a Positioned<SelectionSet>,
2020-03-12 09:11:02 +00:00
) {
let mut find_conflicts = FindConflicts {
outputs: Default::default(),
visited: Default::default(),
2020-03-12 09:11:02 +00:00
ctx,
};
2022-06-25 02:51:59 +00:00
find_conflicts.find(None, selection_set);
2020-03-12 09:11:02 +00:00
}
}
struct FindConflicts<'a, 'ctx> {
2022-06-25 02:51:59 +00:00
outputs: HashMap<(Option<&'a str>, &'a str), &'a Positioned<Field>>,
visited: HashSet<&'a str>,
2020-03-22 08:45:59 +00:00
ctx: &'a mut VisitorContext<'ctx>,
2020-03-12 09:11:02 +00:00
}
impl<'a, 'ctx> FindConflicts<'a, 'ctx> {
2022-06-25 02:51:59 +00:00
pub fn find(&mut self, on_type: Option<&'a str>, selection_set: &'a Positioned<SelectionSet>) {
2020-09-06 05:38:31 +00:00
for selection in &selection_set.node.items {
match &selection.node {
2020-03-12 09:11:02 +00:00
Selection::Field(field) => {
let output_name = field
2020-09-06 05:38:31 +00:00
.node
2020-03-12 09:11:02 +00:00
.alias
.as_ref()
2020-09-06 05:38:31 +00:00
.map(|name| &name.node)
.unwrap_or_else(|| &field.node.name.node);
2022-06-25 02:51:59 +00:00
self.add_output(on_type, &output_name, field);
2020-03-12 09:11:02 +00:00
}
Selection::InlineFragment(inline_fragment) => {
2022-06-25 02:51:59 +00:00
let on_type = inline_fragment
.node
.type_condition
.as_ref()
.map(|cond| cond.node.on.node.as_str());
self.find(on_type, &inline_fragment.node.selection_set);
2020-03-12 09:11:02 +00:00
}
Selection::FragmentSpread(fragment_spread) => {
2020-09-06 06:16:36 +00:00
if let Some(fragment) =
self.ctx.fragment(&fragment_spread.node.fragment_name.node)
{
2022-06-25 02:51:59 +00:00
let on_type = Some(fragment.node.type_condition.node.on.node.as_str());
if !self
.visited
.insert(fragment_spread.node.fragment_name.node.as_str())
{
2022-04-19 04:25:11 +00:00
// To avoid recursing itself, this error is detected by the
// `NoFragmentCycles` validator.
continue;
}
2022-06-25 02:51:59 +00:00
self.find(on_type, &fragment.node.selection_set);
2020-03-12 09:11:02 +00:00
}
}
}
}
}
2022-06-25 02:51:59 +00:00
fn add_output(
&mut self,
on_type: Option<&'a str>,
name: &'a str,
field: &'a Positioned<Field>,
) {
if let Some(prev_field) = self.outputs.get(&(on_type, name)) {
2020-09-06 05:38:31 +00:00
if prev_field.node.name.node != field.node.name.node {
2020-03-12 09:11:02 +00:00
self.ctx.report_error(
2020-09-06 05:38:31 +00:00
vec![prev_field.pos, field.pos],
2020-03-12 09:11:02 +00:00
format!("Fields \"{}\" conflict because \"{}\" and \"{}\" are different fields. Use different aliases on the fields to fetch both if this was intentional.",
2020-09-06 05:38:31 +00:00
name, prev_field.node.name.node, field.node.name.node));
2020-03-12 09:11:02 +00:00
}
// check arguments
2020-09-06 05:38:31 +00:00
if prev_field.node.arguments.len() != field.node.arguments.len() {
2020-03-12 09:11:02 +00:00
self.ctx.report_error(
2020-09-06 05:38:31 +00:00
vec![prev_field.pos, field.pos],
2020-03-12 09:11:02 +00:00
format!("Fields \"{}\" conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional.", name));
}
2020-09-06 05:38:31 +00:00
for (name, value) in &prev_field.node.arguments {
match field.node.get_argument(&name.node) {
2020-03-12 09:11:02 +00:00
Some(other_value) if value == other_value => {}
_=> self.ctx.report_error(
2020-09-06 05:38:31 +00:00
vec![prev_field.pos, field.pos],
2020-03-12 09:11:02 +00:00
format!("Fields \"{}\" conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional.", name)),
}
}
} else {
2022-06-25 02:51:59 +00:00
self.outputs.insert((on_type, name), field);
2020-03-12 09:11:02 +00:00
}
}
}
2022-06-25 02:51:59 +00:00
#[cfg(test)]
mod tests {
use super::*;
pub fn factory() -> OverlappingFieldsCanBeMerged {
OverlappingFieldsCanBeMerged
}
#[test]
fn same_field_on_different_type() {
expect_passes_rule!(
factory,
r#"
{
pet {
... on Dog {
doesKnowCommand(dogCommand: SIT)
}
... on Cat {
doesKnowCommand(catCommand: JUMP)
}
}
}
"#,
);
}
#[test]
fn same_field_on_same_type() {
expect_fails_rule!(
factory,
r#"
{
pet {
... on Dog {
doesKnowCommand(dogCommand: SIT)
}
... on Dog {
doesKnowCommand(dogCommand: Heel)
}
}
}
"#,
);
}
#[test]
fn same_alias_on_different_type() {
expect_passes_rule!(
factory,
r#"
{
pet {
... on Dog {
volume: barkVolume
}
... on Cat {
volume: meowVolume
}
}
}
"#,
);
}
}