241 lines
11 KiB
Haskell
241 lines
11 KiB
Haskell
{-# LANGUAGE DataKinds #-}
|
|
|
|
module Main where
|
|
|
|
|
|
|
|
#include "prelude.inc"
|
|
|
|
import qualified Language.Haskell.GHC.ExactPrint as ExactPrint
|
|
import qualified Language.Haskell.GHC.ExactPrint.Annotate as ExactPrint.Annotate
|
|
import qualified Language.Haskell.GHC.ExactPrint.Types as ExactPrint.Types
|
|
import qualified Language.Haskell.GHC.ExactPrint.Parsers as ExactPrint.Parsers
|
|
import qualified Data.Map as Map
|
|
|
|
import qualified Data.Text.Lazy.Builder as Text.Builder
|
|
|
|
import Data.CZipWith
|
|
|
|
import qualified Debug.Trace as Trace
|
|
|
|
import Language.Haskell.Brittany.Internal.Types
|
|
import Language.Haskell.Brittany.Internal
|
|
import Language.Haskell.Brittany.Internal.Config
|
|
import Language.Haskell.Brittany.Internal.Config.Types
|
|
import Language.Haskell.Brittany.Internal.Utils
|
|
|
|
import qualified Text.PrettyPrint as PP
|
|
|
|
import DataTreePrint
|
|
import UI.Butcher.Monadic
|
|
|
|
import qualified System.Exit
|
|
import qualified System.Directory as Directory
|
|
import qualified System.FilePath.Posix as FilePath
|
|
|
|
import qualified DynFlags as GHC
|
|
import qualified GHC.LanguageExtensions.Type as GHC
|
|
|
|
import Paths_brittany
|
|
|
|
|
|
|
|
main :: IO ()
|
|
main = mainFromCmdParserWithHelpDesc mainCmdParser
|
|
|
|
mainCmdParser :: CommandDesc () -> CmdParser Identity (IO ()) ()
|
|
mainCmdParser helpDesc = do
|
|
addCmdSynopsis "haskell source pretty printer"
|
|
addCmdHelp $ PP.vcat $ List.intersperse
|
|
(PP.text "")
|
|
[ parDoc
|
|
$ "Transforms one haskell module by reformatting"
|
|
++ " (parts of) the source code (while preserving the"
|
|
++ " parts not transformed)."
|
|
, parDoc $ "Based on ghc-exactprint, thus (theoretically) supporting all" ++ " that ghc does."
|
|
, parDoc
|
|
$ "This is an early, experimental release."
|
|
++ " Only type-signatures and function-bindings are transformed."
|
|
++ " There is a check in place, but no warranties that the output"
|
|
++ " is valid haskell."
|
|
, parDoc $ "There is NO WARRANTY, to the extent permitted by law."
|
|
, parDoc $ "See https://github.com/lspitzner/brittany"
|
|
, parDoc $ "Please report bugs at" ++ " https://github.com/lspitzner/brittany/issues"
|
|
]
|
|
-- addCmd "debugArgs" $ do
|
|
addHelpCommand helpDesc
|
|
-- addButcherDebugCommand
|
|
reorderStart
|
|
printHelp <- addSimpleBoolFlag "" ["help"] mempty
|
|
printVersion <- addSimpleBoolFlag "" ["version"] mempty
|
|
inputPaths <- addFlagStringParams "i" ["input"] "PATH" (flagHelpStr "path to input haskell source file")
|
|
outputPaths <- addFlagStringParams "o" ["output"] "PATH" (flagHelpStr "output file path")
|
|
configPaths <- addFlagStringParams "" ["config-file"] "PATH" (flagHelpStr "path to config file") -- TODO: allow default on addFlagStringParam ?
|
|
cmdlineConfig <- configParser
|
|
suppressOutput <- addSimpleBoolFlag
|
|
""
|
|
["suppress-output"]
|
|
(flagHelp $ parDoc "suppress the regular output, i.e. the transformed haskell source")
|
|
_verbosity <- addSimpleCountFlag "v" ["verbose"] (flagHelp $ parDoc "[currently without effect; TODO]")
|
|
reorderStop
|
|
inputParam <- addStringParamOpt "PATH" (paramHelpStr "path to input haskell source file")
|
|
desc <- peekCmdDesc
|
|
addCmdImpl $ void $ do
|
|
when printVersion $ do
|
|
liftIO $ do
|
|
putStrLn $ "brittany version " ++ showVersion version
|
|
putStrLn $ "Copyright (C) 2016-2017 Lennart Spitzner"
|
|
putStrLn $ "There is NO WARRANTY, to the extent permitted by law."
|
|
System.Exit.exitSuccess
|
|
when printHelp $ do
|
|
liftIO $ print $ ppHelpShallow desc
|
|
System.Exit.exitSuccess
|
|
inputPathM <- case maybeToList inputParam ++ inputPaths of
|
|
[] -> do
|
|
return Nothing
|
|
[x] -> return $ Just x
|
|
_ -> do
|
|
liftIO $ putStrErrLn $ "more than one input, aborting"
|
|
System.Exit.exitWith (System.Exit.ExitFailure 50)
|
|
outputPath <- case outputPaths of
|
|
[] -> do
|
|
return Nothing
|
|
[x] -> return $ Just x
|
|
_ -> do
|
|
liftIO $ putStrErrLn $ "more than one output, aborting"
|
|
System.Exit.exitWith (System.Exit.ExitFailure 50)
|
|
config <- runMaybeT (readConfigs cmdlineConfig configPaths) >>= \case
|
|
Nothing -> System.Exit.exitWith (System.Exit.ExitFailure 50)
|
|
Just x -> return x
|
|
when (confUnpack $ _dconf_dump_config $ _conf_debug $ config) $ do
|
|
trace (showConfigYaml config) $ return ()
|
|
let ghcOptions = config & _conf_forward & _options_ghc & runIdentity
|
|
liftIO $ do
|
|
-- there is a good of code duplication between the following code and the
|
|
-- `pureModuleTransform` function. Unfortunately, there are also a good
|
|
-- amount of slight differences: This module is a bit more verbose, and
|
|
-- it tries to use the full-blown `parseModule` function which supports
|
|
-- CPP (but requires the input to be a file..).
|
|
let cppMode = config & _conf_preprocessor & _ppconf_CPPMode & runIdentity & Semigroup.getLast
|
|
-- the flag will do the following: insert a marker string
|
|
-- ("-- BRITTANY_INCLUDE_HACK ") right before any lines starting with
|
|
-- "#include" before processing (parsing) input; and remove that marker
|
|
-- string from the transformation output.
|
|
let hackAroundIncludes =
|
|
config & _conf_preprocessor & _ppconf_hackAroundIncludes & runIdentity & Semigroup.getLast
|
|
let cppCheckFunc dynFlags = if GHC.xopt GHC.Cpp dynFlags
|
|
then case cppMode of
|
|
CPPModeAbort -> do
|
|
return $ Left "Encountered -XCPP. Aborting."
|
|
CPPModeWarn -> do
|
|
putStrErrLn
|
|
$ "Warning: Encountered -XCPP."
|
|
++ " Be warned that -XCPP is not supported and that"
|
|
++ " brittany cannot check that its output is syntactically"
|
|
++ " valid in its presence."
|
|
return $ Right True
|
|
CPPModeNowarn -> return $ Right True
|
|
else return $ Right False
|
|
parseResult <- case inputPathM of
|
|
Nothing -> do
|
|
let hackF s = if "#include" `isPrefixOf` s then "-- BRITTANY_INCLUDE_HACK " ++ s else s
|
|
let hackTransform = if hackAroundIncludes then List.unlines . fmap hackF . List.lines else id
|
|
inputString <- System.IO.hGetContents System.IO.stdin
|
|
parseModuleFromString ghcOptions "stdin" cppCheckFunc (hackTransform inputString)
|
|
Just p -> parseModule ghcOptions p cppCheckFunc
|
|
case parseResult of
|
|
Left left -> do
|
|
putStrErrLn "parse error:"
|
|
printErr left
|
|
System.Exit.exitWith (System.Exit.ExitFailure 60)
|
|
Right (anns, parsedSource, hasCPP) -> do
|
|
when (config & _conf_debug .> _dconf_dump_ast_full .> confUnpack) $ do
|
|
let val = printTreeWithCustom 100 (customLayouterF anns) parsedSource
|
|
trace ("---- ast ----\n" ++ show val) $ return ()
|
|
(errsWarns, outLText) <- do
|
|
let omitCheck = config & _conf_errorHandling .> _econf_omit_output_valid_check .> confUnpack
|
|
(ews, outRaw) <- if hasCPP || omitCheck
|
|
then return $ pPrintModule config anns parsedSource
|
|
else pPrintModuleAndCheck config anns parsedSource
|
|
let hackF s = fromMaybe s $ TextL.stripPrefix (TextL.pack "-- BRITTANY_INCLUDE_HACK ") s
|
|
pure $ if hackAroundIncludes then (ews, TextL.unlines $ fmap hackF $ TextL.lines outRaw) else (ews, outRaw)
|
|
let customErrOrder ErrorInput{} = 4
|
|
customErrOrder LayoutWarning{} = 0 :: Int
|
|
customErrOrder ErrorOutputCheck{} = 1
|
|
customErrOrder ErrorUnusedComment{} = 2
|
|
customErrOrder ErrorUnknownNode{} = 3
|
|
when (not $ null errsWarns) $ do
|
|
let groupedErrsWarns = Data.List.Extra.groupOn customErrOrder $ List.sortOn customErrOrder $ errsWarns
|
|
groupedErrsWarns `forM_` \case
|
|
(ErrorOutputCheck{}:_) -> do
|
|
putStrErrLn $ "ERROR: brittany pretty printer" ++ " returned syntactically invalid result."
|
|
(ErrorInput str:_) -> do
|
|
putStrErrLn $ "ERROR: parse error: " ++ str
|
|
uns@(ErrorUnknownNode{}:_) -> do
|
|
putStrErrLn $ "ERROR: encountered unknown syntactical constructs:"
|
|
uns `forM_` \case
|
|
ErrorUnknownNode str ast -> do
|
|
putStrErrLn str
|
|
when (config & _conf_debug & _dconf_dump_ast_unknown & confUnpack) $ do
|
|
putStrErrLn $ " " ++ show (astToDoc ast)
|
|
_ -> error "cannot happen (TM)"
|
|
warns@(LayoutWarning{}:_) -> do
|
|
putStrErrLn $ "WARNINGS:"
|
|
warns `forM_` \case
|
|
LayoutWarning str -> putStrErrLn str
|
|
_ -> error "cannot happen (TM)"
|
|
unused@(ErrorUnusedComment{}:_) -> do
|
|
putStrErrLn
|
|
$ "Error: detected unprocessed comments."
|
|
++ " The transformation output will most likely"
|
|
++ " not contain certain of the comments"
|
|
++ " present in the input haskell source file."
|
|
putStrErrLn $ "Affected are the following comments:"
|
|
unused `forM_` \case
|
|
ErrorUnusedComment str -> putStrErrLn str
|
|
_ -> error "cannot happen (TM)"
|
|
[] -> error "cannot happen"
|
|
-- TODO: don't output anything when there are errors unless user
|
|
-- adds some override?
|
|
let hasErrors = case config & _conf_errorHandling & _econf_Werror & confUnpack of
|
|
False -> 0 < maximum (-1 : fmap customErrOrder errsWarns)
|
|
True -> not $ null errsWarns
|
|
outputOnErrs = config & _conf_errorHandling & _econf_produceOutputOnErrors & confUnpack
|
|
shouldOutput = not suppressOutput && (not hasErrors || outputOnErrs)
|
|
|
|
when shouldOutput $ addTraceSep (_conf_debug config) $ case outputPath of
|
|
Nothing -> TextL.IO.putStr $ outLText
|
|
Just p -> TextL.IO.writeFile p $ outLText
|
|
|
|
when hasErrors $ System.Exit.exitWith (System.Exit.ExitFailure 70)
|
|
where
|
|
addTraceSep conf =
|
|
if or
|
|
[ confUnpack $ _dconf_dump_annotations conf
|
|
, confUnpack $ _dconf_dump_ast_unknown conf
|
|
, confUnpack $ _dconf_dump_ast_full conf
|
|
, confUnpack $ _dconf_dump_bridoc_raw conf
|
|
, confUnpack $ _dconf_dump_bridoc_simpl_alt conf
|
|
, confUnpack $ _dconf_dump_bridoc_simpl_floating conf
|
|
, confUnpack $ _dconf_dump_bridoc_simpl_columns conf
|
|
, confUnpack $ _dconf_dump_bridoc_simpl_indent conf
|
|
, confUnpack $ _dconf_dump_bridoc_final conf
|
|
]
|
|
then trace "----"
|
|
else id
|
|
|
|
readConfigs :: CConfig Option -> [System.IO.FilePath] -> MaybeT IO Config
|
|
readConfigs cmdlineConfig configPaths = do
|
|
let defLocalConfigPath = "brittany.yaml"
|
|
userBritPath <- liftIO $ Directory.getAppUserDataDirectory "brittany"
|
|
let defUserConfigPath = userBritPath FilePath.</> "config.yaml"
|
|
merged <- case configPaths of
|
|
[] -> do
|
|
liftIO $ Directory.createDirectoryIfMissing False userBritPath
|
|
return cmdlineConfig
|
|
>>= readMergePersConfig defLocalConfigPath False
|
|
>>= readMergePersConfig defUserConfigPath True
|
|
-- TODO: ensure that paths exist ?
|
|
paths -> foldl (\prev p -> prev >>= readMergePersConfig p False) (return cmdlineConfig) paths
|
|
return $ cZipWith fromOptionIdentity staticDefaultConfig merged
|