{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections #-}
module Retrie.Options
(
Options
, Options_(..)
, ExecutionMode(..)
, defaultOptions
, parseOptions
, buildGrepChain
, forFn
, getOptionsParser
, getTargetFiles
, parseRewritesInternal
, parseVerbosity
, ProtoOptions
, resolveOptions
) where
import Control.Concurrent.Async (mapConcurrently)
import Control.Monad (when)
import Data.Bool
import Data.Char (isAlphaNum, isSpace)
import Data.Default as D
import Data.Foldable (toList)
import Data.Functor.Identity
import Data.List
import Data.HashSet (HashSet)
import qualified Data.HashSet as HashSet
import Data.Traversable
import Options.Applicative
import System.Directory
import System.FilePath
import System.Process
import System.Random.Shuffle
import Retrie.CPP
import Retrie.Debug
import Retrie.ExactPrint
import Retrie.Fixity
import Retrie.GroundTerms
import Retrie.GHC
import Retrie.Pretty
import Retrie.Rewrites
import Retrie.Types
import Retrie.Universe
import Retrie.Util
type Options = Options_ [Rewrite Universe] AnnotatedImports
parseOptions :: FixityEnv -> IO Options
parseOptions fixityEnv = do
p <- getOptionsParser fixityEnv
opts <- execParser (info (p <**> helper) fullDesc)
resolveOptions opts
parseRewritesInternal :: Options_ a b -> [RewriteSpec] -> IO [Rewrite Universe]
parseRewritesInternal Options{..} = parseRewriteSpecs parser fixityEnv
where
parser fp = parseCPPFile (parseContent fixityEnv) (targetDir </> fp)
data ExecutionMode
= ExecDryRun
| ExecRewrite
| ExecExtract
| ExecSearch
deriving (Show)
data Options_ rewrites imports = Options
{ additionalImports :: imports
, colorise :: ColoriseFun
, executionMode :: ExecutionMode
, extraIgnores :: [FilePath]
, fixityEnv :: FixityEnv
, iterateN :: Int
, randomOrder :: Bool
, rewrites :: rewrites
, roundtrips :: [RoundTrip]
, singleThreaded :: Bool
, targetDir :: FilePath
, targetFiles :: [FilePath]
, verbosity :: Verbosity
}
defaultOptions
:: (Default rewrites, Default imports)
=> FilePath -> Options_ rewrites imports
defaultOptions fp = Options
{ additionalImports = D.def
, colorise = noColor
, executionMode = ExecRewrite
, extraIgnores = []
, fixityEnv = mempty
, iterateN = 1
, randomOrder = False
, rewrites = D.def
, roundtrips = []
, singleThreaded = False
, targetDir = fp
, targetFiles = []
, verbosity = Normal
}
getOptionsParser :: FixityEnv -> IO (Parser ProtoOptions)
getOptionsParser fEnv = do
dOpts <- defaultOptions <$> getCurrentDirectory
return $ buildParser dOpts { fixityEnv = fEnv }
buildParser :: ProtoOptions -> Parser ProtoOptions
buildParser dOpts = do
singleThreaded <- switch $ mconcat
[ long "single-threaded"
, showDefault
, help "Don't try to parallelize things (for debugging)."
]
targetDir <- option str $ mconcat
[ long "target"
, short 't'
, metavar "PATH"
, action "directory"
, value (targetDir dOpts)
, showDefault
, help "Path to target with rewrites."
]
targetFiles <- many $ option str $ mconcat
[ long "target-file"
, metavar "PATH"
, action "file"
, help "Target specific file for rewriting."
]
verbosity <- parseVerbosity (verbosity dOpts)
additionalImports <- many $ option str $ mconcat
[ long "import"
, metavar "IMPORT"
, help
"Add given import statement to modules that are modified by a rewrite."
]
extraIgnores <- many $ option str $ mconcat
[ long "ignore"
, metavar "PATH"
, action "file"
, help "Ignore specific file while rewriting."
]
colorise <- fmap (bool noColor addColor) $ switch $ mconcat
[ long "color"
, help "Highlight matches with color."
]
randomOrder <- switch $ mconcat
[ long "random-order"
, help "Randomize the order of targeted modules."
]
iterateN <- option auto $ mconcat
[ long "iterate"
, short 'i'
, metavar "N"
, value 1
, help "Iterate rewrites up to N times."
]
executionMode <- parseMode
rewrites <- parseRewriteSpecOptions
roundtrips <- parseRoundtrips
return Options{ fixityEnv = fixityEnv dOpts, ..}
parseRewriteSpecOptions :: Parser [RewriteSpec]
parseRewriteSpecOptions = concat <$> traverse many
[ fmap Unfold $ option str $ mconcat
[ long "unfold"
, short 'u'
, metavar "NAME"
, help "Unfold given fully-qualified name."
]
, fmap Fold $ option str $ mconcat
[ long "fold"
, short 'f'
, metavar "NAME"
, help "Fold given fully-qualified name."
]
, fmap RuleForward $ option str $ mconcat
[ long "rule-forward"
, short 'l'
, metavar "NAME"
, help "Apply fully-qualified RULE name left-to-right."
]
, fmap RuleBackward $ option str $ mconcat
[ long "rule-backward"
, short 'r'
, metavar "NAME"
, help "Apply fully-qualified RULE name right-to-left."
]
, fmap TypeForward $ option str $ mconcat
[ long "type-forward"
, metavar "NAME"
, help "Apply fully-qualified type synonym name left-to-right."
]
, fmap TypeBackward $ option str $ mconcat
[ long "type-backward"
, metavar "NAME"
, help "Apply fully-qualified type synonym name right-to-left."
]
, fmap Adhoc $ option str $ mconcat
[ long "adhoc"
, metavar "EQUATION"
, help "Apply an adhoc equation of the form: forall vs. lhs = rhs"
]
]
parseMode :: Parser ExecutionMode
parseMode =
parseDryRun <|>
parseExtract <|>
parseSearch <|>
pure ExecRewrite
parseDryRun :: Parser ExecutionMode
parseDryRun = flag' ExecDryRun $ mconcat
[ long "dry-run"
, help "Don't overwrite files. Print rewrite results."
]
parseExtract :: Parser ExecutionMode
parseExtract = flag' ExecExtract $ mconcat
[ long "extract"
, help "Find the left-hand side, display the instantiated right-hand side."
]
parseSearch :: Parser ExecutionMode
parseSearch = flag' ExecSearch $ mconcat
[ long "search"
, help "Search for left-hand side of the rewrite and show matches."
]
parseVerbosity :: Verbosity -> Parser Verbosity
parseVerbosity defaultV = option (eitherReader verbosityReader) $ mconcat
[ long "verbosity"
, short 'v'
, value defaultV
, showDefault
, help verbosityHelp
]
verbosityReader :: String -> Either String Verbosity
verbosityReader "0" = Right Silent
verbosityReader "1" = Right Normal
verbosityReader "2" = Right Loud
verbosityReader _ =
Left $ "invalid verbosity. Valid values: " ++ verbosityHelp
verbosityHelp :: String
verbosityHelp = "0: silent, 1: normal, 2: loud (implies --single-threaded)"
type ProtoOptions = Options_ [RewriteSpec] [String]
resolveOptions :: ProtoOptions -> IO Options
resolveOptions protoOpts = do
absoluteTargetDir <- makeAbsolute (targetDir protoOpts)
opts@Options{..} <-
addLocalFixities protoOpts { targetDir = absoluteTargetDir }
parsedImports <- parseImports additionalImports
debugPrint verbosity "Imports:" $
runIdentity $ fmap astA $ transformA parsedImports $ \ imps -> do
anns <- getAnnsT
return $ map (`exactPrint` anns) imps
rrs <- parseRewritesInternal opts rewrites
return Options
{ additionalImports = parsedImports
, rewrites = rrs
, singleThreaded = singleThreaded || verbosity == Loud
, ..
}
addLocalFixities :: Options_ a b -> IO (Options_ a b)
addLocalFixities opts = do
let opts' = opts { targetFiles = [] }
files <- getTargetFiles opts' [HashSet.singleton "infix"]
fixFns <- forFn opts files $ \ fp -> do
ms <- toList <$> parseCPPFile parseContentNoFixity fp
return $ extendFixityEnv
[ (rdrFS nm, fixity)
| m <- ms
, (L _ nm, fixity) <- fixityDecls (unLoc (astA m))
]
return opts { fixityEnv = foldr ($) (fixityEnv opts) fixFns }
forFn :: Options_ x y -> [a] -> (a -> IO b) -> IO [b]
forFn Options{..} c f
| randomOrder = fn f =<< shuffleM c
| otherwise = fn f c
where
fn
| singleThreaded = mapM
| otherwise = mapConcurrently
getTargetFiles :: Options_ a b -> [GroundTerms] -> IO [FilePath]
getTargetFiles opts [] = getTargetFiles opts [mempty]
getTargetFiles Options{..} gtss = do
ignorePred <- maybe onIgnoreErr return =<< vcsIgnorePred verbosity targetDir
let ignore fp = ignorePred fp || extraIgnorePred fp
fpSets <- forM (dedup gtss) $ \ gts -> do
fps <-
case buildGrepChain targetDir gts targetFiles of
Left fs -> return fs
Right (stdin, cmd) -> doCmd targetDir verbosity stdin (unwords cmd)
let
r = filter (not . ignore)
$ map (normalise . (targetDir </>)) fps
debugPrint verbosity "Files:" r
return $ HashSet.fromList r
return $ HashSet.toList $ mconcat fpSets
where
dedup = HashSet.toList . HashSet.fromList
extraIgnorePred =
let fps = [ normalise (targetDir </> f) | f <- extraIgnores ]
in \fp -> any (`isPrefixOf` fp) fps
onIgnoreErr = do
when (verbosity > Silent) $
putStrLn "Reading VCS ignore failed! Continuing without ignoring."
return $ const False
buildGrepChain
:: FilePath
-> HashSet String
-> [FilePath]
-> Either [FilePath] (String, [String])
buildGrepChain targetDir gts =
filterFiles (take 10 $ filter p $ HashSet.toList gts)
where
p [] = False
p (c:cs)
| isSpace c = p cs
| otherwise = isAlphaNum c
hsExtension = "\"*.hs\""
filterFiles [] [] = Right ("", findCmd)
filterFiles [] fs = Left fs
filterFiles (g:gs) [] =
Right ("", intercalate ["|"] $ firstCmd g : filterChain gs)
filterFiles gs fs =
Right (unlines fs, intercalate ["|"] $ filterChain gs)
findCmd = ["find", addTrailingPathSeparator targetDir, "-iname", hsExtension]
firstCmd g =
["grep", "-R", "--include=" ++ hsExtension, "-l", esc g, targetDir]
filterChain gs = [ ["xargs", "grep", "-l", esc gt] | gt <- gs ]
esc s = "'" ++ intercalate "[[:space:]]\\+" (words s) ++ "'"
doCmd :: FilePath -> Verbosity -> String -> String -> IO [FilePath]
doCmd targetDir verbosity inp shellCmd = do
debugPrint verbosity "stdin:" [inp]
debugPrint verbosity "shellCmd:" [shellCmd]
let cmd = (shell shellCmd) { cwd = Just targetDir }
(_ec, fps, _) <- readCreateProcessWithExitCode cmd inp
return $ lines fps