apply-refact-0.8.2.1: src/Refact/Run.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE ViewPatterns #-}
module Refact.Run where
import Language.Haskell.GHC.ExactPrint
import qualified Language.Haskell.GHC.ExactPrint.Parsers as EP
( defaultCppOptions
, ghcWrapper
, initDynFlags
, parseModuleApiAnnsWithCppInternal
, postParseTransform
)
import Language.Haskell.GHC.ExactPrint.Utils
import qualified Refact.Types as R
import Refact.Types hiding (SrcSpan)
import Refact.Apply
import Refact.Fixity
import Refact.Internal (apply)
import DynFlags
import HeaderInfo (getOptions)
import HscTypes (handleSourceError)
import qualified GHC (setSessionDynFlags, ParsedSource)
import Panic (handleGhcException)
import SrcLoc
import StringBuffer (stringToStringBuffer)
import GHC.LanguageExtensions.Type (Extension(..))
import Control.Monad
import Control.Monad.IO.Class (MonadIO(..))
import Data.Char
import Data.List hiding (find)
import qualified Data.List as List
import Data.Maybe
import Data.Version
import Options.Applicative
import System.IO.Extra
import System.FilePath.Find
import System.Exit
import qualified System.PosixCompat.Files as F
import Text.Read
import Paths_apply_refact
import Debug.Trace
#if __GLASGOW_HASKELL__ <= 806
type MonadFail = Monad
#endif
refactMain :: IO ()
refactMain = do
o@Options{..} <- execParser optionsWithHelp
when optionsVersion (putStr ("v" ++ showVersion version) >> exitSuccess)
unless (isJust optionsTarget || isJust optionsRefactFile)
(error "Must specify either the target file or the refact file")
case optionsTarget of
Nothing ->
withTempFile $ \fp -> do
getContents >>= writeFileUTF8 fp
runPipe o fp
Just target -> do
targetStatus <- F.getFileStatus target
if F.isDirectory targetStatus
then findHsFiles target >>= mapM_ (runPipe o)
else runPipe o target
parseVerbosity :: Monad m => String -> m Verbosity
parseVerbosity = pure . \case
"0" -> Silent
"1" -> Normal
"2" -> Loud
_ -> Normal
parsePos :: MonadFail m => String -> m (Int, Int)
parsePos s =
case span isDigit s of
(line, ',':col) ->
case (,) <$> readMaybe line <*> readMaybe col of
Just l -> pure l
Nothing -> fail "Invalid input"
_ -> fail "Invalid input"
data Target = StdIn | File FilePath
data Options = Options
{ optionsTarget :: Maybe FilePath -- ^ Where to process hints
, optionsRefactFile :: Maybe FilePath -- ^ The refactorings to process
, optionsInplace :: Bool
, optionsOutput :: Maybe FilePath -- ^ Whether to overwrite the file inplace
, optionsVerbosity :: Verbosity
, optionsStep :: Bool -- ^ Ask before applying each hint
, optionsDebug :: Bool
, optionsRoundtrip :: Bool
, optionsVersion :: Bool
, optionsLanguage :: [String]
, optionsPos :: Maybe (Int, Int)
}
options :: Parser Options
options = do
optionsTarget <- optional (argument str (metavar "TARGET"))
optionsRefactFile <- option (Just <$> str) $ mconcat
[ long "refact-file"
, value Nothing
, help "A file which specifies which refactorings to perform"
]
optionsInplace <- switch $ mconcat
[ long "inplace"
, short 'i'
, help "Whether to overwrite the target inplace"
]
optionsOutput <- optional . strOption $ mconcat
[ long "output"
, short 'o'
, help "Name of the file to output to"
, metavar "FILE"
]
optionsVerbosity <- option (str >>= parseVerbosity) $ mconcat
[ long "verbosity"
, short 'v'
, value Normal
, help "Specify verbosity, 0, 1 or 2. The default is 1 and 0 is silent."
]
optionsStep <- switch $ mconcat
[ short 's'
, long "step"
, help "Ask before applying each refactoring"
]
optionsDebug <- switch $ mconcat
[ long "debug"
, help "Output the GHC AST for debugging"
, internal
]
optionsRoundtrip <- switch $ mconcat
[ long "roundtrip"
, help "Run ghc-exactprint on the file"
, internal
]
optionsVersion <- switch $ mconcat
[ long "version"
, help "Display version number"
]
optionsLanguage <- many . strOption $ mconcat
[ long "language"
, short 'X'
, help "Language extensions (e.g. LambdaCase, RankNTypes)"
, metavar "Extensions"
]
optionsPos <- option (Just <$> (str >>= parsePos)) $ mconcat
[ long "pos"
, value Nothing
, metavar "<line>,<col>"
, help "Apply hints relevant to a specific position"
]
pure Options{..}
optionsWithHelp :: ParserInfo Options
optionsWithHelp = info (helper <*> options) $ mconcat
[ fullDesc
, progDesc "Automatically perform refactorings on haskell source files"
, header "refactor"
]
-- Given base directory finds all haskell source files
findHsFiles :: FilePath -> IO [FilePath]
findHsFiles = find filterDirectory filterFilename
filterDirectory :: FindClause Bool
filterDirectory =
p <$> fileName
where
p x
| "." `isPrefixOf` x = False
| otherwise = True
filterFilename :: FindClause Bool
filterFilename = do
ext <- extension
fname <- fileName
pure (ext == ".hs" && p fname)
where
p x
| "Setup.hs" `isInfixOf` x = False
| otherwise = True
-- | Parse the input into a list of enabled extensions and a list of disabled extensions.
parseExtensions :: [String] -> ([Extension], [Extension])
parseExtensions = foldl' f ([], [])
where
f :: ([Extension], [Extension]) -> String -> ([Extension], [Extension])
f (ys, ns) ('N' : 'o' : s) | Just ext <- readExtension s =
(delete ext ys, ext : delete ext ns)
f (ys, ns) s | Just ext <- readExtension s =
(ext : delete ext ys, delete ext ns)
-- ignore unknown extensions
f (ys, ns) _ = (ys, ns)
readExtension :: String -> Maybe Extension
readExtension s = flagSpecFlag <$> List.find ((== s) . flagSpecName) xFlags
addExtensionsToFlags
:: [Extension] -> [Extension] -> FilePath -> DynFlags
-> IO (Either String DynFlags)
addExtensionsToFlags es ds fp flags = catchErrors $ do
(stringToStringBuffer -> buf) <- readFileUTF8' fp
let opts = getOptions flags buf fp
withExts = flip (foldl' xopt_unset) ds
. flip (foldl' xopt_set) es
$ flags
(withPragmas, _, _) <- parseDynamicFilePragma withExts opts
pure . Right $ withPragmas `gopt_set` Opt_KeepRawTokenStream
where
catchErrors = handleGhcException (pure . Left . show)
. handleSourceError (pure . Left . show)
parseModuleWithArgs :: [String] -> FilePath -> IO (Either Errors (Anns, GHC.ParsedSource))
parseModuleWithArgs exts fp = EP.ghcWrapper $ do
let (es, ds) = parseExtensions exts
initFlags <- EP.initDynFlags fp
eflags <- liftIO $ addExtensionsToFlags es ds fp initFlags
case eflags of
-- TODO: report error properly.
Left err -> pure . Left $ mkErr initFlags (UnhelpfulSpan mempty) err
Right flags -> do
_ <- GHC.setSessionDynFlags flags
res <- EP.parseModuleApiAnnsWithCppInternal EP.defaultCppOptions flags fp
return $ EP.postParseTransform res rigidLayout
runPipe :: Options -> FilePath -> IO ()
runPipe Options{..} file = do
let verb = optionsVerbosity
rawhints <- getHints optionsRefactFile
when (verb == Loud) (traceM "Got raw hints")
let inp :: [(String, [Refactoring R.SrcSpan])] = read rawhints
n = length inp
when (verb == Loud) (traceM $ "Read " ++ show n ++ " hints")
output <- if null inp then readFileUTF8' file else do
when (verb == Loud) (traceM "Parsing module")
(as, m) <- either (onError "runPipe") (uncurry applyFixities)
<$> parseModuleWithArgs optionsLanguage file
when optionsDebug (putStrLn (showAnnData as 0 m))
apply optionsPos optionsStep inp file verb as m
if optionsInplace && isJust optionsTarget
then writeFileUTF8 file output
else case optionsOutput of
Nothing -> putStr output
Just f -> do
when (verb == Loud) (traceM $ "Writing result to " ++ f)
writeFileUTF8 f output
getHints :: Maybe FilePath -> IO String
getHints (Just hintFile) = readFileUTF8' hintFile
getHints Nothing = getContents