pinned-warnings-0.1.0.3: src/PinnedWarnings.hs
{-# LANGUAGE OverloadedStrings #-}
module PinnedWarnings
( plugin
) where
import Control.Concurrent.MVar
import Control.Monad
import Control.Monad.IO.Class
import Data.IORef
import Data.List
import Data.Monoid (Alt(..))
import qualified Data.Map.Strict as M
import Data.Maybe
import qualified Data.Set as S
import Data.String (fromString)
import qualified System.Directory as Dir
import System.IO.Unsafe (unsafePerformIO)
import qualified Internal.FixWarnings as FW
import qualified Internal.GhcFacade as Ghc
import Internal.Types
-- The infamous mutable global trick.
-- Needed to track the pinned warnings during and after compilation.
globalState :: MVar (M.Map ModuleFile WarningsWithModDate)
globalState = unsafePerformIO $ newMVar mempty
{-# NOINLINE globalState #-}
--------------------------------------------------------------------------------
-- Plugin
--------------------------------------------------------------------------------
plugin :: Ghc.Plugin
plugin =
Ghc.defaultPlugin
{ Ghc.tcPlugin = const $ Just tcPlugin
, Ghc.parsedResultAction = const resetPinnedWarnsForMod
, Ghc.dynflagsPlugin = const addWarningCapture
, Ghc.pluginRecompile = Ghc.purePlugin
}
tcPlugin :: Ghc.TcPlugin
tcPlugin =
Ghc.TcPlugin
{ Ghc.tcPluginInit = initTcPlugin
, Ghc.tcPluginSolve = \(sw, fw, counterRef) _ _ wanteds ->
checkWanteds sw fw counterRef wanteds
, Ghc.tcPluginStop = const $ pure ()
}
initTcPlugin :: Ghc.TcPluginM (Ghc.TyCon, Ghc.TyCon, IORef Int)
initTcPlugin =
(,,) <$> lookupShowWarnings
<*> lookupFixWarnings
<*> Ghc.tcPluginIO (newIORef 0)
-- | Gets a reference to the 'ShowWarnings' constraint
lookupShowWarnings :: Ghc.TcPluginM Ghc.TyCon
lookupShowWarnings = do
result <- Ghc.findImportedModule
(Ghc.mkModuleName "ShowWarnings")
(Just "pinned-warnings")
case result of
Ghc.Found _ mod' -> do
name <- Ghc.lookupOrig mod' $ Ghc.mkTcOcc "ShowWarnings"
Ghc.classTyCon <$> Ghc.tcLookupClass name
_ -> error "ShowWarnings module not found"
-- | Gets a reference to the 'FixWarnings' constraint
lookupFixWarnings :: Ghc.TcPluginM Ghc.TyCon
lookupFixWarnings = do
result <- Ghc.findImportedModule
(Ghc.mkModuleName "ShowWarnings")
(Just "pinned-warnings")
case result of
Ghc.Found _ mod' -> do
name <- Ghc.lookupOrig mod' $ Ghc.mkTcOcc "FixWarnings"
Ghc.classTyCon <$> Ghc.tcLookupClass name
_ -> error "ShowWarnings module not found"
-- | If any wanted constraints are for 'ShowWarnings', then inject the pinned
-- warnings into GHC.
checkWanteds :: Ghc.TyCon
-> Ghc.TyCon
-> IORef Int
-> [Ghc.Ct]
-> Ghc.TcPluginM Ghc.TcPluginResult
checkWanteds sw fw counterRef
= fmap (flip Ghc.TcPluginOk [] . catMaybes)
. traverse go
where
go ct@Ghc.CDictCan { Ghc.cc_class = cls }
| Ghc.classTyCon cls == sw = do
counter <- Ghc.tcPluginIO $ readIORef counterRef
-- for some reason warnings only appear if they are added on
-- particular iterations.
when (counter == 2) addWarningsToContext
incrementCounter
pure $ Just (Ghc.EvExpr Ghc.unitExpr, ct)
| Ghc.classTyCon cls == fw = do
counter <- Ghc.tcPluginIO $ readIORef counterRef
when (counter == 0) (Ghc.tcPluginIO fixWarnings)
incrementCounter
pure $ Just (Ghc.EvExpr Ghc.unitExpr, ct)
go _ = pure Nothing
incrementCounter =
Ghc.tcPluginIO $ modifyIORef' counterRef succ
-- | Add warnings from the global state back into the GHC context
addWarningsToContext :: Ghc.TcPluginM ()
addWarningsToContext = do
errsRef <- Ghc.tcl_errs . snd <$> Ghc.getEnvs
Ghc.tcPluginIO pruneDeleted
pinnedWarns <- Ghc.listToBag . map unWarning
. foldMap (foldMap S.toList . snd)
<$> Ghc.tcPluginIO (readMVar globalState)
Ghc.tcPluginIO . atomicModifyIORef' errsRef
$ \(warnings, errors) ->
((Ghc.unionBags pinnedWarns warnings, errors), ())
-- | Remove warnings for modules that no longer exist
pruneDeleted :: IO ()
pruneDeleted = modifyMVar_ globalState $ \warns -> do
let mods = M.keys warns
deletedMods <-
filterM (fmap not . Dir.doesFileExist . Ghc.unpackFS)
mods
pure $ foldl' (flip M.delete) warns deletedMods
-- | Removes currently pinned warnings for a module and updates the timestamp.
-- This occurs before any new warnings are captured for the module.
resetPinnedWarnsForMod
:: Ghc.ModSummary
-> Ghc.HsParsedModule
-> Ghc.Hsc Ghc.HsParsedModule
resetPinnedWarnsForMod modSummary parsedModule = do
let modFile = fromString $ Ghc.ms_hspp_file modSummary
modifiedTime = Alt . Just $ Ghc.ms_hs_date modSummary
-- Replace any existing pinned warnings with new ones for this module
liftIO . modifyMVar_ globalState
$ pure . M.insert modFile (modifiedTime, mempty)
pure parsedModule
-- | Taps into the log action to capture the warnings that GHC emits.
addWarningCapture :: Ghc.DynFlags -> IO Ghc.DynFlags
addWarningCapture dynFlags = do
pure dynFlags
{ Ghc.log_action = Ghc.log_action' (Ghc.log_action dynFlags) $
\dyn severity srcSpan msgDoc -> do
case (severity, Ghc.srcSpanFileName_maybe srcSpan) of
(Ghc.SevWarning, Just modFile)
| Ghc.RealSrcLoc' start <- Ghc.srcSpanStart srcSpan
, Ghc.RealSrcLoc' end <- Ghc.srcSpanEnd srcSpan
-> do
let warn = M.singleton (start, end)
. S.singleton
. Warning
$ Ghc.mkWarnMsg dyn srcSpan Ghc.alwaysQualify msgDoc
modifyMVar_ globalState
$ pure
. M.insertWith (<>) modFile
(mempty, MonoidMap warn)
_ -> pure ()
}
fixWarnings :: IO ()
fixWarnings = do
pruneDeleted
modifyMVar_ globalState $ traverse FW.fixWarning