on master help sign in
Implement handling of comments and printing of module headers
commit120b4caefccdf754aab43c0152094d4176bc71be authormrkkrp <markkarpov92@gmail.com> dateWed 30 Jan 2019, 18:36 (8 years ago) parentf1d07ca
28 files changed+679−47
app/Main.hs+3−3 data/examples/module-header/double-shebangs-out.hs+2 data/examples/module-header/double-shebangs.hs+2 data/examples/module-header/empty-out.hs data/examples/module-header/empty.hs data/examples/module-header/multiline-out.hs+6 data/examples/module-header/multiline-with-comments-out.hs+21 data/examples/module-header/multiline-with-comments.hs+21 data/examples/module-header/multiline.hs+2 data/examples/module-header/multiline2-out.hs+6 data/examples/module-header/multiline2.hs+2 data/examples/module-header/simple-out.hs+1 data/examples/module-header/simple-with-comments-out.hs+4 data/examples/module-header/simple-with-comments.hs+5 data/examples/module-header/simple.hs+1 data/examples/module-header/singleline-out.hs+1 data/examples/module-header/singleline.hs+1 ormolu.cabal+14−5 src/Ormolu.hs+97−1 src/Ormolu/Type.hs → src/Ormolu/Config.hs+5−5 src/Ormolu/Diff.hs+18 src/Ormolu/Parser.hs+2−2 src/Ormolu/Printer.hs+107−18 src/Ormolu/Printer/Combinators.hs+81−8 src/Ormolu/Printer/Comments.hs+198 src/Ormolu/Printer/Internal.hs+1−1 tests/Ormolu/Printer/CombinatorsSpec.hs+2−4 tests/Ormolu/PrinterSpec.hs+76
app/Main.hs+3−3
@@ -4,16 +4,17 @@ import Control.Monad
44import Ormolu.Parser
55import Ormolu.Printer
66import System.Environment (getArgs)
7+import qualified Data.Text.IO as TIO
78import qualified Outputable as GHC
89
910main :: IO ()
1011main = do
1112 (path:_) <- getArgs
1213 input <- readFile path
14+
1315 (ws, r) <- parseModule [] path input
1416 unless (null ws) $
1517 putStrLn "dynamic option warnings:"
16− -- TODO print ws
1718 case r of
1819 Left (srcSpan, err) -> do
1920 putStrLn (showOutputable srcSpan)
@@ -23,8 +24,7 @@ main = do
2324 putStrLn (showOutputable anns)
2425 putStrLn "\nparsed module:\n"
2526 putStrLn (showOutputable parsedModule)
26−
27− printModule anns parsedModule
27+ TIO.putStr (printModule anns parsedModule)
2828
2929showOutputable :: GHC.Outputable o => o -> String
3030showOutputable = GHC.showSDocUnsafe . GHC.ppr
data/examples/module-header/double-shebangs-out.hsnew file+2
@@ -0,0 +1,2 @@
1+#!/usr/bin/env stack
2+#!/usr/bin/env stack
data/examples/module-header/double-shebangs.hsnew file+2
@@ -0,0 +1,2 @@
1+#!/usr/bin/env stack
2+#!/usr/bin/env stack
data/examples/module-header/empty-out.hsnew file

No changes to the content.

data/examples/module-header/empty.hsnew file

No changes to the content.

data/examples/module-header/multiline-out.hsnew file+6
@@ -0,0 +1,6 @@
1+module Foo
2+ ( foo
3+ , bar
4+ , baz
5+ )
6+where
data/examples/module-header/multiline-with-comments-out.hsnew file+21
@@ -0,0 +1,21 @@
1+-- | Header.
2+
3+{-# LANGUAGE FlexibleContexts #-}
4+{-# LANGUAGE TypeFamilies #-}
5+module My.Module
6+ ( -- * Something
7+ foo
8+ , bar
9+ , -- * Another thing
10+ (<?>)
11+ , {- some other thing -} foo2 -- yet another
12+ , foo3 -- third one
13+ , baz
14+ , bar2 -- a multiline comment
15+ -- the second line
16+ , bar3
17+ , module Foo.Bar.Baz
18+ )
19+where
20+
21+-- Wow
data/examples/module-header/multiline-with-comments.hsnew file+21
@@ -0,0 +1,21 @@
1+-- | Header.
2+
3+{-# LANGUAGE FlexibleContexts #-}
4+{-# LANGUAGE TypeFamilies #-}
5+
6+module My.Module
7+ ( -- * Something
8+ foo,
9+ bar,
10+ -- * Another thing
11+ (<?>),
12+ {- some other thing -} foo2 -- yet another
13+ ,foo3 -- third one
14+ ,baz,
15+ bar2 -- a multiline comment
16+ -- the second line
17+ ,bar3
18+ , module Foo.Bar.Baz )
19+where
20+
21+-- Wow
data/examples/module-header/multiline.hsnew file+2
@@ -0,0 +1,2 @@
1+module Foo (
2+ foo, bar, baz) where
data/examples/module-header/multiline2-out.hsnew file+6
@@ -0,0 +1,6 @@
1+module Foo
2+ ( foo
3+ , bar
4+ , baz
5+ )
6+where
data/examples/module-header/multiline2.hsnew file+2
@@ -0,0 +1,2 @@
1+module Foo
2+ (foo, bar, baz) where
data/examples/module-header/simple-out.hsnew file+1
@@ -0,0 +1 @@
1+module Main where
data/examples/module-header/simple-with-comments-out.hsnew file+4
@@ -0,0 +1,4 @@
1+-- | Here we go.
2+module Main where
3+
4+-- Wow.
data/examples/module-header/simple-with-comments.hsnew file+5
@@ -0,0 +1,5 @@
1+-- | Here we go.
2+
3+module Main where
4+
5+-- Wow.
data/examples/module-header/simple.hsnew file+1
@@ -0,0 +1 @@
1+module Main where
data/examples/module-header/singleline-out.hsnew file+1
@@ -0,0 +1 @@
1+module Foo (foo, bar, baz) where
data/examples/module-header/singleline.hsnew file+1
@@ -0,0 +1 @@
1+module Foo ( foo, bar, baz ) where
ormolu.cabal+14−5
@@ -13,7 +13,8 @@ build-type: Simple
1313description: A formatter for Haskell source code.
1414extra-doc-files: CHANGELOG.md
1515 , README.md
16−data-files: data/printer/*.hs
16+data-files: data/examples/module-header/*.hs
17+ , data/printer/*.hs
1718
1819source-repository head
1920 type: git
@@ -28,17 +29,20 @@ library
2829 hs-source-dirs: src
2930 build-depends: base >= 4.8 && < 5.0
3031 , containers >= 0.5 && < 0.7
32+ , exceptions >= 0.6 && < 0.11
3133 , ghc >= 8.4.3
3234 , ghc-boot-th >= 8.4.3
3335 , ghc-exactprint >= 0.5.6
3436 , mtl >= 2.0 && < 3.0
3537 , text >= 0.2 && < 1.3
3638 exposed-modules: Ormolu
39+ , Ormolu.Config
40+ , Ormolu.Diff
3741 , Ormolu.Parser
3842 , Ormolu.Printer
3943 , Ormolu.Printer.Combinators
44+ , Ormolu.Printer.Comments
4045 , Ormolu.Printer.Internal
41− , Ormolu.Type
4246 if flag(dev)
4347 ghc-options: -Wall -Werror -Wcompat
4448 -Wincomplete-record-updates
@@ -55,12 +59,16 @@ test-suite tests
5559 type: exitcode-stdio-1.0
5660 build-depends: base >= 4.8 && < 5.0
5761 , containers >= 0.5 && < 0.7
62+ , filepath >= 1.2 && < 1.5
5863 , hspec >= 2.0 && < 3.0
5964 , ormolu
65+ , path >= 0.6 && < 0.7
66+ , path-io >= 1.4.2 && < 2.0
6067 , text >= 0.2 && < 1.3
61− build-tools: hspec-discover >= 2.0 && < 3.0
68+ build-tools: hspec-discover >= 2.0 && < 3.0
6269 other-modules:
6370 Ormolu.Printer.CombinatorsSpec
71+ , Ormolu.PrinterSpec
6472
6573 if flag(dev)
6674 ghc-options: -Wall -Werror
@@ -71,9 +79,10 @@ test-suite tests
7179executable ormolu
7280 main-is: Main.hs
7381 hs-source-dirs: app
74− build-depends: base >= 4.8 && < 5.0
75− , ghc >= 8.4.3
82+ build-depends: base >= 4.8 && < 5.0
83+ , ghc >= 8.4.3
7684 , ormolu
85+ , text >= 0.2 && < 1.3
7786 if flag(dev)
7887 ghc-options: -Wall -Werror -Wcompat
7988 -Wincomplete-record-updates
src/Ormolu.hs+97−1
@@ -1,3 +1,99 @@
1+-- | A formatter for Haskell source code.
2+
3+{-# LANGUAGE RecordWildCards #-}
4+
15module Ormolu
2− ( )
6+ ( ormolu
7+ , ormoluFile
8+ , Config (..)
9+ , defaultConfig
10+ , DynOption (..)
11+ , OrmoluException (..)
12+ )
313where
14+
15+import Control.Exception
16+import Control.Monad
17+import Control.Monad.Catch (MonadThrow (..))
18+import Control.Monad.IO.Class (MonadIO (..))
19+import Data.Text (Text)
20+import Language.Haskell.GHC.ExactPrint.Types
21+import Ormolu.Config
22+import Ormolu.Diff
23+import Ormolu.Parser
24+import Ormolu.Printer
25+import qualified Data.Text as T
26+import qualified GHC
27+
28+-- | Format a 'String', return formatted version as 'Text'.
29+--
30+-- The function
31+--
32+-- * Takes 'String' because that's what GHC parser accepts.
33+-- * Needs 'IO' because some functions from GHC that are necessary to
34+-- setup parsing context require 'IO'. There should be no visible
35+-- side-effects though.
36+-- * Takes file name just to use it in parse error messages.
37+-- * Throws 'OrmoluException'.
38+
39+ormolu
40+ :: (MonadIO m, MonadThrow m)
41+ => Config -- ^ Ormolu configuration
42+ -> FilePath -- ^ Location of source file
43+ -> String -- ^ Input to format
44+ -> m Text
45+ormolu cfg path str = do
46+ (anns0, parsedSrc0) <-
47+ parseModule' cfg OrmoluParsingFailed path str
48+ let txt = printModule anns0 parsedSrc0
49+ -- Parse the result of pretty-printing again and make sure that AST is the
50+ -- same as AST of original snippet module span positions.
51+ (anns1, parsedSrc1) <-
52+ parseModule' cfg OrmoluOutputParsingFailed "<rendered>" (T.unpack txt)
53+ when (diff (anns0, parsedSrc0) (anns1, parsedSrc1)) $
54+ throwM (OrmoluASTDiffers str txt)
55+ return txt
56+
57+-- | Load a file and format it. The file stays intact and the rendered
58+-- version is returned as 'Text'.
59+--
60+-- > ormoluFile cfg path =
61+-- > liftIO (readFile path) >>= ormolu cfg path
62+
63+ormoluFile
64+ :: (MonadIO m, MonadThrow m)
65+ => Config -- ^ Ormolu configuration
66+ -> FilePath -- ^ Location of source file
67+ -> m Text -- ^ Resulting rendition
68+ormoluFile cfg path =
69+ liftIO (readFile path) >>= ormolu cfg path
70+
71+-- | A wrapper around 'parseModule'.
72+
73+parseModule'
74+ :: (MonadIO m, MonadThrow m)
75+ => Config -- ^ Ormolu configuration
76+ -> (GHC.SrcSpan -> String -> OrmoluException)
77+ -- ^ How to obtain 'OrmoluException' to throw when parsing fails
78+ -> FilePath -- ^ File name to use in errors
79+ -> String -- ^ Actual input for the parser
80+ -> m (Anns, GHC.ParsedSource) -- ^ Annotations and parsed source
81+parseModule' Config {..} mkException path str = do
82+ (_, r) <- liftIO (parseModule cfgDynOptions path str)
83+ case r of
84+ Left (spn, err) -> throwM (mkException spn err)
85+ Right x -> return x
86+
87+-- | Ormolu exception representing all cases when 'ormoluFile' can fail.
88+
89+data OrmoluException
90+ = OrmoluParsingFailed GHC.SrcSpan String
91+ -- ^ Parsing of original source code failed
92+ | OrmoluOutputParsingFailed GHC.SrcSpan String
93+ -- ^ Parsing of formatted source code failed
94+ | OrmoluASTDiffers String Text
95+ -- ^ Original and resulting ASTs differ, first argument is the original
96+ -- source code, second argument is rendered source code
97+ deriving (Eq, Show)
98+
99+instance Exception OrmoluException
src/Ormolu/Type.hs → src/Ormolu/Config.hsrenamed+5−5
@@ -1,10 +1,10 @@
1−-- | Types used in the library.
1+-- | Configuration options used by the tool.
22
3−module Ormolu.Type
3+module Ormolu.Config
44 ( Config (..)
55 , defaultConfig
66 , DynOption (..)
7− , dynOption
7+ , dynOptionToLocatedStr
88 )
99where
1010
@@ -32,5 +32,5 @@ newtype DynOption = DynOption
3232
3333-- | Convert 'DynOption' to @'GHC.Located' 'String'@.
3434
35−dynOption :: DynOption -> GHC.Located String
36−dynOption (DynOption o) = GHC.L GHC.noSrcSpan o
35+dynOptionToLocatedStr :: DynOption -> GHC.Located String
36+dynOptionToLocatedStr (DynOption o) = GHC.L GHC.noSrcSpan o
src/Ormolu/Diff.hsnew file+18
@@ -0,0 +1,18 @@
1+-- | Diffing GHC ASTs modulo span positions.
2+
3+module Ormolu.Diff
4+ ( diff
5+ )
6+where
7+
8+import GHC
9+import Language.Haskell.GHC.ExactPrint.Types
10+
11+-- | Return 'False' of two annotated ASTs are the same modulo span
12+-- positions.
13+
14+diff
15+ :: (Anns, ParsedSource) -- ^ First annotated AST
16+ -> (Anns, ParsedSource) -- ^ Second annotated AST
17+ -> Bool
18+diff _ _ = False -- TODO
src/Ormolu/Parser.hs+2−2
@@ -6,7 +6,7 @@ where
66
77import Language.Haskell.GHC.ExactPrint.Parsers hiding (parseModule)
88import Language.Haskell.GHC.ExactPrint.Types
9−import Ormolu.Type
9+import Ormolu.Config
1010import qualified CmdLineParser as GHC
1111import qualified DynFlags as GHC
1212import qualified GHC hiding (parseModule)
@@ -21,5 +21,5 @@ parseModule
2121parseModule dynOpts path input = ghcWrapper $ do
2222 dynFlags0 <- initDynFlagsPure path input
2323 (dynFlags1, _, ws) <-
24− GHC.parseDynamicFilePragma dynFlags0 (dynOption <$> dynOpts)
24+ GHC.parseDynamicFilePragma dynFlags0 (dynOptionToLocatedStr <$> dynOpts)
2525 return (ws, parseModuleFromStringInternal dynFlags1 path input)
src/Ormolu/Printer.hs+107−18
@@ -1,3 +1,4 @@
1+{-# LANGUAGE LambdaCase #-}
12{-# LANGUAGE OverloadedStrings #-}
23{-# LANGUAGE RecordWildCards #-}
34
@@ -7,29 +8,117 @@ module Ormolu.Printer
78 ( printModule )
89where
910
10−import GHC
11+import Data.Char (isAlphaNum)
12+import Data.Text (Text)
13+import FieldLabel (FieldLbl (..))
14+import GHC hiding (GhcPs, IE)
15+import HsImpExp (IE (..))
1116import Language.Haskell.GHC.ExactPrint.Types
17+import Module (Module (..))
18+import OccName (OccName (..))
1219import Ormolu.Printer.Combinators
13−import qualified Data.Text.IO as TIO
20+import Outputable (Outputable (..), showSDocUnsafe)
21+import RdrName (RdrName (..), rdrNameOcc)
22+import SrcLoc (combineSrcSpans)
1423
1524-- | Render a module.
1625
1726printModule
18− :: Anns
19− -> ParsedSource
20− -> IO ()
21−printModule anns src = TIO.putStr $
22− runR True (p_HsModule src) anns
27+ :: Anns -- ^ Annotations
28+ -> ParsedSource -- ^ Parsed source
29+ -> Text -- ^ Resulting rendition
30+printModule anns src =
31+ runR False (p_HsModule src) anns
2332
2433p_HsModule :: ParsedSource -> R ()
25−p_HsModule l =
26− located l $ \HsModule {..} ->
27− p_hsmodName hsmodName
28−
29−p_hsmodName :: Maybe (Located ModuleName) -> R ()
30−p_hsmodName Nothing = return ()
31−p_hsmodName (Just l) =
32− located l $ \moduleName -> do
33− line $ do
34− txt "module "
35− atom moduleName
34+p_HsModule loc@(L moduleSpan hsModule) = do
35+ -- NOTE If span of exports in multiline, the whole thing is multiline.
36+ -- This is especially important because span of module itself always seems
37+ -- to have length zero, so it's not reliable for layout selection.
38+ let spn =
39+ case hsmodExports hsModule of
40+ Nothing -> moduleSpan
41+ Just (L exportsSpan _) -> combineSrcSpans moduleSpan exportsSpan
42+ locatedVia spn loc $ \HsModule {..} ->
43+ case hsmodName of
44+ Nothing -> pure ()
45+ Just hsmodName' -> line . velt' $
46+ [ located hsmodName' p_hsmodName ] ++
47+ (case hsmodExports of
48+ Nothing -> []
49+ Just hsmodExports' ->
50+ [ inci (locatedVia spn hsmodExports' p_hsmodExports)
51+ ])
52+ ++ [ txt "where"
53+ ]
54+
55+p_hsmodName :: ModuleName -> R ()
56+p_hsmodName mname = do
57+ txt "module "
58+ atom mname
59+
60+p_hsmodExports :: [LIE GhcPs] -> R ()
61+p_hsmodExports xs = do
62+ parens . velt $ withSep comma (located' p_lie) xs
63+
64+p_lie :: IE GhcPs -> R ()
65+p_lie = \case
66+ IEVar l1 -> located l1 p_ieWrappedName
67+ IEThingAbs l1 -> located l1 p_ieWrappedName
68+ IEThingAll l1 -> do
69+ located l1 p_ieWrappedName
70+ txt " (..)"
71+ IEThingWith l1 w xs fls -> do
72+ located l1 p_ieWrappedName
73+ space
74+ p_ieWildcard w
75+ parens . velt $ withSep comma (located' p_ieWrappedName) xs
76+ parens . velt $ withSep comma (located' p_FieldLbl) fls
77+ IEModuleContents l1 -> located l1 p_hsmodName
78+ -- XXX I have no idea what these things are for.
79+ IEGroup _ _ -> return ()
80+ IEDoc _ -> return ()
81+ IEDocNamed _ -> return ()
82+
83+p_ieWrappedName :: IEWrappedName RdrName -> R ()
84+p_ieWrappedName = \case
85+ IEName l2 -> located l2 p_rdrName
86+ IEPattern l2 -> located l2 $ \x -> do
87+ txt "pattern "
88+ p_rdrName x
89+ IEType l2 -> located l2 $ \x -> do
90+ txt "type "
91+ p_rdrName x
92+
93+p_rdrName :: RdrName -> R ()
94+p_rdrName x = opParens (rdrNameOcc x) $ case x of
95+ Unqual occName -> atom occName
96+ Qual mname occName -> p_qualName mname occName
97+ Orig (Module _ mname) occName -> p_qualName mname occName
98+ Exact name -> atom name
99+
100+p_FieldLbl :: FieldLbl RdrName -> R ()
101+p_FieldLbl (FieldLabel x _ _) = atom x
102+
103+p_qualName :: ModuleName -> OccName -> R ()
104+p_qualName mname occName = do
105+ atom mname
106+ txt "."
107+ atom occName
108+
109+p_ieWildcard :: IEWildcard -> R ()
110+p_ieWildcard = \case
111+ NoIEWildcard -> return ()
112+ IEWildcard n -> parens (atom n)
113+
114+----------------------------------------------------------------------------
115+-- Helpers
116+
117+-- | Put parentheses around the second argument if the 'Outputable' thing
118+-- consists only of punctuation characters.
119+
120+opParens :: Outputable a => a -> R () -> R ()
121+opParens x m =
122+ if all (not . isAlphaNum) (showSDocUnsafe (ppr x))
123+ then txt "(" >> m >> txt ")"
124+ else m
src/Ormolu/Printer/Combinators.hs+81−8
@@ -1,3 +1,4 @@
1+{-# LANGUAGE LambdaCase #-}
12{-# LANGUAGE OverloadedStrings #-}
23{-# LANGUAGE RecordWildCards #-}
34
@@ -14,8 +15,11 @@ module Ormolu.Printer.Combinators
1415 , newline
1516 , inci
1617 , located
18+ , locatedVia
19+ , located'
1720 , velt
1821 , velt'
22+ , withSep
1923 -- ** Wrapping
2024 , line
2125 , braces
@@ -26,14 +30,15 @@ module Ormolu.Printer.Combinators
2630 , ofType
2731 , sarrow
2832 , darrow
33+ , space
2934 )
3035where
3136
3237import Data.Data (Data)
3338import Data.List (intersperse)
3439import Data.Text (Text)
35−import Debug.Trace
3640import Language.Haskell.GHC.ExactPrint.Types
41+import Ormolu.Printer.Comments
3742import Ormolu.Printer.Internal
3843import Outputable (Outputable (..), showSDocUnsafe)
3944import SrcLoc
@@ -57,22 +62,72 @@ atom = txt . T.pack . showSDocUnsafe . ppr
5762-- that may be associated with the primitive and sets corresponding layout
5863-- for the inner computation.
5964
60−located :: Data a => Located a -> (a -> R ()) -> R ()
61−located loc@(L l a) f = do
62− -- TODO implement handling of comments properly
65+located
66+ :: Data a
67+ => Located a -- ^ Thing to enter
68+ -> (a -> R ()) -- ^ How to render inner value
69+ -> R ()
70+located loc@(L l _) = locatedVia l loc
71+
72+-- | A special version of 'located' that allows to control layout using
73+-- externally provided span.
74+
75+locatedVia
76+ :: Data a
77+ => SrcSpan -- ^ Span that controls layout selection
78+ -> Located a -- ^ Thing to enter
79+ -> (a -> R ()) -- ^ How to renedr inner value
80+ -> R ()
81+locatedVia l' loc@(L l a) f = do
6382 mann <- lookupAnn loc
6483 let m = enterLayout
65− (if isOneLineSpan l
84+ (if isOneLineSpan l'
6685 then SingleLine
6786 else MultiLine)
6887 (f a)
6988 case mann of
7089 Nothing -> m
7190 Just Ann {..} ->
72− enterLayout MultiLine $ do
73− traceShow annPriorComments $ mapM_ atom annPriorComments
91+ enterLayout MultiLine . sitcc $ do
92+ -- There are three things in 'Ann' which contain comments:
93+
94+ let cmode =
95+ if annGetConstr a == CN "HsModule"
96+ then Module
97+ else Other
98+ (before, after) = partitionDPs cmode l annsDP
99+
100+ -- 'annPriorComments' contains comments that were directly placed
101+ -- before entities such as comments (in both styles) before function
102+ -- definitions and inline comments before smaller things like types
103+ -- and literals.
104+
105+ spitComments (addDecoration cmode Before l <$> annPriorComments)
106+
107+ -- Comments inside 'annsDP' marked with 'AnnComment' are trickier,
108+ -- they seem to contain everything that goes after the thing they
109+ -- are attached to and in some cases (e.g. for modules) they contain
110+ -- comments that go before things. Exact location can only be
111+ -- deduced by analyzing the associated span.
112+
113+ -- traceShowM (before,after, decoratedElt)
114+
115+ spitComments before
74116 m
75− traceShow annFollowingComments $ mapM_ atom annFollowingComments
117+ spitComments after
118+
119+ -- I wasn't able to find any case when 'annFollowingComments' is
120+ -- populated, so we'll ignore that one for now and fix it when we
121+ -- have an example of source code where it matters.
122+
123+-- | A version of 'located' with arguments flipped.
124+
125+located'
126+ :: Data a
127+ => (a -> R ()) -- ^ How to render inner value
128+ -> Located a -- ^ Thing to enter
129+ -> R ()
130+located' = flip located
76131
77132-- | Element of variable layout. This means that the sub-components may be
78133-- rendered either on single line or each on its own line depending on
@@ -96,6 +151,19 @@ velt' xs = sitcc $ sequence_ (intersperse sep (sitcc <$> xs))
96151 where
97152 sep = vlayout (spit " ") newline
98153
154+-- | Put separator between renderings of items of a list.
155+
156+withSep
157+ :: R () -- ^ Separator
158+ -> (a -> R ()) -- ^ How to render list items
159+ -> [a] -- ^ List to render
160+ -> [R ()] -- ^ List of printing actions
161+withSep sep f = \case
162+ [] -> []
163+ (x:xs) ->
164+ let g a = sep >> f a
165+ in f x : fmap g xs
166+
99167----------------------------------------------------------------------------
100168-- Wrapping
101169
@@ -158,3 +226,8 @@ sarrow = txt "-> "
158226
159227darrow :: R ()
160228darrow = txt "=> "
229+
230+-- | Print single space.
231+
232+space :: R ()
233+space = txt " "
src/Ormolu/Printer/Comments.hsnew file+198
@@ -0,0 +1,198 @@
1+-- | Helpers for formatting of comments.
2+
3+{-# LANGUAGE LambdaCase #-}
4+{-# LANGUAGE OverloadedStrings #-}
5+
6+module Ormolu.Printer.Comments
7+ ( -- * Types
8+ Decoration (..)
9+ , Decorator (..)
10+ , Position (..)
11+ , CommentMode (..)
12+ -- * Functions for working with comments
13+ , spitComments
14+ , partitionDPs
15+ , addDecoration
16+ )
17+where
18+
19+import ApiAnnotation (AnnKeywordId (AnnModule))
20+import Control.Monad
21+import Data.Bifunctor
22+import Data.List (isPrefixOf)
23+import Data.Maybe (mapMaybe)
24+import Language.Haskell.GHC.ExactPrint.Types
25+import Ormolu.Printer.Internal
26+import SrcLoc
27+import qualified Data.Text as T
28+
29+-- | Placement instructions for comments.
30+
31+data Decoration
32+ = Decoration Decorator Decorator
33+ deriving (Eq, Show)
34+
35+-- | Decorator in this context is a thing to put before\/after a comment.
36+
37+data Decorator
38+ = NoDec -- ^ Output nothing
39+ | SpaceDec -- ^ Output single space
40+ | NewlineDec -- ^ Output single newline
41+ deriving (Eq, Show)
42+
43+-- | Position: before vs after.
44+
45+data Position
46+ = Before -- ^ Before
47+ | After -- ^ After
48+ deriving (Eq, Show)
49+
50+-- | For which type of AST leaf we're preparing the comments.
51+
52+data CommentMode
53+ = Module -- ^ Module
54+ | Other -- ^ Other element
55+ deriving (Eq, Show)
56+
57+-- | Output a bunch of 'Comment's. 'DeltaPos'es are used to insert extra
58+-- space between the comments when necessary.
59+
60+spitComments :: [(Comment, Decoration)] -> R ()
61+spitComments = mapM_ $ \(comment, (Decoration d0 d1)) -> do
62+ let spitDecorator = \case
63+ NoDec -> return ()
64+ SpaceDec -> spit " "
65+ NewlineDec -> newline
66+ spitDecorator d0
67+ spitComment comment
68+ spitDecorator d1
69+
70+-- | Output a 'Comment'.
71+
72+spitComment :: Comment -> R ()
73+spitComment (Comment str _ _) =
74+ if isMultiline str
75+ then if isPragma str
76+ then handleOne (normalizePragma str)
77+ else forM_ (normalizeIndent str) handleOne
78+ else handleOne str
79+ where
80+ handleOne x = do
81+ ensureIndent
82+ spit (T.pack x)
83+ isMultiline x = not ("--" `isPrefixOf` x)
84+ isPragma x = "{-#" `isPrefixOf` x
85+ normalizeIndent = fmap (dropWhile (== ' ')) . lines
86+ normalizePragma = unwords . words
87+
88+-- | Partition annotations to get a collection of 'Comment's preceding a
89+-- definition and following it. Every 'Comment' has corresponding
90+-- 'Decoration' which is used to understand how to decorate it.
91+
92+partitionDPs
93+ :: CommentMode -- ^ For which type of element we prepare comments
94+ -> SrcSpan -- ^ Span of element the comments are attached to
95+ -> [(KeywordId, DeltaPos)] -- ^ Annotations
96+ -> ([(Comment, Decoration)], [(Comment, Decoration)])
97+partitionDPs cmode refSpan anns =
98+ case cmode of
99+ Module -> partitionDPsModule refSpan anns
100+ Other -> partitionDPsOther refSpan anns
101+
102+-- | Try to partition comments as if for a module.
103+
104+partitionDPsModule
105+ :: SrcSpan -- ^ Span of element the comments are attached to
106+ -> [(KeywordId, DeltaPos)] -- ^ Annotations
107+ -> ([(Comment, Decoration)], [(Comment, Decoration)])
108+partitionDPsModule refSpan
109+ = bimap (takeComments Before) (takeComments After)
110+ . break ((== G AnnModule) . fst)
111+ where
112+ takeComments pos = mapMaybe $ \(keywordId, dpos) -> do
113+ c <- annComment keywordId
114+ return (c, getDecoration Module pos refSpan (c, dpos))
115+
116+-- | Partition comments according to their spans (works for everything but
117+-- modules).
118+
119+partitionDPsOther
120+ :: SrcSpan -- ^ Span of element the comments are attached to
121+ -> [(KeywordId, DeltaPos)] -- ^ Annotations
122+ -> ([(Comment, Decoration)], [(Comment, Decoration)])
123+partitionDPsOther refSpan
124+ = bimap (fmap (addDecoration Other Before refSpan))
125+ (fixupLastDec . fmap (addDecoration Other After refSpan))
126+ . break (followedBySpan refSpan . commentIdentifier . fst)
127+ . mapMaybe annComment'
128+ where
129+ annComment' :: (KeywordId, DeltaPos) -> Maybe (Comment, DeltaPos)
130+ annComment' (keywordId, dpos) = do
131+ c <- annComment keywordId
132+ return (c, dpos)
133+ followedBySpan :: SrcSpan -> SrcSpan -> Bool
134+ followedBySpan spn0 spn1 =
135+ if srcSpanEnd spn0 < srcSpanStart spn1
136+ then True
137+ else False
138+
139+-- | Last following comment cannot be standalone because in that case we get
140+-- redundant newlines.
141+
142+fixupLastDec :: [(Comment, Decoration)] -> [(Comment, Decoration)]
143+fixupLastDec [] = []
144+fixupLastDec [(c, Decoration d0 _)] = [(c, Decoration d0 NoDec)]
145+fixupLastDec (c:cs) = c : fixupLastDec cs
146+
147+-- | If 'KeywordId' is a comment, extract it.
148+
149+annComment :: KeywordId -> Maybe Comment
150+annComment (AnnComment x) = Just x
151+annComment _ = Nothing
152+
153+-- | Replace 'DeltaPos' with 'Decoration'.
154+
155+addDecoration
156+ :: CommentMode -- ^ For which type of element we prepare comments
157+ -> Position -- ^ Is this for comments before or after?
158+ -> SrcSpan -- ^ Span of element the comments are attached to
159+ -> (Comment, DeltaPos) -- ^ Thing to decorate
160+ -> (Comment, Decoration)
161+addDecoration cmode pos refSpan (comment, dpos) =
162+ ( comment
163+ , getDecoration cmode pos refSpan (comment, dpos)
164+ )
165+
166+-- | Calculate decoration for a comment.
167+
168+getDecoration
169+ :: CommentMode -- ^ For which type of element we prepare comments
170+ -> Position -- ^ Is this for comment before or after?
171+ -> SrcSpan -- ^ Span of element the comments are attached to
172+ -> (Comment, DeltaPos) -- ^ Thing to decorate
173+ -> Decoration
174+getDecoration cmode pos refSpan (c, (DP (r, _))) =
175+ Decoration preceedingDec followingDec
176+ where
177+ preceedingDec =
178+ if sameLine
179+ then case pos of
180+ Before -> NoDec
181+ After -> SpaceDec
182+ else if r > 1
183+ then NewlineDec
184+ else NoDec
185+ followingDec =
186+ if sameLine
187+ then case pos of
188+ Before -> SpaceDec
189+ After -> NewlineDec
190+ else NewlineDec
191+ sameLine =
192+ case cmode of
193+ Module -> False
194+ Other ->
195+ case (refSpan, commentIdentifier c) of
196+ (RealSrcSpan spn0, RealSrcSpan spn1) ->
197+ srcSpanStartLine spn0 == srcSpanStartLine spn1
198+ (_, _) -> False
src/Ormolu/Printer/Internal.hs+1−1
@@ -200,7 +200,7 @@ lookupAnn :: Data a => Located a -> R (Maybe Annotation)
200200lookupAnn (L s d) = do
201201 anns <- R (asks rcAnns)
202202 let k = AnnKey s (annGetConstr d)
203− traceShow k $ return (M.lookup k anns)
203+ return (M.lookup k anns)
204204
205205----------------------------------------------------------------------------
206206-- Debug helpers
tests/Ormolu/Printer/CombinatorsSpec.hs+2−4
@@ -35,11 +35,9 @@ stdTest
3535stdTest name m path = describe name $ do
3636 let spath = "data/printer/" ++ path ++ "-single.hs"
3737 mpath = "data/printer/" ++ path ++ "-multi.hs"
38− context "single-line" $
39− it "works as expected" $
38+ it "single-line works" $
4039 singleLine m `shouldRender` spath
41− context "multi-line" $
42− it "works as expected" $
40+ it "multi-line works" $
4341 multiLine m `shouldRender` mpath
4442
4543----------------------------------------------------------------------------
tests/Ormolu/PrinterSpec.hsnew file+76
@@ -0,0 +1,76 @@
1+{-# LANGUAGE TemplateHaskell #-}
2+
3+module Ormolu.PrinterSpec (spec) where
4+
5+import Control.Monad
6+import Control.Monad.IO.Class
7+import Data.List (isSuffixOf)
8+import Data.Text (Text)
9+import Ormolu
10+import Path
11+import Path.IO
12+import System.FilePath (addExtension, dropExtensions, splitExtensions)
13+import Test.Hspec
14+import qualified Data.Text as T
15+import qualified Data.Text.IO as T
16+
17+spec :: Spec
18+spec = do
19+ es <- runIO locateExamples
20+ forM_ es checkExample
21+
22+-- | Check a single given example.
23+
24+checkExample :: Path Rel File -> Spec
25+checkExample srcPath' = it (fromRelFile srcPath' ++ " works") $ do
26+ let srcPath = examplesDir </> srcPath'
27+ expectedOutputPath <- deriveOutput srcPath
28+ -- 1. Given input snippet of source code parse it and pretty print it.
29+ -- 2. Parse the result of pretty-printing again and make sure that AST
30+ -- is the same as AST of the original snippet. (This happens in
31+ -- 'ormoluFile' automatically.)
32+ formatted0 <- ormoluFile defaultConfig (fromRelFile srcPath)
33+ -- 3. Check the output against expected output. Thus all tests should
34+ -- include two files: input and expected output.
35+ expected <- (liftIO . T.readFile . fromRelFile) expectedOutputPath
36+ formatted0 `shouldMatch` expected
37+ -- 4. Check that running the formatter on the output produces the same
38+ -- output again (the transformation is idempotent).
39+ formatted1 <- ormolu defaultConfig "<formatted>" (T.unpack formatted0)
40+ formatted1 `shouldMatch` formatted0
41+
42+-- | Build list of examples for testing.
43+
44+locateExamples :: IO [Path Rel File]
45+locateExamples =
46+ filter isInput . snd <$> listDirRecurRel examplesDir
47+
48+-- | Does given path look like input path (as opposed to expected output
49+-- path)?
50+
51+isInput :: Path Rel File -> Bool
52+isInput path =
53+ let s = fromRelFile path
54+ (s', exts) = splitExtensions s
55+ in exts == ".hs" && not ("-out" `isSuffixOf` s')
56+
57+-- | For given path of input file return expected name of output.
58+
59+deriveOutput :: Path Rel File -> IO (Path Rel File)
60+deriveOutput path = parseRelFile $
61+ addExtension (dropExtensions (fromRelFile path) ++ "-out") "hs"
62+
63+-- | A version of 'shouldBe' that is specialized to comparing 'Text' values.
64+-- It also prints multi-line snippets in a more readable form.
65+
66+shouldMatch :: Text -> Text -> Expectation
67+shouldMatch actual expected =
68+ when (actual /= expected) . expectationFailure $ unlines
69+ [ "expected:"
70+ , T.unpack expected
71+ , "bot got:"
72+ , T.unpack actual
73+ ]
74+
75+examplesDir :: Path Rel Dir
76+examplesDir = $(mkRelDir "data/examples")