packages feed

mockcat-1.3.3.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,
    deriveMockInstances,
    deriveNoopInstance,
  )
where

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

import Language.Haskell.TH
  ( Clause (..),
    Body (..),
    Cxt,
    Dec (..),
    Exp (..),
    Extension (..),
    FunDep,
    Info (..),
    Lit (..),
    Name,
    Pat (..),
    Pred,
    Q,
    TyVarBndr (..),
    TySynEqn (..),
    TypeFamilyHead (..),
    Type (..),
    isExtEnabled,
    mkName,
    newName,
    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,
    getReturnType
  )
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 hiding (when)
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.")

deriveMockInstances :: Q Type -> Q [Dec]
deriveMockInstances qType = do
  ty <- qType
  let className = getClassName ty
  classMetadata <- loadClassMetadata className
  monadVarName <- selectMonadVarName classMetadata
  let classParamNames = filter (className /=) (getClassNames ty)
      newTypeVars = drop (length classParamNames) (cmTypeVars classMetadata)
  let sigDecs = [dec | dec@(SigD _ _) <- cmDecs classMetadata]
  
  let isSupportedDec (SigD _ _) = True
      isSupportedDec (PragmaD _) = True
      isSupportedDec (OpenTypeFamilyD _) = True
      isSupportedDec _ = False
  let unsupportedDecs = filter (not . isSupportedDec) (cmDecs classMetadata)
  
  instanceBodyDecsResult <- 
    case unsupportedDecs of
      (x:_) -> pure $ Left $ "deriveMockInstances: Unsupported declaration in class: " <> pprint x <> 
                        ". This error is reported at the usage site, but the cause is the macro definition for `" <> show className <> "`."
      [] -> do
        let typeFamilyHeads = [head | OpenTypeFamilyD head <- cmDecs classMetadata]
        typeInstDecs <- sequence <$> mapM (\h -> Right <$> createTypeInstanceDec monadVarName h) typeFamilyHeads
        sigInstDecs <- sequence <$> mapM (createLiftInstanceFnDec monadVarName) sigDecs
        case (typeInstDecs, sigInstDecs) of
          (Right tDecs, Right sDecs) -> pure $ Right (tDecs ++ sDecs)
          (Left err, _) -> pure $ Left err
          (_, Left err) -> pure $ Left err

  case instanceBodyDecsResult of
    Right decs -> do
      instanceHead <- createInstanceType ty monadVarName newTypeVars
      let instanceConstraint = foldl AppT ty (map (VarT . getTypeVarName) newTypeVars)
      instanceDec <- instanceD
        (pure [instanceConstraint])
        (pure instanceHead)
        (map pure decs)
      pure [instanceDec]
    Left err -> fail err

createLiftInstanceFnDec :: Name -> Dec -> Q (Either String Dec)
createLiftInstanceFnDec _ (SigD fnName ty) = do
  let n = countArgs ty
  argNames <- replicateM n (newName "a")
  let params = map VarP argNames
      args = map VarE argNames
      body = NormalB $ AppE (VarE 'lift) (foldl AppE (VarE fnName) args)
  pure $ Right $ FunD fnName [Clause params body []]
createLiftInstanceFnDec _ dec = pure $ Left $ 
  "deriveMockInstances: Unsupported declaration in class: " <> pprint dec <> 
  ". Currently only standard method signatures are supported for automatic derivation."

deriveNoopInstance :: Q Type -> Q [Dec]
deriveNoopInstance qType = do
  ty <- qType
  let className = getClassName ty
  classMetadata <- loadClassMetadata className
  monadVarName <- selectMonadVarName classMetadata
  let classParamNames = filter (className /=) (getClassNames ty)
      newTypeVars = drop (length classParamNames) (cmTypeVars classMetadata)
  let sigDecs = [dec | dec@(SigD _ _) <- cmDecs classMetadata]
  instanceBodyDecs <- mapM createNoopInstanceFnDec sigDecs
  case sequence instanceBodyDecs of
    Right decs -> do
      instanceHead <- createInstanceType ty monadVarName newTypeVars
      instanceDec <- instanceD
        (pure (cmContext classMetadata))
        (pure instanceHead)
        (map pure decs)
      pure [instanceDec]
    Left err -> fail err

createNoopInstanceFnDec :: Dec -> Q (Either String Dec)
createNoopInstanceFnDec (SigD fnName ty) = do
  let n = countArgs ty
  let returnType = getReturnType ty
  case returnType of
    AppT _ (TupleT 0) -> do
      let params = replicate n WildP
          body = NormalB $ AppE (VarE 'pure) (ConE '())
      pure $ Right $ FunD fnName [Clause params body []]
    _ -> pure $ Left $ 
      "deriveNoopInstance: Function `" <> nameBase fnName <> "` does not return `m ()` (actual return type: " <> pprint returnType <> "). " <> 
      "`deriveNoopInstance` only supports functions that return `m ()`. " <> 
      "Please implement this instance manually or exclude this function from the derivation target."
createNoopInstanceFnDec dec = pure $ Left $ 
  "deriveNoopInstance: Unsupported declaration in class: " <> pprint dec <> 
  ". Currently only standard method signatures are supported for automatic derivation."

countArgs :: Type -> Int
countArgs (AppT (AppT ArrowT _) t) = 1 + countArgs t
countArgs (ForallT _ _ t) = countArgs t
countArgs (SigT t _) = countArgs t
countArgs (ParensT t) = countArgs t
countArgs _ = 0