packages feed

sydtest-mutation-plugin-0.5.0.0: src/Test/Syd/Mutation/Plugin/Operator/ConstEmptyList.hs

{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}

module Test.Syd.Mutation.Plugin.Operator.ConstEmptyList (theOperator) where

import Control.Monad.Reader (asks)
import qualified Data.Map as Map
import qualified Data.Text as T
import GHC
import GHC.Builtin.Types (charTyCon, listTyCon)
import GHC.Core.Type (tyConAppTyCon_maybe)
import Test.Syd.Mutation.Plugin.Instrument (InstrM, InstrumentEnv (..), MutationAlt (..), MutationOperator (..), MutationOperatorKind (..), OpAppCtx (..), SrcSpanDelta (..))
import Test.Syd.Mutation.Plugin.Operator.Util (ConstFnMatch (..), arrowTy, mkConstLambda, nameMatchCandidates, prefixFormPreview, unwrapWrap, viewConstFnResult)
import Test.Syd.Mutation.Plugin.OptParse (OperatorConfig (..), operatorExtraFlag, operatorExtraStrings)

-- | Replace an expression whose type is @arg1 -> ... -> argN -> [a]@
-- (with @N >= 0@) with the constant function returning @[]@.
--
-- Same shape and rationale as 'ConstNothing'; see that module's haddock.
-- Complements 'ListLit', which targets the syntactic 'ExplicitList' form.
--
-- It complements it in @skip-calls-to@ too, with the same meaning and the same
-- matching: a list holding the pieces of a message is a list nobody asserts
-- the elements of, and emptying it is a mutant that can only be killed by
-- pinning wording. Naming the calls that take a message reaches those lists
-- wherever they are written, including inside an instance method, which is
-- where an @ANN@ pragma cannot go.
--
-- > operators:
-- >   ConstEmptyList:
-- >     skip-calls-to:
-- >       - fail
--
-- 'ListLit' documents why any enclosing call counts rather than only the
-- immediate one.
theOperator :: MutationOperator
theOperator =
  MutationOperator
    { operatorName = "ConstEmptyList",
      operatorDescription = "Replace a list-typed expression (or a function returning a list) with a constant []",
      operatorKind = ExpressionOperator $ \le ->
        case viewConstFnResult 0 listTyCon le of
          Just m@ConstFnMatch {cfnTyConArgs = [elTy]} -> Just (action le elTy m)
          _ -> Nothing
    }

action ::
  LHsExpr GhcTc ->
  Type ->
  ConstFnMatch ->
  InstrM [MutationAlt]
action le elTy ConstFnMatch {cfnArgTys, cfnResTy} = do
  opAppCtx <- asks instrumentEnvOpAppCtx
  appDepth <- asks instrumentEnvAppDepth
  -- This operator interprets its own config entry's extra keys.
  opsConfig <- asks instrumentEnvOperatorsConfig
  rdrEnv <- asks instrumentEnvRdrEnv
  enclosing <- asks instrumentEnvEnclosingCalls
  let extra = maybe Map.empty operatorConfigExtra (Map.lookup "ConstEmptyList" opsConfig)
      skipStrings = operatorExtraFlag "skip-strings" extra
      skipLiteralStrings = operatorExtraFlag "skip-literal-strings" extra
      skipCallsTo = operatorExtraStrings "skip-calls-to" extra
      skippedByCall =
        not (null skipCallsTo)
          && any
            (\n -> any (`elem` skipCallsTo) (nameMatchCandidates rdrEnv n))
            enclosing
      arity = length cfnArgTys
      -- 'skip-strings' drops every @[Char]@-typed expression; the narrower
      -- 'skip-literal-strings' drops only syntactic string literals.
      skippedAsString =
        (skipStrings && isCharElemTy elTy)
          || (skipLiteralStrings && isCharElemTy elTy && isStringLiteralExpr le)
  -- See 'ConstNothing' for the dominance rule.
  if (arity >= 1 && appDepth >= arity) || skippedAsString || skippedByCall
    then pure []
    else
      let emptyExpr = noLocA (ExplicitList elTy [])
          wholeTy = arrowTy cfnArgTys cfnResTy
          mutated = mkConstLambda cfnArgTys cfnResTy emptyExpr
          atOpToken = case (arity, opAppCtx, getLocA le) of
            (n, Just ctx, RealSrcSpan mSp _) | n >= 1, mSp == opAppOpSpan ctx -> Just ctx
            _ -> Nothing
          delta = case atOpToken of
            Just ctx ->
              ReplaceOuterSpan
                (opAppOuterSpan ctx)
                (prefixFormPreview arity "[]" (opAppLhsText ctx) (opAppRhsText ctx))
            Nothing ->
              let tokenText = case cfnArgTys of
                    [] -> "[]"
                    _ ->
                      "(\\"
                        <> T.replicate arity "_ "
                        <> "-> [])"
               in TokenReplace tokenText
          origLabel = case cfnArgTys of
            [] -> "e"
            _ -> "f"
          replLabel = case cfnArgTys of
            [] -> "[]"
            _ -> "\\" ++ unwords (replicate arity "_") ++ " -> []"
       in pure
            [ MutationAlt
                { mutAltType = wholeTy,
                  mutAltExpr = mutated,
                  mutAltOriginal = origLabel,
                  mutAltReplacement = replLabel,
                  mutAltDelta = delta,
                  mutAltMitigation = Nothing
                }
            ]

-- | Whether the list element type is 'Char' (i.e. the list is a
-- @[Char]@/'String').  Used to honour @skip-strings@.
isCharElemTy :: Type -> Bool
isCharElemTy ty = tyConAppTyCon_maybe ty == Just charTyCon

-- | Whether the expression is (syntactically) a string literal.  Strips
-- evidence/parens wrappers and recognises the @fromString \"...\"@ form
-- that @OverloadedStrings@ produces.  Used to honour
-- @skip-literal-strings@.
isStringLiteralExpr :: LHsExpr GhcTc -> Bool
isStringLiteralExpr = go . unLoc
  where
    go e = case unwrapWrap e of
      HsLit _ HsString {} -> True
      HsPar _ inner -> go (unLoc inner)
      -- @OverloadedStrings@: @fromString "..."@.
      HsApp _ _ arg -> case unwrapWrap (unLoc arg) of
        HsLit _ HsString {} -> True
        _ -> False
      _ -> False