{-# LANGUAGE CPP #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module Main where
import Boilerplate.ConfigParser
import Boilerplate.Doc
import Boilerplate.GhcParser
import Boilerplate.Interpreter
import Boilerplate.RuleFinder
import Boilerplate.RuleParser
import Boilerplate.Types
import qualified Config as GHC
import Control.Monad (when)
import Data.IORef (modifyIORef')
import Data.IORef (readIORef)
import Data.IORef (newIORef)
import Data.List (isPrefixOf)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (mapMaybe)
import Data.Maybe (catMaybes)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import HsInspect.Types (Comment(..), Pos(..), Type(..))
import System.Environment (getArgs)
import System.Exit (ExitCode(..), exitWith)
import System.IO (stderr)
import System.IO (hPutStrLn)
import Text.Parsec (parse, setPosition)
import Text.Parsec.Pos (newPos)
import Text.Parsec.Text (parseFromFile)
version :: String
#ifdef CURRENT_PACKAGE_VERSION
version = CURRENT_PACKAGE_VERSION
#else
version = "unknown"
#endif
help :: String
help =
"boilerplate [GLOBAL FLAGS] FILE\n\n" ++
"Global flags:\n\n" ++
" -h,--help Show this help text\n" ++
" -v,--version Print version information\n" ++
" --ghc-version Print ghc version information\n" ++
" -i,--inplace Overwrite FILE in place\n" ++
" --verbose Print debugging information to stderr\n" ++
" --check-rule Parse FILE as a rule file and display errors\n"
-- TODO automatic imports
--
-- rules could declare imports they probably need quite easily, but figuring out
-- if an import is necessary (especially considering reexports) and making a
-- diff to the import section is very difficult and probably warrants its own
-- tool. Nevertheless, this would be an excellent feature.
main :: IO ()
main = do
args <- getArgs
let hasAny :: Eq a => [a] -> [a] -> Bool
hasAny [] _ = False
hasAny _ [] = False
hasAny as (x : xs) | elem x as = True
| otherwise = hasAny as xs
when (hasAny ["-h", "--help"] args) $
(putStrLn help) >> exitWith ExitSuccess
when (hasAny ["-v", "--version"] args) $
(putStrLn version) >> exitWith ExitSuccess
when (elem "--ghc-version" args) $
(putStrLn GHC.cProjectVersion) >> exitWith ExitSuccess
let exit1 msg = (hPutStrLn stderr msg) >> (exitWith $ ExitFailure 1)
when (null args) $ exit1 "missing arguments"
let inplace = hasAny ["-i", "--inplace"] args
file = last $ filter (not . isPrefixOf "-") args
verbose = elem "--verbose" args
-- TODO all parsec errors should be formatted as path/to/file.hs:line:col:
when (elem "--check-rule" args) $ do
parsed <- parseFromFile ruleParser file
case parsed of
Left err -> exit1 $ show err
Right _ -> exitWith ExitSuccess
content <- T.readFile file
-- early exit when there is no work to do
when (not $ T.isInfixOf "BOILERPLATE" content) $
if inplace
then exitWith ExitSuccess
else (T.putStr content) >> exitWith ExitSuccess
(types, comments) <- parseHaskell file
let parseComment (Comment txt (Pos line col) end) =
case parse (setPosition (newPos file line col) *> configCommentParser) file txt of
Left msg -> do
when (verbose && T.isInfixOf "BOILERPLATE" txt) $ do
hPutStrLn stderr $ show msg
pure $ Nothing
Right c -> pure $ Just (c, end)
configs <- catMaybes <$> traverse parseComment comments
let targets = flip mapMaybe (threes configs) $ \case
(One ((Config t r c), s)) -> Just $ Action t r c s Nothing
(Two ((Config t r c), s) _) -> Just $ Action t r c s Nothing
-- the ConfigStart is irrelevant for position detection, it is
-- to make it easier for text editors to hide expansions.
(Three ((Config t r c), s) (ConfigStart, _) (ConfigEnd, e)) -> Just $ Action t r c s (Just e)
(Three ((Config t r c), s) _ _) -> Just $ Action t r c s Nothing
_ -> Nothing
ruleFiles <- findRules file
when verbose $ do
hPutStrLn stderr $ show types
hPutStrLn stderr $ show targets
hPutStrLn stderr $ show ruleFiles
rulesCache <- newIORef M.empty
let
findRule :: Text -> IO Rule
findRule rulename = do
let pickRule (fqn, short, path) =
if fqn == rulename || short == rulename
then Just path
else Nothing
case mapMaybe pickRule ruleFiles of
[] -> exit1 "no rules"
[ruleFile] -> do
cached <- readIORef rulesCache
case M.lookup ruleFile cached of
Nothing -> do
parsed <- parseFromFile ruleParser ruleFile
case parsed of
Left err -> exit1 $ show err
Right r -> r <$ modifyIORef' rulesCache (M.insert ruleFile r)
Just hit -> do
when verbose $
hPutStrLn stderr $ "hit cache for " <> ruleFile
pure hit
many -> exit1 $ "ambiguous rules" <> show many
tpes :: Map Text Type
tpes = M.fromList $ (\t -> (tycon t, t)) <$> types
where
tycon = \case
ProductType tc _ _ _ _ -> tc
RecordType tc _ _ _ _ -> tc
SumType tc _ _ -> tc
resolve (Action tycon rulenames custom start end) = do
tpe <- case M.lookup tycon tpes of
Nothing -> exit1 $ "could not find the type definition for " <> show tycon
Just a -> pure a
rules' <- traverse findRule rulenames
pure $ Action tpe rules' custom start end
resolved <- traverse resolve targets
when verbose $ do
hPutStrLn stderr $ show resolved
let
interpret (Action tpe rules cus start end) =
(\txt -> (start, end, prepare $ T.unlines txt)) <$>
(traverse (\r -> interpretRule r tpe (M.fromList cus)) rules)
interpreted = traverse interpret resolved
prepare snippet = start_comment <> T.strip snippet <> end_comment
start_comment = "\n{- BOILERPLATE START -}\n"
end_comment = "\n{- BOILERPLATE END -}"
replacement <- unDoc <$> case interpreted of
Left err -> exit1 $ T.unpack err
Right good -> pure $ upsertMany (mkDoc content) good
if inplace
then T.writeFile file replacement
else T.putStr replacement
data Three a = One a | Two a a | Three a a a
threes :: [a] -> [Three a]
threes = \case
[] -> []
[a] -> [One a]
a1 : a2 : [] -> Two a1 a2 : (threes [a2])
a1 : a2 : a3 : as -> Three a1 a2 a3 : (threes $ a2 : a3 : as)
type Target = Action Text Text
type Resolved = Action Type Rule
data Action tpe rule = Action tpe [rule] [(Text, Custom)] Pos (Maybe Pos)
deriving (Eq, Show)