From e0c557a1a6fcb6985157bb0ce5fbc596aa4c625d Mon Sep 17 00:00:00 2001 From: Sunli Date: Thu, 18 Nov 2021 20:14:56 +0800 Subject: [PATCH] Add DirectivesUnique rule --- src/model/directive.rs | 5 + src/registry/mod.rs | 1 + src/schema.rs | 6 +- src/validation/mod.rs | 1 + .../rules/arguments_of_correct_type.rs | 3 +- src/validation/rules/directives_unique.rs | 176 ++++++++++++++++++ src/validation/rules/known_argument_names.rs | 2 +- src/validation/rules/mod.rs | 2 + src/validation/rules/no_unused_variables.rs | 3 +- src/validation/rules/unique_argument_names.rs | 3 +- .../rules/variables_in_allowed_position.rs | 3 +- 11 files changed, 198 insertions(+), 7 deletions(-) create mode 100644 src/validation/rules/directives_unique.rs diff --git a/src/model/directive.rs b/src/model/directive.rs index d73d9c1b..db032ac8 100644 --- a/src/model/directive.rs +++ b/src/model/directive.rs @@ -103,4 +103,9 @@ impl<'a> __Directive<'a> { }) .collect() } + + #[inline] + async fn is_repeatable(&self) -> bool { + self.directive.is_repeatable + } } diff --git a/src/registry/mod.rs b/src/registry/mod.rs index 5d9aa982..c5f06397 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -349,6 +349,7 @@ pub struct MetaDirective { pub description: Option<&'static str>, pub locations: Vec, pub args: IndexMap<&'static str, MetaInputValue>, + pub is_repeatable: bool, } #[derive(Default)] diff --git a/src/schema.rs b/src/schema.rs index 536bb30c..4c63a592 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -287,7 +287,8 @@ where is_secret: false, }); args - } + }, + is_repeatable: false, }); registry.add_directive(MetaDirective { @@ -309,7 +310,8 @@ where is_secret: false, }); args - } + }, + is_repeatable: false, }); // register scalars diff --git a/src/validation/mod.rs b/src/validation/mod.rs index 81658447..fe8c030c 100644 --- a/src/validation/mod.rs +++ b/src/validation/mod.rs @@ -71,6 +71,7 @@ pub fn check_rules( .with(rules::PossibleFragmentSpreads::default()) .with(rules::ProvidedNonNullArguments) .with(rules::KnownDirectives::default()) + .with(rules::DirectivesUnique::default()) .with(rules::OverlappingFieldsCanBeMerged) .with(rules::UploadFile) .with(visitors::CacheControlCalculate { diff --git a/src/validation/rules/arguments_of_correct_type.rs b/src/validation/rules/arguments_of_correct_type.rs index 4bec68fe..8e6d09ae 100644 --- a/src/validation/rules/arguments_of_correct_type.rs +++ b/src/validation/rules/arguments_of_correct_type.rs @@ -1,12 +1,13 @@ use indexmap::map::IndexMap; +use async_graphql_value::Value; + use crate::context::QueryPathNode; use crate::parser::types::{Directive, Field}; use crate::registry::MetaInputValue; use crate::validation::utils::is_valid_input_value; use crate::validation::visitor::{Visitor, VisitorContext}; use crate::{Name, Positioned, QueryPathSegment}; -use async_graphql_value::Value; #[derive(Default)] pub struct ArgumentsOfCorrectType<'a> { diff --git a/src/validation/rules/directives_unique.rs b/src/validation/rules/directives_unique.rs new file mode 100644 index 00000000..14e74b18 --- /dev/null +++ b/src/validation/rules/directives_unique.rs @@ -0,0 +1,176 @@ +use crate::parser::types::{ + Directive, Field, FragmentDefinition, FragmentSpread, InlineFragment, OperationDefinition, + VariableDefinition, +}; +use crate::validation::visitor::Visitor; +use crate::VisitorContext; +use crate::{Name, Positioned}; +use std::collections::HashSet; + +#[derive(Default)] +pub struct DirectivesUnique; + +impl<'a> Visitor<'a> for DirectivesUnique { + fn enter_operation_definition( + &mut self, + ctx: &mut VisitorContext<'a>, + _name: Option<&'a Name>, + operation_definition: &'a Positioned, + ) { + check_duplicate_directive(ctx, &operation_definition.node.directives); + } + + fn enter_fragment_definition( + &mut self, + ctx: &mut VisitorContext<'a>, + _name: &'a Name, + fragment_definition: &'a Positioned, + ) { + check_duplicate_directive(ctx, &fragment_definition.node.directives); + } + + fn enter_variable_definition( + &mut self, + ctx: &mut VisitorContext<'a>, + variable_definition: &'a Positioned, + ) { + check_duplicate_directive(ctx, &variable_definition.node.directives); + } + + fn enter_field(&mut self, ctx: &mut VisitorContext<'a>, field: &'a Positioned) { + check_duplicate_directive(ctx, &field.node.directives); + } + + fn enter_fragment_spread( + &mut self, + ctx: &mut VisitorContext<'a>, + fragment_spread: &'a Positioned, + ) { + check_duplicate_directive(ctx, &fragment_spread.node.directives); + } + + fn enter_inline_fragment( + &mut self, + ctx: &mut VisitorContext<'a>, + inline_fragment: &'a Positioned, + ) { + check_duplicate_directive(ctx, &inline_fragment.node.directives); + } +} + +fn check_duplicate_directive(ctx: &mut VisitorContext<'_>, directives: &[Positioned]) { + let mut exists = HashSet::new(); + + for directive in directives { + let name = &directive.node.name.node; + if let Some(meta_directive) = ctx.registry.directives.get(name.as_str()) { + if !meta_directive.is_repeatable { + if exists.contains(name) { + ctx.report_error( + vec![directive.pos], + format!("Duplicate directive \"{}\"", name), + ); + continue; + } + exists.insert(name); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + pub fn factory() -> DirectivesUnique { + DirectivesUnique + } + + #[test] + fn skip_on_field() { + expect_passes_rule!( + factory, + r#" + { + dog { + name @skip(if: true) + } + } + "#, + ); + } + + #[test] + fn duplicate_skip_on_field() { + expect_fails_rule!( + factory, + r#" + { + dog { + name @skip(if: true) @skip(if: false) + } + } + "#, + ); + } + + #[test] + fn skip_on_fragment_spread() { + expect_passes_rule!( + factory, + r#" + fragment A on Dog { + name + } + + query { + dog ... A @skip(if: true) + } + "#, + ); + } + + #[test] + fn duplicate_skip_on_fragment_spread() { + expect_fails_rule!( + factory, + r#" + fragment A on Dog { + name + } + + query { + dog ... A @skip(if: true) @skip(if: false) + } + "#, + ); + } + + #[test] + fn skip_on_inline_fragment() { + expect_passes_rule!( + factory, + r#" + query { + dog ... @skip(if: true) { + name + } + } + "#, + ); + } + + #[test] + fn duplicate_skip_on_inline_fragment() { + expect_fails_rule!( + factory, + r#" + query { + dog ... @skip(if: true) @skip(if: false) { + name + } + } + "#, + ); + } +} diff --git a/src/validation/rules/known_argument_names.rs b/src/validation/rules/known_argument_names.rs index 56708706..b80fe396 100644 --- a/src/validation/rules/known_argument_names.rs +++ b/src/validation/rules/known_argument_names.rs @@ -1,3 +1,4 @@ +use async_graphql_value::Value; use indexmap::map::IndexMap; use crate::parser::types::{Directive, Field}; @@ -5,7 +6,6 @@ use crate::registry::MetaInputValue; use crate::validation::suggestion::make_suggestion; use crate::validation::visitor::{Visitor, VisitorContext}; use crate::{Name, Positioned}; -use async_graphql_value::Value; enum ArgsType<'a> { Directive(&'a str), diff --git a/src/validation/rules/mod.rs b/src/validation/rules/mod.rs index 157f741d..e36f0391 100644 --- a/src/validation/rules/mod.rs +++ b/src/validation/rules/mod.rs @@ -1,5 +1,6 @@ mod arguments_of_correct_type; mod default_values_of_correct_type; +mod directives_unique; mod fields_on_correct_type; mod fragments_on_composite_types; mod known_argument_names; @@ -22,6 +23,7 @@ mod variables_in_allowed_position; pub use arguments_of_correct_type::ArgumentsOfCorrectType; pub use default_values_of_correct_type::DefaultValuesOfCorrectType; +pub use directives_unique::DirectivesUnique; pub use fields_on_correct_type::FieldsOnCorrectType; pub use fragments_on_composite_types::FragmentsOnCompositeTypes; pub use known_argument_names::KnownArgumentNames; diff --git a/src/validation/rules/no_unused_variables.rs b/src/validation/rules/no_unused_variables.rs index 86103dca..44c44f09 100644 --- a/src/validation/rules/no_unused_variables.rs +++ b/src/validation/rules/no_unused_variables.rs @@ -1,12 +1,13 @@ use std::collections::{HashMap, HashSet}; +use async_graphql_value::Value; + use crate::parser::types::{ ExecutableDocument, FragmentDefinition, FragmentSpread, OperationDefinition, VariableDefinition, }; use crate::validation::utils::{referenced_variables, Scope}; use crate::validation::visitor::{Visitor, VisitorContext}; use crate::{Name, Pos, Positioned}; -use async_graphql_value::Value; #[derive(Default)] pub struct NoUnusedVariables<'a> { diff --git a/src/validation/rules/unique_argument_names.rs b/src/validation/rules/unique_argument_names.rs index c30e3795..30f59529 100644 --- a/src/validation/rules/unique_argument_names.rs +++ b/src/validation/rules/unique_argument_names.rs @@ -1,9 +1,10 @@ use std::collections::HashSet; +use async_graphql_value::Value; + use crate::parser::types::{Directive, Field}; use crate::validation::visitor::{Visitor, VisitorContext}; use crate::{Name, Positioned}; -use async_graphql_value::Value; #[derive(Default)] pub struct UniqueArgumentNames<'a> { diff --git a/src/validation/rules/variables_in_allowed_position.rs b/src/validation/rules/variables_in_allowed_position.rs index e8a837ee..db028f70 100644 --- a/src/validation/rules/variables_in_allowed_position.rs +++ b/src/validation/rules/variables_in_allowed_position.rs @@ -1,5 +1,7 @@ use std::collections::{HashMap, HashSet}; +use async_graphql_value::Value; + use crate::parser::types::{ ExecutableDocument, FragmentDefinition, FragmentSpread, OperationDefinition, VariableDefinition, }; @@ -7,7 +9,6 @@ use crate::registry::MetaTypeName; use crate::validation::utils::Scope; use crate::validation::visitor::{Visitor, VisitorContext}; use crate::{Name, Pos, Positioned}; -use async_graphql_value::Value; #[derive(Default)] pub struct VariableInAllowedPosition<'a> {