packages feed

hhp-1.0.3: src/hhpi.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}

-- Commands:
--  check <file>
--  find <symbol>
--  info <file> <expr>
--  type <file> <line> <column>
--  lint [hlint options] <file>
--     the format of hlint options is [String] because they may contain
--     spaces and also <file> may contain spaces.
--  boot
--  browse [<package>:]<module>
--  quit
--
-- Session separators:
--   OK -- success
--   NG -- failure

module Main where

import Control.Concurrent (MVar, forkIO, newEmptyMVar, putMVar, readMVar)
import Control.Exception (Exception, SomeException (..))
import qualified Control.Exception as E
import Control.Monad (void, when)
import Data.Set (Set)
import qualified Data.Set as S
import Data.Version (showVersion)
import System.Console.GetOpt
import System.Directory (setCurrentDirectory)
import System.Environment (getArgs)
import System.IO (hFlush, stdout)

import Hhp
import Hhp.Ghc

-- import Hhp.Internal
import Paths_hhp

----------------------------------------------------------------

type Logger = IO String

----------------------------------------------------------------

progVersion :: String
progVersion =
    "hhpi version "
        ++ showVersion version
        ++ " compiled by GHC "
        ++ cProjectVersion
        ++ "\n"

argspec :: [OptDescr (Options -> Options)]
argspec =
    [ Option
        "b"
        ["boundary"]
        (ReqArg (\s opts -> opts{lineSeparator = LineSeparator s}) "sep")
        "specify line separator (default is Nul string)"
    , Option
        "l"
        ["tolisp"]
        (NoArg (\opts -> opts{outputStyle = LispStyle}))
        "print as a list of Lisp"
    , Option
        "g"
        []
        (ReqArg (\s opts -> opts{ghcOpts = s : ghcOpts opts}) "flag")
        "specify a ghc flag"
    ]

usage :: String
usage =
    progVersion
        ++ "Usage:\n"
        ++ "\t hhpi [-l] [-b sep] [-g flag]\n"
        ++ "\t hhpi version\n"
        ++ "\t hhpi help\n"

parseArgs :: [OptDescr (Options -> Options)] -> [String] -> (Options, [String])
parseArgs spec argv =
    case getOpt Permute spec argv of
        (o, n, []) -> (foldr id defaultOptions o, n)
        (_, _, errs) -> E.throw (CmdArg errs)

----------------------------------------------------------------

newtype HhpiError = CmdArg [String] deriving (Show)

instance Exception HhpiError

----------------------------------------------------------------

-- Running two GHC monad threads disables the handling of
-- C-c since installSignalHandlers is called twice, sigh.

main :: IO ()
main =
    E.handle cmdHandler $
        go =<< parseArgs argspec <$> getArgs
  where
    cmdHandler (CmdArg _) = putStr $ usageInfo usage argspec
    go (_, "help" : _) = putStr $ usageInfo usage argspec
    go (_, "version" : _) = putStr progVersion
    go (opt, _) = E.handle someHandler $ do
        cradle0 <- findCradle
        let rootdir = cradleRootDir cradle0
            cradle = cradle0{cradleCurrentDir = rootdir}
        setCurrentDirectory rootdir
        mvar <- liftIO newEmptyMVar
        mlibdir <- getSystemLibDir
        void $ forkIO $ setupDB cradle mlibdir opt mvar
        run cradle mlibdir opt $ loop opt S.empty mvar
      where
        -- this is just in case.
        -- If an error is caught here, it is a bug of Hhp library.
        someHandler (SomeException e) = do
            putStrLn $ "NG " ++ replace (show e)

replace :: String -> String
replace [] = []
replace ('\n' : xs) = ';' : replace xs
replace (x : xs) = x : replace xs

----------------------------------------------------------------

run :: Cradle -> Maybe FilePath -> Options -> Ghc a -> IO a
run cradle mlibdir opt body = runGhc mlibdir $ do
    initializeFlagsWithCradle opt cradle
    body

----------------------------------------------------------------

setupDB :: Cradle -> Maybe FilePath -> Options -> MVar SymMdlDb -> IO ()
setupDB cradle mlibdir opt mvar = E.handle handler $ do
    db <- run cradle mlibdir opt getSymMdlDb
    putMVar mvar db
  where
    handler (SomeException _) = return () -- fixme: put emptyDb?

----------------------------------------------------------------

loop :: Options -> Set FilePath -> MVar SymMdlDb -> Ghc ()
loop opt set mvar = do
    cmdArg <- liftIO getLine
    let (cmd, arg') = break (== ' ') cmdArg
        arg = dropWhile (== ' ') arg'
    (ret, ok, set') <- case cmd of
        "check" -> checkStx opt set arg
        "find" -> findSym opt set arg mvar
        "lint" -> lintStx opt set arg
        "info" -> showInfo opt set arg
        "type" -> showType opt set arg
        "boot" -> bootIt opt set
        "browse" -> browseIt opt set arg
        "quit" -> return ("quit", False, set)
        "" -> return ("quit", False, set)
        _ -> return ([], True, set)
    if ok
        then do
            liftIO $ putStr ret
            liftIO $ putStrLn "OK"
        else do
            liftIO $ putStrLn $ "NG " ++ replace ret
    liftIO $ hFlush stdout
    when ok $ loop opt set' mvar

----------------------------------------------------------------

checkStx
    :: Options
    -> Set FilePath
    -> FilePath
    -> Ghc (String, Bool, Set FilePath)
checkStx opt set file = do
    set' <- newFileSet set file
    let files = S.toList set'
    eret <- check opt files
    case eret of
        Right ret -> return (ret, True, set')
        Left ret -> return (ret, True, set) -- fxime: set

newFileSet :: Set FilePath -> FilePath -> Ghc (Set FilePath)
newFileSet set file = do
    let set1
            | S.member file set = set
            | otherwise = S.insert file set
    mf <- getMainFileToBeDeleted file
    return $ case mf of
        Nothing -> set1
        Just mainfile -> S.delete mainfile set1

----------------------------------------------------------------

findSym
    :: Options
    -> Set FilePath
    -> String
    -> MVar SymMdlDb
    -> Ghc (String, Bool, Set FilePath)
findSym opt set sym mvar = do
    db <- liftIO $ readMVar mvar
    let ret = lookupSym opt sym db
    return (ret, True, set)

lintStx
    :: Options
    -> Set FilePath
    -> FilePath
    -> Ghc (String, Bool, Set FilePath)
lintStx opt set optFile = liftIO $ do
    ret <- lintSyntax opt' file
    return (ret, True, set)
  where
    (opts, file) = parseLintOptions optFile
    hopts = if opts == "" then [] else read opts
    opt' = opt{hlintOpts = hopts}

-- |
-- >>> parseLintOptions "[\"--ignore=Use camelCase\", \"--ignore=Eta reduce\"] file name"
-- (["--ignore=Use camelCase", "--ignore=Eta reduce"], "file name")
-- >>> parseLintOptions "file name"
-- ([], "file name")
parseLintOptions :: String -> (String, String)
parseLintOptions optFile = case brk (== ']') (dropWhile (/= '[') optFile) of
    ("", "") -> ([], optFile)
    (opt', file') -> (opt', dropWhile (== ' ') file')
  where
    brk _ [] = ([], [])
    brk p (x : xs')
        | p x = ([x], xs')
        | otherwise = let (ys, zs) = brk p xs' in (x : ys, zs)

----------------------------------------------------------------

showInfo
    :: Options
    -> Set FilePath
    -> FilePath
    -> Ghc (String, Bool, Set FilePath)
showInfo opt set fileArg = do
    let (file, expr) = case words fileArg of
            [file0, expr0] -> (file0, expr0)
            _ -> error "showInfo"
    set' <- newFileSet set file
    ret <- info opt file expr
    return (ret, True, set')

showType
    :: Options
    -> Set FilePath
    -> FilePath
    -> Ghc (String, Bool, Set FilePath)
showType opt set fileArg = do
    let (file, line, column) = case words fileArg of
            [file0, line0, column0] -> (file0, line0, column0)
            _ -> error "showInfo"
    set' <- newFileSet set file
    ret <- types opt file (read line) (read column)
    return (ret, True, set')

----------------------------------------------------------------

bootIt
    :: Options
    -> Set FilePath
    -> Ghc (String, Bool, Set FilePath)
bootIt opt set = do
    ret <- boot opt
    return (ret, True, set)

browseIt
    :: Options
    -> Set FilePath
    -> ModuleString
    -> Ghc (String, Bool, Set FilePath)
browseIt opt set mdl = do
    ret <- browse opt mdl
    return (ret, True, set)