packages feed

boilerplate-0.0.2: 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 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)