summaryrefslogtreecommitdiff
path: root/src/Language/GraphQL/Validate
diff options
context:
space:
mode:
authorEugen Wissner <belka@caraus.de>2020-09-11 08:03:49 +0200
committerEugen Wissner <belka@caraus.de>2020-09-11 08:03:49 +0200
commit08998dbd935e65aab10ff53c249cb214af2522f2 (patch)
treef5b502ce73ede2500dd0a508145b317e5f81b7fe /src/Language/GraphQL/Validate
parentc2c57b636392ae67a118ce5be04ad8f4b1304ed5 (diff)
downloadgraphql-08998dbd935e65aab10ff53c249cb214af2522f2.tar.gz
Validate fragments don't form cycles
Diffstat (limited to 'src/Language/GraphQL/Validate')
-rw-r--r--src/Language/GraphQL/Validate/Rules.hs69
1 files changed, 67 insertions, 2 deletions
diff --git a/src/Language/GraphQL/Validate/Rules.hs b/src/Language/GraphQL/Validate/Rules.hs
index 6a079f1..f5fdf9f 100644
--- a/src/Language/GraphQL/Validate/Rules.hs
+++ b/src/Language/GraphQL/Validate/Rules.hs
@@ -14,6 +14,7 @@ module Language.GraphQL.Validate.Rules
, fragmentSpreadTargetDefinedRule
, fragmentSpreadTypeExistenceRule
, loneAnonymousOperationRule
+ , noFragmentCyclesRule
, noUnusedFragmentsRule
, singleFieldSubscriptionsRule
, specifiedRules
@@ -23,11 +24,15 @@ module Language.GraphQL.Validate.Rules
import Control.Monad (foldM)
import Control.Monad.Trans.Class (MonadTrans(..))
-import Control.Monad.Trans.Reader (asks)
-import Control.Monad.Trans.State (evalStateT, gets, modify)
+import Control.Monad.Trans.Reader (ReaderT, asks)
+import Control.Monad.Trans.State (StateT, evalStateT, gets, modify)
+import Data.Bifunctor (first)
import Data.Foldable (find)
import qualified Data.HashMap.Strict as HashMap
+import Data.HashMap.Strict (HashMap)
import qualified Data.HashSet as HashSet
+import Data.List (sortBy)
+import Data.Ord (comparing)
import Data.Text (Text)
import qualified Data.Text as Text
import Language.GraphQL.AST.Document
@@ -50,6 +55,7 @@ specifiedRules =
, fragmentsOnCompositeTypesRule
, noUnusedFragmentsRule
, fragmentSpreadTargetDefinedRule
+ , noFragmentCyclesRule
]
-- | Definition must be OperationDefinition or FragmentDefinition.
@@ -382,3 +388,62 @@ noUnusedFragmentsRule = FragmentDefinitionRule $ \fragment ->
evaluateSelections :: Foldable t => Name -> Bool -> t Selection -> Bool
evaluateSelections fragName accumulator selections =
foldr (evaluateSelection fragName) accumulator selections
+
+-- | The graph of fragment spreads must not form any cycles including spreading
+-- itself. Otherwise an operation could infinitely spread or infinitely execute
+-- on cycles in the underlying data.
+noFragmentCyclesRule :: forall m. Rule m
+noFragmentCyclesRule = FragmentDefinitionRule $ \case
+ FragmentDefinition fragmentName _ _ selections location -> do
+ state <- evalStateT (collectFields selections)
+ (0, fragmentName)
+ let spreadPath = fst <$> sortBy (comparing snd) (HashMap.toList state)
+ case reverse spreadPath of
+ x : _ | x == fragmentName -> pure $ Error
+ { message = concat
+ [ "Cannot spread fragment \""
+ , Text.unpack fragmentName
+ , "\" within itself (via "
+ , Text.unpack $ Text.intercalate " -> " $ fragmentName : spreadPath
+ , ")."
+ ]
+ , locations = [location]
+ , path = []
+ }
+ _ -> lift Nothing
+ where
+ collectFields :: Traversable t
+ => forall m
+ . t Selection
+ -> StateT (Int, Name) (ReaderT (Validation m) Maybe) (HashMap Name Int)
+ collectFields selectionSet = foldM forEach HashMap.empty selectionSet
+ forEach accumulator = \case
+ FieldSelection fieldSelection -> forField accumulator fieldSelection
+ InlineFragmentSelection fragmentSelection ->
+ forInline accumulator fragmentSelection
+ FragmentSpreadSelection fragmentSelection ->
+ forSpread accumulator fragmentSelection
+ forSpread accumulator (FragmentSpread fragmentName _ _) = do
+ firstFragmentName <- gets snd
+ modify $ first (+ 1)
+ lastIndex <- gets fst
+ let newAccumulator = HashMap.insert fragmentName lastIndex accumulator
+ let inVisitetFragment =HashMap.member fragmentName accumulator
+ if fragmentName == firstFragmentName || inVisitetFragment
+ then pure newAccumulator
+ else collectFromSpread fragmentName newAccumulator
+ forInline accumulator (InlineFragment _ _ selections _) =
+ (accumulator <>) <$> collectFields selections
+ forField accumulator (Field _ _ _ _ selections _) =
+ (accumulator <>) <$> collectFields selections
+ findFragmentDefinition n (ExecutableDefinition executableDefinition) Nothing
+ | DefinitionFragment fragmentDefinition <- executableDefinition
+ , FragmentDefinition fragmentName _ _ _ _ <- fragmentDefinition
+ , fragmentName == n = Just fragmentDefinition
+ findFragmentDefinition _ _ accumulator = accumulator
+ collectFromSpread _fragmentName accumulator = do
+ ast' <- lift $ asks ast
+ case foldr (findFragmentDefinition _fragmentName) Nothing ast' of
+ Nothing -> pure accumulator
+ Just (FragmentDefinition _ _ _ selections _) ->
+ (accumulator <>) <$> collectFields selections