on master help sign in
Implement the --check-idempotency flag
commitda0b3b856539ff7a276b72a82b2e101faeea28b5 authorUtku Demir <me@utdemir.com> dateFri 16 Aug 2019, 21:40 (7 years ago) parente598b72
6 files changed+94−29
app/Main.hs+5 nix/ormolize/ormolize.sh+2−1 src/Ormolu.hs+19−9 src/Ormolu/Config.hs+3 src/Ormolu/Diff.hs+49−11 src/Ormolu/Exception.hs+16−8
app/Main.hs+5
@@ -137,6 +137,11 @@ configParser = Config
137137 , short 'p'
138138 , help "Do not fail if CPP pragma is present"
139139 ]
140+ <*> (switch . mconcat)
141+ [ long "check-idempotency"
142+ , short 'c'
143+ , help "Fail if formatting is not idempotent."
144+ ]
140145
141146----------------------------------------------------------------------------
142147-- Helpers
nix/ormolize/ormolize.sh+2−1
@@ -13,4 +13,5 @@ mv "$1-nocpp" "$1"
1313cp "$1" "$1-original"
1414
1515# run ormolu
16−ormolu --tolerate-cpp --mode inplace "$1" || ormolu --tolerate-cpp --unsafe --mode inplace "$1"
16+ormolu --tolerate-cpp --check-idempotency --mode inplace "$1" ||
17+ ormolu --tolerate-cpp --unsafe --mode inplace "$1"
src/Ormolu.hs+19−9
@@ -26,10 +26,10 @@ import Ormolu.Parser
2626import Ormolu.Parser.Result
2727import Ormolu.Printer
2828import Ormolu.Utils (showOutputable)
29+import System.IO (hGetContents, stdin)
2930import qualified CmdLineParser as GHC
3031import qualified Data.Text as T
31−import qualified GHC
32−import System.IO (hGetContents, stdin)
32+import qualified SrcLoc as GHC
3333
3434-- | Format a 'String', return formatted version as 'Text'.
3535--
@@ -56,18 +56,28 @@ ormolu cfg path str = do
5656 traceM (concatMap showWarn ws)
5757 traceM (prettyPrintParseResult result0)
5858 let txt = printModule (cfgDebug cfg) result0
59− -- Parse the result of pretty-printing again and make sure that AST is the
60− -- same as AST of original snippet module span positions.
61− unless (cfgUnsafe cfg) $ do
59+ when (not (cfgUnsafe cfg) || cfgCheckIdempotency cfg) $ do
60+ let pathRendered = path ++ "<rendered>"
61+ -- Parse the result of pretty-printing again and make sure that AST
62+ -- is the same as AST of original snippet module span positions.
6263 (_, result1) <-
6364 parseModule'
6465 cfg
6566 OrmoluOutputParsingFailed
66− (path ++ "<rendered>")
67+ pathRendered
6768 (T.unpack txt)
68− case diff result0 result1 of
69− Same -> return ()
70− Different ss -> liftIO $ throwIO (OrmoluASTDiffers path ss)
69+ unless (cfgUnsafe cfg) $
70+ case diffParseResult result0 result1 of
71+ Same -> return ()
72+ Different ss -> liftIO $ throwIO (OrmoluASTDiffers path ss)
73+ -- Try re-formatting the formatted result to check if we get exactly
74+ -- the same output.
75+ when (cfgCheckIdempotency cfg) $
76+ let txt2 = printModule False result1
77+ in case diffText txt txt2 pathRendered of
78+ Nothing -> return ()
79+ Just (loc, l, r) -> liftIO $
80+ throwIO (OrmoluNonIdempotentOutput loc l r)
7181 return txt
7282
7383-- | Load a file and format it. The file stays intact and the rendered
src/Ormolu/Config.hs+3
@@ -27,6 +27,8 @@ data Config = Config
2727 -- ^ Do not fail if CPP pragma is present (still doesn't handle CPP but
2828 -- useful for formatting of files that enable the extension without
2929 -- actually containing CPP macros)
30+ , cfgCheckIdempotency :: !Bool
31+ -- ^ Checks if re-formatting the result is idempotent.
3032 } deriving (Eq, Show)
3133
3234-- | Default 'Config'.
@@ -37,6 +39,7 @@ defaultConfig = Config
3739 , cfgUnsafe = False
3840 , cfgDebug = False
3941 , cfgTolerateCpp = False
42+ , cfgCheckIdempotency = False
4043 }
4144
4245-- | A wrapper for dynamic options.
src/Ormolu/Diff.hs+49−11
@@ -5,17 +5,22 @@
55-- | Diffing GHC ASTs modulo span positions.
66
77module Ormolu.Diff
8− ( diff
9− , Diff(..)
8+ ( Diff(..)
9+ , diffParseResult
10+ , diffText
1011 )
1112where
1213
1314import BasicTypes (SourceText)
1415import Data.ByteString (ByteString)
1516import Data.Generics
17+import Data.Text (Text)
1618import GHC
1719import Ormolu.Imports (sortImports)
1820import Ormolu.Parser.Result
21+import qualified Data.Text as T
22+import qualified FastString as GHC
23+import qualified SrcLoc as GHC
1924
2025-- | Result of comparing two 'ParseResult's.
2126
@@ -31,16 +36,16 @@ instance Semigroup Diff where
3136instance Monoid Diff where
3237 mempty = Same
3338
34−-- | Return 'False' if two annotated ASTs are the same modulo span
35−-- positions.
39+-- | Return 'Diff' of two 'ParseResult's.
3640
37−diff :: ParseResult -> ParseResult -> Diff
38−diff ParseResult { prCommentStream = cstream0
39− , prParsedSource = ps0
40− }
41− ParseResult { prCommentStream = cstream1
42− , prParsedSource = ps1
43− } =
41+diffParseResult :: ParseResult -> ParseResult -> Diff
42+diffParseResult
43+ ParseResult { prCommentStream = cstream0
44+ , prParsedSource = ps0
45+ }
46+ ParseResult { prCommentStream = cstream1
47+ , prParsedSource = ps1
48+ } =
4449 matchIgnoringSrcSpans cstream0 cstream1 <>
4550 matchIgnoringSrcSpans ps0 ps1
4651
@@ -96,3 +101,36 @@ matchIgnoringSrcSpans = genericQuery
96101 fresh = not $ any (flip isSubspanOf s) ss
97102 helpful = isGoodSrcSpan s
98103 appendSpan _ d = d
104+
105+-- | Diff two texts and return the location they start to differ, alongside
106+-- with excerpts around that location.
107+
108+diffText
109+ :: Text -- ^ Text before
110+ -> Text -- ^ Text after
111+ -> FilePath -- ^ Path to use to construct 'GHC.RealSrcLoc'
112+ -> Maybe (GHC.RealSrcLoc, Text, Text)
113+diffText left right fp =
114+ case go (0, 0, 0) left right of
115+ Nothing -> Nothing
116+ Just (row, col, loc) -> Just (
117+ GHC.mkRealSrcLoc (GHC.mkFastString fp) row col,
118+ getSpan loc left,
119+ getSpan loc right
120+ )
121+ where
122+ go (row, col, loc) t1 t2 =
123+ case (T.uncons t1, T.uncons t2) of
124+ -- both text empty, all good
125+ (Nothing, Nothing) ->
126+ Nothing
127+ -- first chars are the same, adjust position and recurse
128+ (Just (c1, r1), Just (c2, r2)) | c1 == c2 ->
129+ let (row', col', loc') = if c1 == '\n'
130+ then (row + 1, 0, loc + 1)
131+ else (row, col + 1, loc + 1)
132+ in go (row', col', loc') r1 r2
133+ -- something is different, return the position
134+ _ ->
135+ Just (row, col, loc)
136+ getSpan loc = T.take 20 . T.drop (loc - 10)
src/Ormolu/Exception.hs+16−8
@@ -12,7 +12,9 @@ import Control.Exception
1212import Ormolu.Utils (showOutputable)
1313import System.Exit (ExitCode (..), exitWith)
1414import System.IO
15+import Data.Text (Text)
1516import qualified GHC
17+import qualified Outputable as GHC
1618
1719-- | Ormolu exception representing all cases when Ormolu can fail.
1820
@@ -25,6 +27,8 @@ data OrmoluException
2527 -- ^ Parsing of formatted source code failed
2628 | OrmoluASTDiffers FilePath [GHC.SrcSpan]
2729 -- ^ Original and resulting ASTs differ
30+ | OrmoluNonIdempotentOutput GHC.RealSrcLoc Text Text
31+ -- ^ Formatted source code is not idempotent
2832 deriving (Eq, Show)
2933
3034instance Exception OrmoluException where
@@ -34,9 +38,9 @@ instance Exception OrmoluException where
3438 , withIndent path
3539 ]
3640 OrmoluParsingFailed s e ->
37− showParsingErr "Parsing of source code failed:" s e
41+ showParsingErr "Parsing of source code failed:" s [e]
3842 OrmoluOutputParsingFailed s e ->
39− showParsingErr "Parsing of formatted code failed:" s e ++
43+ showParsingErr "Parsing of formatted code failed:" s [e] ++
4044 "Please, consider reporting the bug.\n"
4145 OrmoluASTDiffers path ss -> unlines $
4246 [ "AST of input and AST of formatted code differ."
@@ -44,8 +48,12 @@ instance Exception OrmoluException where
4448 ++ (fmap withIndent $ case fmap (\s -> "at " ++ showOutputable s) ss of
4549 [] -> ["in " ++ path]
4650 xs -> xs) ++
47− [ "Please, consider reporting the bug."
48− ]
51+ [ "Please, consider reporting the bug." ]
52+ OrmoluNonIdempotentOutput loc left right ->
53+ showParsingErr "Formatting is not idempotent:" loc
54+ [ "before: " ++ show left , "after: " ++ show right ]
55+ ++ "Please, consider reporting the bug.\n"
56+
4957
5058-- | Inside this wrapper 'OrmoluException' will be caught and displayed
5159-- nicely using 'displayException'.
@@ -65,18 +73,18 @@ withPrettyOrmoluExceptions m = m `catch` h
6573 OrmoluParsingFailed _ _ -> 3
6674 OrmoluOutputParsingFailed _ _ -> 4
6775 OrmoluASTDiffers _ _ -> 5
76+ OrmoluNonIdempotentOutput _ _ _ -> 6
6877
6978----------------------------------------------------------------------------
7079-- Helpers
7180
7281-- | Show a parse error.
7382
74−showParsingErr :: String -> GHC.SrcSpan -> String -> String
75−showParsingErr msg spn err = unlines
83+showParsingErr :: GHC.Outputable a => String -> a -> [String] -> String
84+showParsingErr msg spn err = unlines $
7685 [ msg
7786 , withIndent (showOutputable spn)
78− , withIndent err
79− ]
87+ ] ++ map withIndent err
8088
8189-- | Indent with 2 spaces for readability.
8290