on master help sign in
Implement CLI for the application
commit2f73bae977aceebc4a88773828356b147b1df7b8 authormrkkrp <mark.karpov@tweag.io> dateSun 24 Feb 2019, 21:38 (8 years ago) parent53285db
6 files changed+197−47
app/Main.hs+135−25 ormolu.cabal+6 src/Ormolu.hs+34−10 src/Ormolu/Config.hs+16−7 src/Ormolu/Printer.hs+4−3 tests/Ormolu/PrinterSpec.hs+2−2
app/Main.hs+135−25
@@ -1,32 +1,142 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE LambdaCase #-}
3+{-# LANGUAGE RecordWildCards #-}
4+{-# LANGUAGE TemplateHaskell #-}
5+
16module Main (main) where
27
3−-- import Control.Monad
8+import Control.Monad
9+import Data.List (intercalate)
10+import Data.Version (showVersion)
11+import Development.GitRev
12+import Options.Applicative
413import Ormolu
5−import System.Environment (getArgs)
14+import Paths_ormolu (version)
15+import System.Exit (exitFailure)
616import qualified Data.Text.IO as TIO
7−-- import qualified Outputable as GHC
17+import qualified Data.Yaml as Yaml
18+
19+-- | Entry point of the program.
820
921main :: IO ()
1022main = withPrettyOrmoluExceptions $ do
11− (path:_) <- getArgs
12− r <- ormoluFile defaultConfig path
13− TIO.putStr r
14− -- TIO.
15−
16− -- input <- readFile path
17−
18− -- (ws, r) <- parseModule [] path input
19− -- unless (null ws) $
20− -- putStrLn "dynamic option warnings:"
21− -- case r of
22− -- Left (srcSpan, err) -> do
23− -- putStrLn (showOutputable srcSpan)
24− -- putStrLn err
25− -- Right (anns, parsedModule) -> do
26− -- putStrLn "\nannotations:\n"
27− -- putStrLn (showOutputable anns)
28− -- putStrLn "\nparsed module:\n"
29− -- putStrLn (showOutputable parsedModule)
30−
31−-- showOutputable :: GHC.Outputable o => o -> String
32−-- showOutputable = GHC.showSDocUnsafe . GHC.ppr
23+ Opts {..} <- execParser optsParserInfo
24+ config <- case optConfigFile of
25+ Nothing -> return Config
26+ { cfgDynOptions = optDynOptions
27+ , cfgUnsafe = optUnsafe
28+ }
29+ Just path -> do
30+ Config {..} <- Yaml.decodeFileThrow path
31+ return Config
32+ { cfgDynOptions = cfgDynOptions ++ optDynOptions
33+ , cfgUnsafe = optUnsafe || cfgUnsafe
34+ }
35+ r <- ormoluFile config optDebug optInputFile
36+ case optMode of
37+ Stdout ->
38+ TIO.putStr r
39+ InPlace ->
40+ TIO.writeFile optInputFile r
41+ Check -> do
42+ r' <- TIO.readFile optInputFile
43+ when (r /= r') exitFailure
44+
45+----------------------------------------------------------------------------
46+-- Command line options parsing.
47+
48+data Opts = Opts
49+ { optMode :: !Mode
50+ -- ^ Mode of operation
51+ , optConfigFile :: !(Maybe FilePath)
52+ -- ^ Location of configuration file (optional)
53+ , optUnsafe :: !Bool
54+ -- ^ Whether to skip sanity checking
55+ , optDynOptions :: ![DynOption]
56+ -- ^ GHC options to set
57+ , optDebug :: !Bool
58+ -- ^ Output information useful for debugging
59+ , optInputFile :: !FilePath
60+ -- ^ Input source file
61+ }
62+
63+-- | Mode of operation.
64+
65+data Mode
66+ = Stdout -- ^ Output formatted source code to stdout
67+ | InPlace -- ^ Overwrite original file
68+ | Check -- ^ Exit with non-zero status code if
69+ -- source is not already formatted
70+ deriving (Eq, Show)
71+
72+optsParserInfo :: ParserInfo Opts
73+optsParserInfo = info (helper <*> ver <*> optsParser) . mconcat $
74+ [ fullDesc
75+ , progDesc ""
76+ , header ""
77+ ]
78+ where
79+ ver :: Parser (a -> a)
80+ ver = infoOption verStr . mconcat $
81+ [ long "version"
82+ , short 'v'
83+ , help "Print version of the program"
84+ ]
85+ verStr = intercalate "\n"
86+ [ unwords
87+ [ "ormolu"
88+ , showVersion version
89+ , $gitBranch
90+ , $gitHash
91+ ]
92+ , "using ghc-exactprint " ++ VERSION_ghc_exactprint
93+ , "using ghc " ++ VERSION_ghc
94+ ]
95+
96+optsParser :: Parser Opts
97+optsParser = Opts
98+ <$> (option parseMode . mconcat)
99+ [ long "mode"
100+ , short 'm'
101+ , metavar "MODE"
102+ , value Stdout
103+ , help "Mode of operation: 'stdout', 'inplace', or 'check'"
104+ ]
105+ <*> (optional . strOption . mconcat)
106+ [ long "config"
107+ , short 'c'
108+ , metavar "CONFIG"
109+ , help "Location of configuration file"
110+ ]
111+ <*> (switch . mconcat)
112+ [ long "unsafe"
113+ , short 'u'
114+ , help "Do formatting faster but without automatic detection of defects"
115+ ]
116+ <*> (fmap (fmap DynOption) . many . strOption . mconcat)
117+ [ long "ghc-opt"
118+ , short 'o'
119+ , metavar "OPT"
120+ , help "GHC options to enable (e.g. language extensions)"
121+ ]
122+ <*> (switch . mconcat)
123+ [ long "debug"
124+ , short 'd'
125+ , help "Output information useful for debugging"
126+ ]
127+ <*> (strArgument . mconcat)
128+ [ metavar "FILE"
129+ , help "Haskell source file to format"
130+ ]
131+
132+----------------------------------------------------------------------------
133+-- Helpers
134+
135+-- | Parse 'Mode'.
136+
137+parseMode :: ReadM Mode
138+parseMode = eitherReader $ \case
139+ "stdout" -> Right Stdout
140+ "inplace" -> Right InPlace
141+ "check" -> Right Check
142+ s -> Left $ "unknown mode: " ++ s
ormolu.cabal+6
@@ -36,6 +36,7 @@ library
3636 , mtl >= 2.0 && < 3.0
3737 , syb >= 0.7 && < 0.8
3838 , text >= 0.2 && < 1.3
39+ , yaml >= 0.8 && < 0.12
3940 exposed-modules: Ormolu
4041 , Ormolu.Comments
4142 , Ormolu.Config
@@ -84,8 +85,13 @@ executable ormolu
8485 hs-source-dirs: app
8586 build-depends: base >= 4.8 && < 5.0
8687 , ghc >= 8.4.3
88+ , ghc-exactprint >= 0.5.6
89+ , gitrev >= 1.3 && < 1.4
90+ , optparse-applicative >= 0.14 && < 0.15
8791 , ormolu
8892 , text >= 0.2 && < 1.3
93+ , yaml >= 0.8 && < 0.12
94+ other-modules: Paths_ormolu
8995 if flag(dev)
9096 ghc-options: -Wall -Werror -Wcompat
9197 -Wincomplete-record-updates
src/Ormolu.hs+34−10
@@ -17,14 +17,17 @@ import Control.Exception
1717import Control.Monad
1818import Control.Monad.IO.Class (MonadIO (..))
1919import Data.Text (Text)
20+import Debug.Trace
2021import Language.Haskell.GHC.ExactPrint.Types
2122import Ormolu.Config
2223import Ormolu.Diff
2324import Ormolu.Exception
2425import Ormolu.Parser
2526import Ormolu.Printer
27+import qualified CmdLineParser as GHC
2628import qualified Data.Text as T
2729import qualified GHC
30+import qualified Outputable as GHC
2831
2932-- | Format a 'String', return formatted version as 'Text'.
3033--
@@ -40,17 +43,23 @@ import qualified GHC
4043ormolu
4144 :: MonadIO m
4245 => Config -- ^ Ormolu configuration
46+ -> Bool -- ^ Output debugging info
4347 -> FilePath -- ^ Location of source file
4448 -> String -- ^ Input to format
4549 -> m Text
46−ormolu cfg path str = do
47− (anns0, parsedSrc0) <-
50+ormolu cfg debugOn path str = do
51+ (ws, (anns0, parsedSrc0)) <-
4852 parseModule' cfg OrmoluParsingFailed path str
49− let txt = printModule anns0 parsedSrc0
53+ when debugOn $ do
54+ traceM "warnings:\n"
55+ traceM (concatMap showWarn ws)
56+ traceM "anns:\n"
57+ traceM (showOutputable anns0)
58+ let txt = printModule debugOn anns0 parsedSrc0
5059 -- Parse the result of pretty-printing again and make sure that AST is the
5160 -- same as AST of original snippet module span positions.
52− when (cfgSanityCheck cfg) $ do
53− (anns1, parsedSrc1) <-
61+ unless (cfgUnsafe cfg) $ do
62+ (_, (anns1, parsedSrc1)) <-
5463 parseModule' cfg OrmoluOutputParsingFailed "<rendered>" (T.unpack txt)
5564 when (diff (anns0, parsedSrc0) (anns1, parsedSrc1)) $
5665 liftIO $ throwIO (OrmoluASTDiffers str txt)
@@ -65,10 +74,14 @@ ormolu cfg path str = do
6574ormoluFile
6675 :: MonadIO m
6776 => Config -- ^ Ormolu configuration
77+ -> Bool -- ^ Output debugging info
6878 -> FilePath -- ^ Location of source file
6979 -> m Text -- ^ Resulting rendition
70−ormoluFile cfg path =
71− liftIO (readFile path) >>= ormolu cfg path
80+ormoluFile cfg debugOn path =
81+ liftIO (readFile path) >>= ormolu cfg debugOn path
82+
83+----------------------------------------------------------------------------
84+-- Helpers
7285
7386-- | A wrapper around 'parseModule'.
7487
@@ -79,9 +92,20 @@ parseModule'
7992 -- ^ How to obtain 'OrmoluException' to throw when parsing fails
8093 -> FilePath -- ^ File name to use in errors
8194 -> String -- ^ Actual input for the parser
82− -> m (Anns, GHC.ParsedSource) -- ^ Annotations and parsed source
95+ -> m ([GHC.Warn], (Anns, GHC.ParsedSource)) -- ^ Annotations and parsed source
8396parseModule' Config {..} mkException path str = do
84− (_, r) <- parseModule cfgDynOptions path str
97+ (ws, r) <- parseModule cfgDynOptions path str
8598 case r of
8699 Left (spn, err) -> liftIO $ throwIO (mkException spn err)
87− Right x -> return x
100+ Right x -> return (ws, x)
101+
102+-- | Pretty-print a 'GHC.Warn'.
103+
104+showWarn :: GHC.Warn -> String
105+showWarn (GHC.Warn reason l) =
106+ showOutputable reason ++ "\n" ++ showOutputable l ++ "\n"
107+
108+-- | Pretty-print an 'GHC.Outputable' thing.
109+
110+showOutputable :: GHC.Outputable o => o -> String
111+showOutputable = GHC.showSDocUnsafe . GHC.ppr
src/Ormolu/Config.hs+16−7
@@ -1,5 +1,9 @@
11-- | Configuration options used by the tool.
22
3+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+{-# LANGUAGE OverloadedStrings #-}
5+{-# LANGUAGE RecordWildCards #-}
6+
37module Ormolu.Config
48 ( Config (..)
59 , defaultConfig
@@ -8,32 +12,37 @@ module Ormolu.Config
812 )
913where
1014
15+import Data.Yaml
1116import qualified SrcLoc as GHC
1217
1318-- | Ormolu configuration.
1419
1520data Config = Config
16− { cfgDynOptions :: [DynOption]
21+ { cfgDynOptions :: ![DynOption]
1722 -- ^ Dynamic options to pass to GHC parser
18− , cfgSanityCheck :: Bool
19− -- ^ Whether to parse output of formatter and compare the obtained AST
20− -- with original AST. Doing this makes the program much slower, but
21− -- it'll catch and report all possible issues.
23+ , cfgUnsafe :: !Bool
24+ -- ^ Do formatting faster but without automatic detection of defects
2225 } deriving (Eq, Show)
2326
27+instance FromJSON Config where
28+ parseJSON = withObject "config" $ \o -> do
29+ cfgDynOptions <- o .: "ghc-opts"
30+ cfgUnsafe <- o .: "unsafe"
31+ return Config {..}
32+
2433-- | Default 'Config'.
2534
2635defaultConfig :: Config
2736defaultConfig = Config
2837 { cfgDynOptions = []
29− , cfgSanityCheck = True
38+ , cfgUnsafe = False
3039 }
3140
3241-- | A wrapper for dynamic options.
3342
3443newtype DynOption = DynOption
3544 { unDynOption :: String
36− } deriving (Eq, Ord, Show)
45+ } deriving (Eq, Ord, Show, FromJSON)
3746
3847-- | Convert 'DynOption' to @'GHC.Located' 'String'@.
3948
src/Ormolu/Printer.hs+4−3
@@ -25,11 +25,12 @@ import SrcLoc (combineSrcSpans)
2525-- | Render a module.
2626
2727printModule
28− :: Anns -- ^ Annotations
28+ :: Bool -- ^ Trace debugging information
29+ -> Anns -- ^ Annotations
2930 -> ParsedSource -- ^ Parsed source
3031 -> Text -- ^ Resulting rendition
31−printModule anns src =
32− runR False (p_HsModule src) anns
32+printModule debugOn anns src =
33+ runR debugOn (p_HsModule src) anns
3334
3435p_HsModule :: ParsedSource -> R ()
3536p_HsModule loc@(L moduleSpan hsModule) = do
tests/Ormolu/PrinterSpec.hs+2−2
@@ -29,14 +29,14 @@ checkExample srcPath' = it (fromRelFile srcPath' ++ " works") $ do
2929 -- 2. Parse the result of pretty-printing again and make sure that AST
3030 -- is the same as AST of the original snippet. (This happens in
3131 -- 'ormoluFile' automatically.)
32− formatted0 <- ormoluFile defaultConfig (fromRelFile srcPath)
32+ formatted0 <- ormoluFile defaultConfig False (fromRelFile srcPath)
3333 -- 3. Check the output against expected output. Thus all tests should
3434 -- include two files: input and expected output.
3535 expected <- (liftIO . T.readFile . fromRelFile) expectedOutputPath
3636 formatted0 `shouldMatch` expected
3737 -- 4. Check that running the formatter on the output produces the same
3838 -- output again (the transformation is idempotent).
39− formatted1 <- ormolu defaultConfig "<formatted>" (T.unpack formatted0)
39+ formatted1 <- ormolu defaultConfig False "<formatted>" (T.unpack formatted0)
4040 formatted1 `shouldMatch` formatted0
4141
4242-- | Build list of examples for testing.