packages feed

boilerplate-0.0.1: exe/Main.hs

{-# 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 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"

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

  when (null args) $
    (putStrLn "missing arguments") >> (exitWith $ ExitFailure 1)

  let inplace = hasAny ["-i", "--inplace"] args
      file = last $ filter (not . isPrefixOf "-") args
      verbose = elem "--verbose" args

  -- FIXME a --check-rule flag that just parses a rule

  content <- T.readFile file

  -- TODO all parsec errors should be formatted as path/to/file.hs:line:col:

  -- 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
        [] -> error "no rules"
        [ruleFile] -> do
          cached <- readIORef rulesCache
          case M.lookup ruleFile cached of
            Nothing -> do
              parsed <- parseFromFile ruleParser ruleFile
              case parsed of
                Left err -> error $ 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 -> error $ "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 -> error $ "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 -> (putStrLn $ T.unpack err) >> (exitWith $ ExitFailure 1)
    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)