packages feed

hiedb-plugin-0.1.0: src/Plugin/HieDb.hs

{-# LANGUAGE GADTs #-}

module Plugin.HieDb (plugin) where

import Control.Concurrent.STM
import Control.Exception
import Control.Monad
import Data.IORef
import GHC
import GHC.Driver.Hooks
import GHC.Driver.Pipeline
import GHC.Driver.Pipeline.Phases
import GHC.Plugins as Plugins
import GHC.Types.Name.Cache
import HieDb.Create
import HieDb.Types
import System.Directory (doesPathExist, makeAbsolute)
import System.FilePath
import qualified System.IO.Unsafe as Unsafe

plugin :: Plugin
plugin =
  defaultPlugin
    { pluginRecompile = Plugins.purePlugin
    , driverPlugin = driver
    }

driver :: [CommandLineOption] -> HscEnv -> IO HscEnv
driver _ hscEnv = do
  initializeHiedb
  pure
    hscEnv
      { hsc_hooks =
          (hsc_hooks hscEnv)
            { runPhaseHook = Just phaseHook
            }
      }
 where
  initializeHiedb = liftIO $ withHieDb defaultHiedbFile initConn

  -- We index using a phase hook instead of typeCheckResultAction since
  -- the hie file can be written after that plugin phase
  phaseHook =
    PhaseHook $ \phase -> do
      case phase of
        T_HscPostTc _ modSummary _ _ _ -> do
          let dynFlags = hsc_dflags hscEnv
              hieDirectory = hieDir dynFlags
          _ <-
            liftIO $
              mapM
                (addModuleToDb defaultHiedbFile $ ms_mod modSummary)
                hieDirectory
          runPhase phase
        _ -> runPhase phase

addModuleToDb :: FilePath -> Module -> FilePath -> IO ()
addModuleToDb hiedbFile mod' hieBaseDir = do
  let
    -- Note: For performance reasons we intentionally skip the type
    -- indexing phase
    -- TODO: pass this in as a user defined option
    skipOptions = defaultSkipOptions{skipTypes = True}
    modToPath = moduleNameSlashes . moduleName

  let hieFile = hieBaseDir </> modToPath mod' -<.> ".hie"

  absoluteHieFile <- makeAbsolute hieFile
  hieExists <- doesPathExist absoluteHieFile
  when hieExists $ do
    _ <- withDbLock $ do
      nc <- newIORef =<< initNameCache 'a' []
      _ <-
        withHieDb
          hiedbFile
          (\conn -> runDbM nc $ addRefsFrom conn (Just ".") skipOptions absoluteHieFile)
          -- TODO: report this and maybe make configurable in future versions
          `catch` (\(_ :: SomeException) -> pure False)
      pure ()
    pure ()
 where
  acquireDbLock =
    liftIO $ atomically $ takeTMVar dbLock
  releaseDbLock =
    liftIO $ atomically $ putTMVar dbLock ()
  -- Safely use a db lock - ensure the lock is released if an exception occurs
  withDbLock :: IO () -> IO ()
  withDbLock fn = do
    bracket_
      acquireDbLock
      releaseDbLock
      fn

defaultHiedbFile :: String
defaultHiedbFile = ".hiedb"

-----------------------------------------------------
-- Since we cant pass state along through the phases we use unsafePerformIO
-- to define global mutable state
-----------------------------------------------------

-- | We need to ensure only one thread writes to the db at once since sqlite
-- only maintains one WAL file and will throw an error on concurrent writes
dbLock :: TMVar ()
dbLock = Unsafe.unsafePerformIO $ newTMVarIO ()
{-# NOINLINE dbLock #-}