packages feed

mockcat-1.2.1.0: src/Test/MockCat/TH.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -Wno-name-shadowing #-}
{-# OPTIONS_GHC -Wno-unused-local-binds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE NamedFieldPuns #-}

module Test.MockCat.TH
  ( showExp,
    expectByExpr,
    makeMock,
    makeAutoLiftMock,
    makePartialMock,
    makeAutoLiftPartialMock,
  )
where

import Control.Monad (unless, when)
import Data.List (elemIndex, nub)
import Data.Maybe (catMaybes)
import qualified Data.Map.Strict as Map

import Language.Haskell.TH
  ( Cxt,
    Dec (..),
    Exp (..),
    Extension (..),
    FunDep,
    Info (..),
    Lit (..),
    Name,
    Pat (..),
    Pred,
    Q,
    TyVarBndr (..),
    TySynEqn (..),
    TypeFamilyHead (..),
    Type (..),
    isExtEnabled,
    mkName,
    pprint,
    reify,
  )
import Language.Haskell.TH.Lib
import Language.Haskell.TH.PprLib (Doc, hcat, parens, text)
import Language.Haskell.TH.Syntax (nameBase)
import Test.MockCat.Mock ()
import Test.MockCat.MockT
import Test.MockCat.TH.ClassAnalysis
  ( ClassName2VarNames(..),
    VarName2ClassNames(..),
    filterClassInfo,
    filterMonadicVarInfos,
    getClassName,
    getClassNames,
    toClassInfos,
    VarAppliedType(..),
    applyVarAppliedTypes )
import Test.MockCat.TH.ContextBuilder
  ( MockType (..),
    buildContext,
    getTypeVarName,
    getTypeVarNames,
    tyVarBndrToType,
    applyFamilyArg,
    convertTyVarBndr
  )
import Test.MockCat.TH.TypeUtils
  ( splitApps,
    substituteType
  )
import Test.MockCat.TH.FunctionBuilder
  ( createFnName,
    typeToNames,
    safeIndex,
    MockFnContext(..)
    , buildMockFnContext
    , buildMockFnDeclarations
    , createNoInlinePragma
    , generateInstanceMockFnBody
    , generateInstanceRealFnBody
  )
import Test.MockCat.TH.Types (MockOptions(..), options)
import Test.MockCat.Verify ()
import Test.MockCat.Param
import Prelude as P


showExp :: Q Exp -> Q String
showExp qexp = show . pprintExp <$> qexp

pprintExp :: Exp -> Doc
pprintExp (VarE name) = text (nameBase name)
pprintExp (ConE name) = text (nameBase name)
pprintExp (LitE lit) = pprintLit lit
pprintExp (AppE e1 e2) = parens $ hcat [pprintExp e1, text " ", pprintExp e2]
pprintExp (InfixE e1 e2 e3) = pprintInfixE e1 e2 e3
pprintExp (LamE pats body) = parens $ hcat [text "\\", pprintPats pats, text " -> ", pprintExp body]
pprintExp (TupE exps) = parens $ hcat (map (maybe (text "") pprintExp) exps)
pprintExp (ListE exps) = parens $ hcat (map pprintExp exps)
pprintExp (SigE e _) = pprintExp e
pprintExp x = text (pprint x)

pprintInfixE :: Maybe Exp -> Exp -> Maybe Exp -> Doc
pprintInfixE e1 e2 e3 =
  parens $
    hcat
      [ maybe (text "") pprintExp e1,
        maybe (text "") (const (text " ")) e1,
        pprintExp e2,
        text " ",
        maybe (text "") pprintExp e3
      ]

pprintPats :: [Pat] -> Doc
pprintPats = hcat . map pprintPat

pprintPat :: Pat -> Doc
pprintPat (VarP name) = text (nameBase name)
pprintPat p = text (pprint p)

pprintLit :: Lit -> Doc
pprintLit (IntegerL n) = text (show n)
pprintLit (RationalL r) = text (show r)
pprintLit (StringL s) = text (show s)
pprintLit (CharL c) = text (show c)
pprintLit l = text (pprint l)

-- | Create a conditional parameter based on @Q Exp@.
--
--  In calling a mock function, if the argument does not satisfy this condition, an error is raised.
--
--  The conditional expression is displayed in the error message.
expectByExpr :: Q Exp -> Q Exp
expectByExpr qf = do
  str <- showExp qf
  [|ExpectCondition $qf str|]



-- | Create a mock of a typeclasses that returns a monad.
--
--  Given a monad type class, generate the following.
--
--  - MockT instance of the given typeclass
--  - A stub function corresponding to a function of the original class type.
-- The name of stub function is the name of the original function with a "_" appended.
--
--  The prefix can be changed.
--  In that case, use `makeMockWithOptions`.
--
--  @
--  class (Monad m) => FileOperation m where
--    writeFile :: FilePath -\> Text -\> m ()
--    readFile :: FilePath -\> m Text
--
--  makeMock [t|FileOperation|]
--
--  spec :: Spec
--  spec = do
--    it "test runMockT" do
--      result \<- runMockT do
--        _readFile $ "input.txt" ~> pack "content"
--        _writeFile $ "output.text" ~> pack "content" ~> ()
--        somethingProgram
--
--      result `shouldBe` ()
--  @
makeMock :: Q Type -> Q [Dec]
makeMock t = doMakeMock t Total options

-- | Create a mock of a typeclasses that returns a monad.
--
--  Given a monad type class, generate the following.
--
--  - MockT instance of the given typeclass
--  - A stub function corresponding to a function of the original class type.
-- The name of stub function is the name of the original function with a "_" appended.
--
--  This function automatically wraps the return value in a monad (Implicit Monadic Return).
--
--  @
--  class (Monad m) => FileOperation m where
--    writeFile :: FilePath -\> Text -\> m ()
--    readFile :: FilePath -\> m Text
--
--  makeAutoLiftMock [t|FileOperation|]
--
--  spec :: Spec
--  spec = do
--    it "test runMockT" do
--      result \<- runMockT do
--        _readFile $ "input.txt" ~> pack "content"
--        _writeFile $ "output.text" ~> pack "content" ~> ()
--        somethingProgram
--
--      result `shouldBe` ()
--  @
makeAutoLiftMock :: Q Type -> Q [Dec]
makeAutoLiftMock t = doMakeMock t Total (options { implicitMonadicReturn = True })

-- | Create a partial mock of a typeclasses that returns a monad.
--
--  Given a monad type class, generate the following.
--
--  - MockT instance of the given typeclass
--  - A stub function corresponding to a function of the original class type.
-- The name of stub function is the name of the original function with a "_" appended.
--
--  For functions that are not stubbed in the test, the real function is used as appropriate for the context.
--
--  The prefix can be changed.
--  In that case, use `makePartialMockWithOptions`.
--
--  @
--  class Monad m => Finder a b m | a -> b, b -> a where
--    findIds :: m [a]
--    findById :: a -> m b
--
--  instance Finder Int String IO where
--    findIds = pure [1, 2, 3]
--    findById id = pure $ "{id: " <> show id <> "}"
--
--  findValue :: Finder a b m => m [b]
--  findValue = do
--    ids <- findIds
--    mapM findById ids
--
--  makePartialMock [t|Finder|]
--
--  spec :: Spec
--  spec = do
--    it "Use all real functions." do
--      values <- runMockT findValue
--      values `shouldBe` ["{id: 1}", "{id: 2}", "{id: 3}"]
--
--    it "Only findIds should be stubbed." do
--      values <- runMockT do
--        _findIds [1 :: Int, 2]
--        findValue
--      values `shouldBe` ["{id: 1}", "{id: 2}"]
--  @
makePartialMock :: Q Type -> Q [Dec]
makePartialMock t = doMakeMock t Partial options

-- | `makePartialMock` with `implicitMonadicReturn = True` by default.
makeAutoLiftPartialMock :: Q Type -> Q [Dec]
makeAutoLiftPartialMock t = doMakeMock t Partial (options { implicitMonadicReturn = True })



doMakeMock :: Q Type -> MockType -> MockOptions -> Q [Dec]
doMakeMock qType mockType options = do
  ty <- qType
  let className = getClassName ty
  classMetadata <- loadClassMetadata className
  verifyRequiredExtensions (cmRequirements classMetadata)
  monadVarName <- selectMonadVarName classMetadata
  makeMockDecs
    ty
    mockType
    className
    monadVarName
    (cmContext classMetadata)
    (cmTypeVars classMetadata)
    (cmDecs classMetadata)
    options

data ClassRequirements = ClassRequirements
  { reqMultiParamTypeClasses :: Bool,
    reqFunctionalDependencies :: Bool,
    reqTypeFamilies :: Bool,
    reqTypeOperators :: Bool,
    reqHasContext :: Bool
  }

verifyRequiredExtensions :: ClassRequirements -> Q ()
verifyRequiredExtensions requirements = do
  -- Standard set of extensions required for Mockcat's machinery to function correctly
  mapM_
    verifyExtension
    [ DataKinds,
      FlexibleInstances,
      FlexibleContexts,
      TypeApplications,
      ScopedTypeVariables,
      TypeFamilies
    ]

  -- Additional extensions required based on the specific type class definition
  when requirements.reqMultiParamTypeClasses (verifyExtension MultiParamTypeClasses)

  when
    ( (requirements.reqMultiParamTypeClasses && requirements.reqHasContext)
        || requirements.reqFunctionalDependencies
    )
    (verifyExtension UndecidableInstances)

  when requirements.reqFunctionalDependencies do
    verifyExtension AllowAmbiguousTypes
    verifyExtension FunctionalDependencies

  when requirements.reqTypeOperators (verifyExtension TypeOperators)

loadClassMetadata :: Name -> Q ClassMetadata
loadClassMetadata className = do
  info <- reify className
  case info of
    ClassI (ClassD _ _ [] _ _) _ ->
      fail $ "A type parameter is required for class " <> show className
    ClassI (ClassD cxt name typeVars fundeps decs) _ ->
      pure $
        ClassMetadata
          { cmName = className,
            cmContext = cxt,
            cmTypeVars = map convertTyVarBndr typeVars,
            cmDecs = decs,
            cmRequirements = detectRequirements cxt name typeVars fundeps decs
          }
    other -> error $ "unsupported type: " <> show other

detectRequirements :: Cxt -> Name -> [TyVarBndr a] -> [FunDep] -> [Dec] -> ClassRequirements
detectRequirements cxt className typeVars fundeps decs =
  ClassRequirements
    { reqMultiParamTypeClasses = length typeVars > 1,
      reqFunctionalDependencies = not (null fundeps),
      reqTypeFamilies = P.any isTypeFamilyDec decs,
      reqTypeOperators = P.any isOperatorName allNames,
      reqHasContext = not (null cxt)
    }
  where
    allNames = className : concatMap collectDecNames decs
    isTypeFamilyDec (OpenTypeFamilyD _) = True
    isTypeFamilyDec (ClosedTypeFamilyD _ _) = True
    isTypeFamilyDec (DataFamilyD _ _ _) = True
    isTypeFamilyDec (TySynInstD _) = True
    isTypeFamilyDec (DataInstD {}) = True
    isTypeFamilyDec _ = False

    isOperatorName n = P.any (`elem` (":!#$%&*+./<=>?@\\^|-~" :: String)) (nameBase n)

    collectDecNames (SigD n _) = [n]
    collectDecNames (OpenTypeFamilyD (TypeFamilyHead n _ _ _)) = [n]
    collectDecNames (ClosedTypeFamilyD (TypeFamilyHead n _ _ _) _) = [n]
    collectDecNames (DataFamilyD n _ _) = [n]
    collectDecNames (TySynInstD (TySynEqn _ lhs _)) = collectTypeNames lhs
    collectDecNames _ = []

    collectTypeNames (AppT t1 t2) = collectTypeNames t1 ++ collectTypeNames t2
    collectTypeNames (ConT n) = [n]
    collectTypeNames (VarT n) = [n]
    collectTypeNames _ = []

selectMonadVarName :: ClassMetadata -> Q Name
selectMonadVarName metadata = do
  monadVarNames <- getMonadVarNames (cmContext metadata) (cmTypeVars metadata)
  case nub monadVarNames of
    [] -> fail "Monad parameter not found."
    (monadVarName : rest)
      | length rest > 1 -> fail "Monad parameter must be unique."
      | otherwise -> pure monadVarName

makeMockDecs :: Type -> MockType -> Name -> Name -> Cxt -> [TyVarBndr a] -> [Dec] -> MockOptions -> Q [Dec]
makeMockDecs ty mockType className monadVarName cxt typeVars decs options = do
  let classParamNames = filter (className /=) (getClassNames ty)
      newTypeVars = drop (length classParamNames) typeVars
      varAppliedTypes = zipWith (\t i -> VarAppliedType t (safeIndex classParamNames i)) (getTypeVarNames typeVars) [0 ..]
      sigDecs = [dec | dec@(SigD _ _) <- decs]
      typeFamilyHeads =
        [head | OpenTypeFamilyD head <- decs] ++
        [head | ClosedTypeFamilyD head _ <- decs]

  let typeInstDecs = map (createTypeInstanceDec monadVarName) typeFamilyHeads
      instanceBodyDecs = map (createInstanceFnDec mockType options) sigDecs ++ typeInstDecs
      fullCxt = buildContext cxt mockType className monadVarName newTypeVars varAppliedTypes
  (superClassDecs, predsToDrop) <-
    deriveSuperClassInstances
      mockType
      monadVarName
      newTypeVars
      varAppliedTypes
      options
      cxt
  let filteredCxt = filter (`notElem` predsToDrop) fullCxt
  instanceDec <-
    instanceD
      (pure filteredCxt)
      (createInstanceType ty monadVarName newTypeVars)
      instanceBodyDecs
  mockFnDecs <- concat <$> mapM (mockDec mockType monadVarName varAppliedTypes options) sigDecs

  pure $ superClassDecs ++ (instanceDec : mockFnDecs)

deriveSuperClassInstances ::
  MockType ->
  Name ->
  [TyVarBndr a] ->
  [VarAppliedType] ->
  MockOptions ->
  Cxt ->
  Q ([Dec], [Pred])
deriveSuperClassInstances mockType _ _ _ _ _
  | mockType /= Total = pure ([], [])
deriveSuperClassInstances _ monadVarName typeVars varAppliedTypes _ cxt = do
  results <- mapM (deriveSuperClassInstance monadVarName typeVars varAppliedTypes) cxt
  let valid = catMaybes results
  pure (map fst valid, map snd valid)

deriveSuperClassInstance ::
  Name ->
  [TyVarBndr a] ->
  [VarAppliedType] ->
  Pred ->
  Q (Maybe (Dec, Pred))
deriveSuperClassInstance _ _ varAppliedTypes pred = do
  superInfo <- resolveSuperClassInfo pred
  maybe (pure Nothing) (buildSuperClassDerivation varAppliedTypes) superInfo
  where
    resolveSuperClassInfo :: Pred -> Q (Maybe SuperClassInfo)
    resolveSuperClassInfo target =
      case splitApps target of
        (ConT superName, args) -> do
          info <- reify superName
          pure $
            case info of
              ClassI (ClassD superCxt _ superTypeVars _ superDecs) _ ->
                Just $ SuperClassInfo superName args superCxt (map convertTyVarBndr superTypeVars) superDecs
              _ -> Nothing
        _ -> pure Nothing

    buildSuperClassDerivation ::
      [VarAppliedType] ->
      SuperClassInfo ->
      Q (Maybe (Dec, Pred))
    buildSuperClassDerivation appliedTypes info
      | superClassHasMethods info = pure Nothing
      | otherwise = do
          superMonadVars <- getMonadVarNames (scContext info) (scTypeVars info)
          case superMonadVars of
            [superMonadVar] -> buildMockInstance appliedTypes info superMonadVar
            _ -> pure Nothing

    buildMockInstance ::
      [VarAppliedType] ->
      SuperClassInfo ->
      Name ->
      Q (Maybe (Dec, Pred))
    buildMockInstance appliedTypes info superMonadVar = do
      let superVarNames = map getTypeVarName (scTypeVars info)
      if length superVarNames /= length (scArgs info)
        then pure Nothing
        else do
          let (contextPreds, instanceType) =
                buildInstancePieces appliedTypes info superMonadVar superVarNames
          instanceDec <- instanceD (pure contextPreds) (pure instanceType) []
          pure $ Just (instanceDec, instanceType)

    buildInstancePieces ::
      [VarAppliedType] ->
      SuperClassInfo ->
      Name ->
      [Name] ->
      ([Pred], Pred)
    buildInstancePieces appliedTypes info superMonadVar superVarNames =
      let substitutedArgs = map (applyVarAppliedTypes appliedTypes) (scArgs info)
          subMap = Map.fromList (zip superVarNames substitutedArgs)
          instanceArgs =
            map
              (buildInstanceArg appliedTypes superMonadVar subMap)
              superVarNames
          instanceType = foldl AppT (ConT (scName info)) instanceArgs
          contextPreds =
            map
              (applyVarAppliedTypes appliedTypes . substituteType subMap)
              (scContext info)
       in (contextPreds, instanceType)

    buildInstanceArg ::
      [VarAppliedType] ->
      Name ->
      Map.Map Name Type ->
      Name ->
      Type
    buildInstanceArg appliedTypes superMonadVar subMap var =
      let applied = applyVarAppliedTypes appliedTypes (lookupType subMap var)
       in if var == superMonadVar
            then AppT (ConT ''MockT) applied
            else applied

    lookupType :: Map.Map Name Type -> Name -> Type
    lookupType subMap key = Map.findWithDefault (VarT key) key subMap

    superClassHasMethods :: SuperClassInfo -> Bool
    superClassHasMethods = P.any isSignature . scDecs

    isSignature (SigD _ _) = True
    isSignature _ = False


data SuperClassInfo = SuperClassInfo
  { scName :: Name,
    scArgs :: [Type],
    scContext :: Cxt,
    scTypeVars :: [TyVarBndr ()],
    scDecs :: [Dec]
  }

data ClassMetadata = ClassMetadata
  { cmName :: Name,
    cmContext :: Cxt,
    cmTypeVars :: [TyVarBndr ()],
    cmDecs :: [Dec],
    cmRequirements :: ClassRequirements
  }

getMonadVarNames :: Cxt -> [TyVarBndr a] -> Q [Name]
getMonadVarNames cxt typeVars = do
  let parentClassInfos = toClassInfos cxt

      typeVarNames = getTypeVarNames typeVars
      -- VarInfos (class names is empty)
      emptyClassVarInfos = map (`VarName2ClassNames` []) typeVarNames

  varInfos <- collectVarInfos parentClassInfos emptyClassVarInfos

  pure $ (\(VarName2ClassNames n _) -> n) <$> filterMonadicVarInfos varInfos

collectVarInfos :: [ClassName2VarNames] -> [VarName2ClassNames] -> Q [VarName2ClassNames]
collectVarInfos classInfos = mapM (collectVarInfo classInfos)

collectVarInfo :: [ClassName2VarNames] -> VarName2ClassNames -> Q VarName2ClassNames
collectVarInfo classInfos (VarName2ClassNames vName classNames) = do
  varClassNames <- collectVarClassNames vName classInfos
  pure $ VarName2ClassNames vName (classNames ++ varClassNames)

collectVarClassNames :: Name -> [ClassName2VarNames] -> Q [Name]
collectVarClassNames varName classInfos = do
  let targetClassInfos = filterClassInfo varName classInfos
  concat <$> mapM (collectVarClassNames_ varName) targetClassInfos

collectVarClassNames_ :: Name -> ClassName2VarNames -> Q [Name]
collectVarClassNames_ name (ClassName2VarNames cName vNames) = do
  case elemIndex name vNames of
    Nothing -> pure []
    Just i -> do
      ClassI (ClassD cxt _ typeVars _ _) _ <- reify cName
      let -- type variable names
          typeVarNames = getTypeVarNames typeVars
          -- type variable name of same position
          typeVarName = typeVarNames !! i
          -- parent class information
          parentClassInfos = toClassInfos cxt

      case parentClassInfos of
        [] -> pure [cName]
        _ -> do
          result <- concat <$> mapM (collectVarClassNames_ typeVarName) parentClassInfos
          pure $ cName : result

createInstanceType :: Type -> Name -> [TyVarBndr a] -> Q Type
createInstanceType className monadName tvbs = do
  let types = fmap (tyVarBndrToType monadName) tvbs
  pure $ foldl AppT className types

createTypeInstanceDec :: Name -> TypeFamilyHead -> Q Dec
createTypeInstanceDec monadVarName (TypeFamilyHead familyName tfVars _ _) = do
  let lhsArgs = map (applyFamilyArg monadVarName) tfVars
      rhsArgs = map (VarT . getTypeVarName) tfVars
      lhsType = foldl AppT (ConT familyName) lhsArgs
      rhsType = foldl AppT (ConT familyName) rhsArgs
  pure $ TySynInstD (TySynEqn Nothing lhsType rhsType)

createInstanceFnDec :: MockType -> MockOptions -> Dec -> Q Dec
createInstanceFnDec mockType options (SigD fnName funType) = do
  names <- sequence $ typeToNames funType
  let r = mkName "result"
      params = varP <$> names
      args = varE <$> names
      fnNameStr = createFnName fnName options

      fnBody = case mockType of
        Total -> generateInstanceMockFnBody fnNameStr args r options
        Partial -> generateInstanceRealFnBody fnName fnNameStr args r options

      fnClause = clause params (normalB fnBody) []
  funD fnName [fnClause]
createInstanceFnDec _ _ dec = fail $ "unsuported dec: " <> pprint dec



mockDec :: MockType -> Name -> [VarAppliedType] -> MockOptions -> Dec -> Q [Dec]
mockDec mockType monadVarName varAppliedTypes options (SigD sigFnName ty) = do
  let ctx = buildMockFnContext mockType monadVarName varAppliedTypes options sigFnName ty
  fnDecs <- buildMockFnDeclarations ctx
  pragmaDec <- createNoInlinePragma (mockFnName ctx)
  pure $ pragmaDec : fnDecs
mockDec _ _ _ _ dec = fail $ "unsupport dec: " <> pprint dec



verifyExtension :: Extension -> Q ()
verifyExtension e = isExtEnabled e >>= flip unless (fail $ "Language extensions `" ++ show e ++ "` is required.")