sydtest-mutation-plugin-0.5.0.0: src/Test/Syd/Mutation/Plugin/Operator/ListLit.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Test.Syd.Mutation.Plugin.Operator.ListLit (theOperator) where
import Control.Monad.Reader (asks)
import qualified Data.Map.Strict as Map
import GHC
import GHC.Builtin.Types (mkListTy)
import Test.Syd.Mutation.Plugin.Instrument (InstrM, InstrumentEnv (..), MutationAlt (..), MutationOperator (..), MutationOperatorKind (..), SrcSpanDelta (..))
import Test.Syd.Mutation.Plugin.Operator.Util (nameMatchCandidates)
import Test.Syd.Mutation.Plugin.OptParse (OperatorConfig (..), operatorExtraStrings)
-- | Shrink a list literal by removing elements or emptying it.
--
-- A list whose elements are the pieces of a message is a list nobody asserts
-- the elements of, and every such literal is one more mutant that can only be
-- killed by pinning wording. Those are named by the call they are an argument
-- of, under the operator's @skip-calls-to@ config key:
--
-- > operators:
-- > ListLit:
-- > skip-calls-to:
-- > - logInfo
-- > - fail
--
-- Any enclosing call counts, not only the immediate one, because the immediate
-- one is @mconcat@ or @unwords@ for every message built out of pieces: in
-- @fail $ mconcat [\"unknown: \", x]@ it is @fail@ that says what the list is
-- for, and it is two applications out. @$@ is seen through for the same
-- reason.
--
-- A name matches either bare (@fail@, matching any module) or fully qualified
-- (@GHC.Internal.Base.fail@). A qualifier may be either the function's
-- defining module or a module it is imported through.
theOperator :: MutationOperator
theOperator =
MutationOperator
{ operatorName = "ListLit",
operatorDescription = "Shrink a list literal by removing elements or emptying it",
operatorKind = ExpressionOperator $ \case
(L ann (ExplicitList elTy es))
| length es >= 2 ->
Just (action ann elTy es)
_ -> Nothing
}
action ::
SrcSpanAnnA ->
Type ->
[LHsExpr GhcTc] ->
InstrM [MutationAlt]
action ann elTy es = do
opsConfig <- asks instrumentEnvOperatorsConfig
rdrEnv <- asks instrumentEnvRdrEnv
enclosing <- asks instrumentEnvEnclosingCalls
let extra = maybe Map.empty operatorConfigExtra (Map.lookup "ListLit" opsConfig)
skipCallsTo = operatorExtraStrings "skip-calls-to" extra
skipThisList =
not (null skipCallsTo)
&& any
(\n -> any (`elem` skipCallsTo) (nameMatchCandidates rdrEnv n))
enclosing
if skipThisList
then pure []
else
let listTy = mkListTy elTy
n = length es
toRss e = case getLocA e of
RealSrcSpan rss _ -> [rss]
UnhelpfulSpan _ -> []
mkList xs delta =
MutationAlt
{ mutAltType = listTy,
mutAltExpr = L ann (ExplicitList elTy xs),
mutAltOriginal = show n ++ " elements",
mutAltReplacement = show (length xs) ++ " elements",
mutAltDelta = delta,
mutAltMitigation = Nothing
}
-- Always produce: empty list, drop-head.
-- Only add drop-last if it gives a different length than drop-head
-- (i.e. n > 2; when n == 2 both give one element).
lastE = reverse es
repls = case es of
[] -> []
(firstE : restEs) ->
mkList [] (SpanRemoval (concatMap toRss es))
: mkList restEs (SpanRemoval (toRss firstE))
: case lastE of
[] -> []
(le : _) -> [mkList (take (n - 1) es) (SpanRemoval (toRss le)) | n > 2]
in pure repls