Validate fragments don't form cycles

This commit is contained in:
2020-09-11 08:03:49 +02:00
parent c2c57b6363
commit 08998dbd93
5 changed files with 128 additions and 21 deletions

View File

@ -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