use async_graphql_value::Value; use indexmap::map::IndexMap; use crate::{ parser::types::{Directive, Field}, registry::MetaInputValue, validation::{ suggestion::make_suggestion, visitor::{Visitor, VisitorContext}, }, Name, Positioned, }; enum ArgsType<'a> { Directive(&'a str), Field { field_name: &'a str, type_name: &'a str, }, } #[derive(Default)] pub struct KnownArgumentNames<'a> { current_args: Option<(&'a IndexMap, ArgsType<'a>)>, } impl<'a> KnownArgumentNames<'a> { fn get_suggestion(&self, name: &str) -> String { make_suggestion( " Did you mean", self.current_args .iter() .map(|(args, _)| args.iter().map(|arg| arg.0.as_str())) .flatten(), name, ) .unwrap_or_default() } } impl<'a> Visitor<'a> for KnownArgumentNames<'a> { fn enter_directive( &mut self, ctx: &mut VisitorContext<'a>, directive: &'a Positioned, ) { self.current_args = ctx .registry .directives .get(directive.node.name.node.as_str()) .map(|d| (&d.args, ArgsType::Directive(&directive.node.name.node))); } fn exit_directive( &mut self, _ctx: &mut VisitorContext<'a>, _directive: &'a Positioned, ) { self.current_args = None; } fn enter_argument( &mut self, ctx: &mut VisitorContext<'a>, name: &'a Positioned, _value: &'a Positioned, ) { if let Some((args, arg_type)) = &self.current_args { if !args.contains_key(name.node.as_str()) { match arg_type { ArgsType::Field { field_name, type_name, } => { ctx.report_error( vec![name.pos], format!( "Unknown argument \"{}\" on field \"{}\" of type \"{}\".{}", name, field_name, type_name, self.get_suggestion(name.node.as_str()) ), ); } ArgsType::Directive(directive_name) => { ctx.report_error( vec![name.pos], format!( "Unknown argument \"{}\" on directive \"{}\".{}", name, directive_name, self.get_suggestion(name.node.as_str()) ), ); } } } } } fn enter_field(&mut self, ctx: &mut VisitorContext<'a>, field: &'a Positioned) { if let Some(parent_type) = ctx.parent_type() { if let Some(schema_field) = parent_type.field_by_name(&field.node.name.node) { self.current_args = Some(( &schema_field.args, ArgsType::Field { field_name: &field.node.name.node, type_name: ctx.parent_type().unwrap().name(), }, )); } } } fn exit_field(&mut self, _ctx: &mut VisitorContext<'a>, _field: &'a Positioned) { self.current_args = None; } } #[cfg(test)] mod tests { use super::*; pub fn factory<'a>() -> KnownArgumentNames<'a> { KnownArgumentNames::default() } #[test] fn single_arg_is_known() { expect_passes_rule!( factory, r#" fragment argOnRequiredArg on Dog { doesKnowCommand(dogCommand: SIT) } { __typename } "#, ); } #[test] fn multiple_args_are_known() { expect_passes_rule!( factory, r#" fragment multipleArgs on ComplicatedArgs { multipleReqs(req1: 1, req2: 2) } { __typename } "#, ); } #[test] fn ignores_args_of_unknown_fields() { expect_passes_rule!( factory, r#" fragment argOnUnknownField on Dog { unknownField(unknownArg: SIT) } { __typename } "#, ); } #[test] fn multiple_args_in_reverse_order_are_known() { expect_passes_rule!( factory, r#" fragment multipleArgsReverseOrder on ComplicatedArgs { multipleReqs(req2: 2, req1: 1) } { __typename } "#, ); } #[test] fn no_args_on_optional_arg() { expect_passes_rule!( factory, r#" fragment noArgOnOptionalArg on Dog { isHousetrained } { __typename } "#, ); } #[test] fn args_are_known_deeply() { expect_passes_rule!( factory, r#" { dog { doesKnowCommand(dogCommand: SIT) } human { pet { ... on Dog { doesKnowCommand(dogCommand: SIT) } } } } "#, ); } #[test] fn directive_args_are_known() { expect_passes_rule!( factory, r#" { dog @skip(if: true) } "#, ); } #[test] fn undirective_args_are_invalid() { expect_fails_rule!( factory, r#" { dog @skip(unless: true) } "#, ); } #[test] fn invalid_arg_name() { expect_fails_rule!( factory, r#" fragment invalidArgName on Dog { doesKnowCommand(unknown: true) } { __typename } "#, ); } #[test] fn unknown_args_amongst_known_args() { expect_fails_rule!( factory, r#" fragment oneGoodArgOneInvalidArg on Dog { doesKnowCommand(whoknows: 1, dogCommand: SIT, unknown: true) } { __typename } "#, ); } #[test] fn unknown_args_deeply() { expect_fails_rule!( factory, r#" { dog { doesKnowCommand(unknown: true) } human { pet { ... on Dog { doesKnowCommand(unknown: true) } } } } "#, ); } }