{-# LANGUAGE TupleSections #-} module Smuggler.Plugin ( plugin ) where import Control.Monad (guard, void) import Control.Monad.IO.Class (MonadIO (..)) import Data.ByteString (ByteString) import Data.List (foldl') import HashStore (hashStore) import Language.Haskell.GHC.ExactPrint (exactPrint) import System.FilePath ((-<.>)) import HscTypes (ModSummary (..)) import HsExtension (GhcRn) import HsImpExp (IE (..), IEWrappedName (..), ImportDecl (..), LIE, LIEWrappedName) import IOEnv (readMutVar) import Name (Name) import Plugins (CommandLineOption, Plugin (..), defaultPlugin) import PrelNames (pRELUDE_NAME) import RdrName (GlobalRdrElt) import RnNames (ImportDeclUsage, findImportUsage) import SrcLoc (GenLocated (..), SrcSpan (..), srcSpanEndLine, srcSpanStartCol, srcSpanStartLine, unLoc) import TcRnTypes (TcGblEnv (..), TcM) import Smuggler.Anns (removeAnnAtLoc) import Smuggler.Parser (runParser) import qualified Data.ByteString as BS plugin :: Plugin plugin = defaultPlugin { typeCheckResultAction = smugglerPlugin } cacheDir :: FilePath cacheDir = ".smuggler" munglePath :: FilePath -> FilePath munglePath = \case [] -> [] '/' : xs -> '.' : munglePath xs c : xs -> c : munglePath xs smugglerPlugin :: [CommandLineOption] -> ModSummary -> TcGblEnv -> TcM TcGblEnv smugglerPlugin clis modSummary tcEnv = do let modulePath = ms_hspp_file modSummary uses <- readMutVar (tcg_used_gres tcEnv) fileContent <- liftIO $ BS.readFile modulePath let modifiedName = munglePath modulePath liftIO $ void $ hashStore cacheDir (smuggling uses modulePath) (modifiedName, fileContent) pure tcEnv where smuggling :: [GlobalRdrElt] -> FilePath -> ByteString -> IO ByteString smuggling uses modulePath _ = do -- 1. Parse given file runParser modulePath >>= \case -- TODO: don't read file, use given ByteString Left () -> pure () -- do nothing if file is invalid Haskell Right (anns, ast) -> do -- 2. find positions of unused imports let user_imports = filter (not . ideclImplicit . unLoc) (tcg_rn_imports tcEnv) let usage = findImportUsage user_imports uses let unusedPositions = concatMap unusedLocs usage -- 3. Remove positions of unused imports from annotations. let purifiedAnnotations = foldl' (\ann (x, y) -> removeAnnAtLoc x y ann) anns unusedPositions let newContent = exactPrint ast purifiedAnnotations case clis of [] -> writeFile modulePath newContent (ext:_) -> writeFile (modulePath -<.> ext) newContent -- 4. Return empty ByteString pure "" unusedLocs ::ImportDeclUsage -> [(Int, Int)] unusedLocs (L (UnhelpfulSpan _) _, _, _) = [] unusedLocs (L (RealSrcSpan loc) decl, used, unused) -- Do not remove `import M ()` | Just (False, L _ []) <- ideclHiding decl = [] -- Note [Do not warn about Prelude hiding] -- TODO: add ability to support custom prelude | Just (True, L _ hides) <- ideclHiding decl , not (null hides) , pRELUDE_NAME == unLoc (ideclName decl) = [] -- Nothing used; drop entire decl -- TODO: optimize | null used = [ (lineNum, colNum) | lineNum <- [srcSpanStartLine loc .. srcSpanEndLine loc] , colNum <- [1..100] ] -- Everything imported is used; drop nothing | null unused = [] -- only part of non-hiding import is used | Just (False, L _ lies) <- ideclHiding decl = unusedLies lies -- TODO: unused hidings | otherwise = [] where unusedLies :: [LIE GhcRn] -> [(Int, Int)] unusedLies = concatMap lieToLoc lieToLoc :: LIE GhcRn -> [(Int, Int)] lieToLoc (L _ lie) = case lie of IEVar _ name -> lieNameToLoc name IEThingAbs _ name -> lieNameToLoc name IEThingAll _ name -> lieNameToLoc name IEThingWith _ name _ names _ -> concatMap lieNameToLoc (name : names) _ -> [] lieNameToLoc :: LIEWrappedName Name -> [(Int, Int)] lieNameToLoc lieName = do L _ (IEName (L (RealSrcSpan lieLoc) name)) <- [lieName] guard $ name `elem` unused pure (srcSpanStartLine lieLoc, srcSpanStartCol lieLoc)