{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE PartialTypeSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE NoMonomorphismRestriction #-} module Refact.Run where import Language.Haskell.GHC.ExactPrint import Language.Haskell.GHC.ExactPrint.Print import Language.Haskell.GHC.ExactPrint.Parsers (parseModuleWithOptions) 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.Utils (toGhcSrcSpan, Module) import qualified SrcLoc as GHC import Options.Applicative import Data.Maybe import Data.Monoid ( (<>) ) import Control.Monad.Trans.Maybe import Data.List hiding (find) import System.IO import System.IO.Temp import System.FilePath.Find import System.Exit import qualified System.PosixCompat.Files as F import Control.Monad import Control.Monad.State import Control.Monad.Identity import Paths_apply_refact import Data.Version import Debug.Trace import SrcLoc import Text.Read import Data.Char 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 -> withSystemTempFile "stdin" (\fp hin -> do getContents >>= hPutStrLn hin >> hClose hin 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 s = return $ case s of "0" -> Silent "1" -> Normal "2" -> Loud _ -> Normal parsePos :: Monad m => String -> m (Int, Int) parsePos s = case span isDigit s of (line, ',':col) -> case (,) <$> readMaybe line <*> readMaybe col of Just l -> return 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 , optionsPos :: Maybe (Int, Int) } options :: Parser Options options = Options <$> optional (argument str (metavar "TARGET")) <*> option (Just <$> str) (long "refact-file" <> value Nothing <> help "A file which specifies which refactorings to perform") <*> switch (long "inplace" <> short 'i' <> help "Whether to overwrite the target inplace") <*> optional (strOption (long "output" <> short 'o' <> help "Name of the file to output to" <> metavar "FILE")) <*> option (str >>= parseVerbosity) ( long "verbosity" <> short 'v' <> value Normal <> help "Specify verbosity, 0, 1 or 2. The default is 1 and 0 is silent.") <*> switch (short 's' <> long "step" <> help "Ask before applying each refactoring") <*> switch (long "debug" <> help "Output the GHC AST for debugging" <> internal) <*> switch (long "roundtrip" <> help "Run ghc-exactprint on the file" <> internal) <*> switch (long "version" <> help "Display version number") <*> option (Just <$> (str >>= parsePos)) (long "pos" <> value Nothing <> metavar "," <> help "Apply hints relevant to a specific position") optionsWithHelp :: ParserInfo Options optionsWithHelp = info (helper <*> options) ( 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 return (ext == ".hs" && p fname) where p x | "Setup.hs" `isInfixOf` x = False | otherwise = True -- Pipe runPipe :: Options -> FilePath -> IO () runPipe Options{..} file = do let verb = optionsVerbosity when (verb == Loud) (traceM "Parsing module") (as, m) <- either (error . show) (uncurry applyFixities) <$> parseModuleWithOptions rigidLayout file when optionsDebug (putStrLn (showAnnData as 0 m)) 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") let noOverlapInp = removeOverlap verb inp refacts = (fmap . fmap . fmap) (toGhcSrcSpan file) <$> noOverlapInp posFilter (_, rs) = case optionsPos of Nothing -> True Just p -> any (flip spans p . pos) rs filtRefacts = filter posFilter refacts when (verb >= Normal) (traceM $ "Applying " ++ show (length (concatMap snd filtRefacts)) ++ " hints") when (verb == Loud) (traceM $ show filtRefacts) -- need a check here to avoid overlap (ares, res) <- if optionsStep then fromMaybe (as, m) <$> runMaybeT (refactoringLoop as m filtRefacts) else return . flip evalState 0 $ foldM (uncurry runRefactoring) (as, m) (concatMap snd filtRefacts) when (optionsDebug) (putStrLn (showAnnData ares 0 res)) let output = runIdentity $ exactPrintWithOptions refactOptions res ares if optionsInplace && isJust optionsTarget then writeFile file output else case optionsOutput of Nothing -> putStr output Just f -> do when (verb == Loud) (traceM $ "Writing result to " ++ f) writeFile f output data LoopOption = LoopOption { desc :: String , perform :: MaybeT IO (Anns, Module) } refactoringLoop :: Anns -> Module -> [(String, [Refactoring GHC.SrcSpan])] -> MaybeT IO (Anns, Module) refactoringLoop as m [] = return (as, m) refactoringLoop as m ((_, []): rs) = refactoringLoop as m rs refactoringLoop as m hints@((hintDesc, rs): rss) = do inp <- liftIO $ do putStrLn hintDesc putStrLn $ "Apply hint [" ++ intercalate ", " (map fst opts) ++ "]" -- In case that the input also comes from stdin withFile "/dev/tty" ReadMode hGetLine case lookup inp opts of Just opt -> perform opt Nothing -> loopHelp where opts = [ ("y", LoopOption "Apply current hint" yAction) , ("n", LoopOption "Don't apply the current hint" (refactoringLoop as m rss)) , ("q", LoopOption "Apply no further hints" (return (as, m))) , ("d", LoopOption "Discard previous changes" mzero ) , ("v", LoopOption "View current file" (liftIO (putStrLn (exactPrint m as)) >> refactoringLoop as m hints)) , ("?", LoopOption "Show this help menu" loopHelp)] loopHelp = do liftIO . putStrLn . unlines . map mkLine $ opts refactoringLoop as m hints mkLine (c, opt) = c ++ " - " ++ desc opt -- Force to force bottoms yAction = let (!r1, !r2) = flip evalState 0 $ foldM (uncurry runRefactoring) (as, m) rs in do exactPrint r2 r1 `seq` return () refactoringLoop r1 r2 rss getHints :: Maybe FilePath -> IO String getHints (Just hintFile) = readFile hintFile getHints Nothing = getContents