{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeApplications #-} module Clippy ( plugin ) where import Bag import Control.Exception (SomeException, tryJust) import Data.Bifunctor import Data.Function import Data.IORef import Data.String (fromString) import qualified Data.Text as T import Data.Text.ICU (regex) import Data.Text.ICU.Replace (replaceAll) import Dhall import ErrUtils import GhcPlugins hiding (Rule, (<>)) import Prelude hiding (print) import TcPluginM import TcRnTypes plugin :: Plugin plugin = defaultPlugin { tcPlugin = const $ Just $ TcPlugin { tcPluginInit = pure () , tcPluginSolve = \_ _ _ _ -> pure $ TcPluginOk [] [] , tcPluginStop = const $ loadConfig >>= either cantInitializeWarning replaceMessages } , pluginRecompile = purePlugin } newtype Config = Config { rules :: [Rule] } deriving Generic instance FromDhall Config data Rule = Rule { match :: Text , print :: Text } deriving Generic instance FromDhall Rule data PEnv = PEnv { showMsgDoc :: MsgDoc -> Text , config :: Config } loadConfig :: TcPluginM (Either String Config) loadConfig = tcPluginIO . tryJust (Just . show @SomeException) $ inputFile auto "./.clippy.dhall" cantInitializeWarning :: String -> TcPluginM () cantInitializeWarning cause = do dynFlags <- hsc_dflags <$> getTopEnv let warning = mkPlainWarnMsg dynFlags _span msgDoc _span = mkGeneralSrcSpan $ mkFastString "ghc-clippy-plugin" msgDoc = text "Clippy plugin couldn't start. Cause:" $$ text cause tcPluginIO $ printOrThrowWarnings dynFlags $ unitBag warning replaceMessages :: Config -> TcPluginM () replaceMessages conf = do errsRef <- tcl_errs . snd <$> getEnvs dynFlags <- hsc_dflags <$> getTopEnv let _showMsgDoc = T.pack . showSDoc dynFlags replaceErrMsgs = fmap $ replaceErrMsgDoc $ PEnv _showMsgDoc conf tcPluginIO $ modifyIORef errsRef (bimap replaceErrMsgs replaceErrMsgs) replaceErrMsgDoc :: PEnv -> ErrMsg -> ErrMsg replaceErrMsgDoc env e = e { errMsgDoc = replaceMsgDocs env (errMsgDoc e) } replaceMsgDocs :: PEnv -> ErrDoc -> ErrDoc replaceMsgDocs env e = e { errDocImportant = replaceMsgDocsGroup env "I" (errDocImportant e) , errDocContext = replaceMsgDocsGroup env "C" (errDocContext e) , errDocSupplementary = replaceMsgDocsGroup env "S" (errDocSupplementary e) } replaceMsgDocsGroup :: PEnv -> Text -> [MsgDoc] -> [MsgDoc] replaceMsgDocsGroup env label msgDocs = text . T.unpack <$> filtered where filtered = filter (not . T.null . T.strip) (T.lines replaced) replaced = replaceText env wrapped wrapped = (env & showMsgDoc) . vcat $ wrapGroup label msgDocs replaceText :: PEnv -> Text -> Text replaceText env t = foldr replaceRule t (rules . config $ env) replaceRule :: Rule -> Text -> Text replaceRule rule = replaceAll (regex [] $ rule & match) (fromString . T.unpack $ rule & print) wrapGroup :: Text -> [MsgDoc] -> [MsgDoc] wrapGroup label group = [open $ ">" <> label] ++ (wrapDoc label <$> group) ++ [close $ label <> "<"] wrapDoc :: Text -> MsgDoc -> MsgDoc wrapDoc label doc = vcat [open label, doc, close label] open :: Text -> MsgDoc open label = text . T.unpack $ ">" <> label <> ">" close :: Text -> MsgDoc close label = text . T.unpack $ "<" <> label <> "<"