packages feed

smuggler-0.0.0: src/Smuggler/Plugin.hs

{-# 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
                    []      -> putStrLn 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)