This repository has been archived by the owner on Apr 29, 2024. It is now read-only.
forked from mihaimaruseac/hindent
-
Notifications
You must be signed in to change notification settings - Fork 0
/
HIndent.hs
434 lines (410 loc) · 15.7 KB
/
HIndent.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, PatternGuards #-}
-- | Haskell indenter.
module HIndent
(-- * Formatting functions.
reformat
,prettyPrint
,parseMode
-- * Testing
,test
,testFile
,testAst
,testFileAst
,defaultExtensions
,getExtensions
)
where
import Control.Monad.State.Strict
import Control.Monad.Trans.Maybe
import Data.ByteString (ByteString)
import qualified Data.ByteString as S
import Data.ByteString.Builder (Builder)
import qualified Data.ByteString.Builder as S
import qualified Data.ByteString.Char8 as S8
import qualified Data.ByteString.Internal as S
import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.Lazy.Char8 as L8
import qualified Data.ByteString.UTF8 as UTF8
import qualified Data.ByteString.Unsafe as S
import Data.Char
import Data.Foldable (foldr')
import Data.Either
import Data.Function
import Data.Functor.Identity
import Data.List
import Data.Maybe
import Data.Monoid
import Data.Text (Text)
import qualified Data.Text as T
import Data.Traversable hiding (mapM)
import HIndent.CodeBlock
import HIndent.Pretty
import HIndent.Types
import qualified Language.Haskell.Exts as Exts
import Language.Haskell.Exts hiding (Style, prettyPrint, Pretty, style, parse)
import Prelude
-- | Format the given source.
reformat :: Config -> Maybe [Extension] -> Maybe FilePath -> ByteString -> Either String Builder
reformat config mexts mfilepath =
preserveTrailingNewline
(fmap (mconcat . intersperse "\n") . mapM processBlock . cppSplitBlocks)
where
processBlock :: CodeBlock -> Either String Builder
processBlock (Shebang text) = Right $ S.byteString text
processBlock (CPPDirectives text) = Right $ S.byteString text
processBlock (HaskellSource line text) =
let ls = S8.lines text
prefix = findPrefix ls
code = unlines' (map (stripPrefix prefix) ls)
exts = readExtensions (UTF8.toString code)
mode'' = case exts of
Nothing -> mode'
Just (Nothing, exts') ->
mode' { extensions =
exts'
++ configExtensions config
++ extensions mode' }
Just (Just lang, exts') ->
mode' { baseLanguage = lang
, extensions =
exts'
++ configExtensions config
++ extensions mode' }
in case parseModuleWithComments mode'' (UTF8.toString code) of
ParseOk (m, comments) ->
fmap
(S.lazyByteString . addPrefix prefix . S.toLazyByteString)
(prettyPrint config m comments)
ParseFailed loc e ->
Left (Exts.prettyPrint (loc {srcLine = srcLine loc + line}) ++ ": " ++ e)
unlines' = S.concat . intersperse "\n"
unlines'' = L.concat . intersperse "\n"
addPrefix :: ByteString -> L8.ByteString -> L8.ByteString
addPrefix prefix = unlines'' . map (L8.fromStrict prefix <>) . L8.lines
stripPrefix :: ByteString -> ByteString -> ByteString
stripPrefix prefix line =
if S.null (S8.dropWhile (== '\n') line)
then line
else fromMaybe (error "Missing expected prefix") . s8_stripPrefix prefix $
line
findPrefix :: [ByteString] -> ByteString
findPrefix = takePrefix False . findSmallestPrefix . dropNewlines
dropNewlines :: [ByteString] -> [ByteString]
dropNewlines = filter (not . S.null . S8.dropWhile (== '\n'))
takePrefix :: Bool -> ByteString -> ByteString
takePrefix bracketUsed txt =
case S8.uncons txt of
Nothing -> ""
Just ('>', txt') ->
if not bracketUsed
then S8.cons '>' (takePrefix True txt')
else ""
Just (c, txt') ->
if c == ' ' || c == '\t'
then S8.cons c (takePrefix bracketUsed txt')
else ""
findSmallestPrefix :: [ByteString] -> ByteString
findSmallestPrefix [] = ""
findSmallestPrefix ("":_) = ""
findSmallestPrefix (p:ps) =
let first = S8.head p
startsWithChar c x = S8.length x > 0 && S8.head x == c
in if all (startsWithChar first) ps
then S8.cons
first
(findSmallestPrefix (S.tail p : map S.tail ps))
else ""
mode' =
let m = case mexts of
Just exts ->
parseMode
{ extensions = exts
}
Nothing -> parseMode
in m { parseFilename = fromMaybe "<interactive>" mfilepath }
preserveTrailingNewline f x =
if S8.null x || S8.all isSpace x
then return mempty
else if hasTrailingLine x || configTrailingNewline config
then fmap
(\x' ->
if hasTrailingLine
(L.toStrict (S.toLazyByteString x'))
then x'
else x' <> "\n")
(f x)
else f x
-- | Does the strict bytestring have a trailing newline?
hasTrailingLine :: ByteString -> Bool
hasTrailingLine xs =
if S8.null xs
then False
else S8.last xs == '\n'
-- | Print the module.
prettyPrint :: Config
-> Module SrcSpanInfo
-> [Comment]
-> Either a Builder
prettyPrint config m comments =
let ast =
evalState
(collectAllComments
(fromMaybe m (applyFixities baseFixities m)))
comments
in Right (runPrinterStyle config (pretty ast))
-- | Pretty print the given printable thing.
runPrinterStyle :: Config -> Printer () -> Builder
runPrinterStyle config m =
maybe
(error "Printer failed with mzero call.")
psOutput
(runIdentity
(runMaybeT
(execStateT
(runPrinter m)
(PrintState
{ psIndentLevel = 0
, psOutput = mempty
, psNewline = False
, psColumn = 0
, psLine = 1
, psConfig = config
, psInsideCase = False
, psFitOnOneLine = False
, psEolComment = False
}))))
-- | Parse mode, includes all extensions, doesn't assume any fixities.
parseMode :: ParseMode
parseMode =
defaultParseMode {extensions = allExtensions
,fixities = Nothing}
where allExtensions =
filter isDisabledExtension knownExtensions
isDisabledExtension (DisableExtension _) = False
isDisabledExtension _ = True
-- | Test the given file.
testFile :: FilePath -> IO ()
testFile fp = S.readFile fp >>= test
-- | Test the given file.
testFileAst :: FilePath -> IO ()
testFileAst fp = S.readFile fp >>= print . testAst
-- | Test with the given style, prints to stdout.
test :: ByteString -> IO ()
test =
either error (L8.putStrLn . S.toLazyByteString) .
reformat defaultConfig Nothing Nothing
-- | Parse the source and annotate it with comments, yielding the resulting AST.
testAst :: ByteString -> Either String (Module NodeInfo)
testAst x =
case parseModuleWithComments parseMode (UTF8.toString x) of
ParseOk (m,comments) ->
Right
(let ast =
evalState
(collectAllComments
(fromMaybe m (applyFixities baseFixities m)))
comments
in ast)
ParseFailed _ e -> Left e
-- | Default extensions.
defaultExtensions :: [Extension]
defaultExtensions =
[ e
| e@EnableExtension {} <- knownExtensions ] \\
map EnableExtension badExtensions
-- | Extensions which steal too much syntax.
badExtensions :: [KnownExtension]
badExtensions =
[Arrows -- steals proc
,TransformListComp -- steals the group keyword
,XmlSyntax, RegularPatterns -- steals a-b
,UnboxedTuples -- breaks (#) lens operator
-- ,QuasiQuotes -- breaks [x| ...], making whitespace free list comps break
,PatternSynonyms -- steals the pattern keyword
,RecursiveDo -- steals the rec keyword
,DoRec -- same
,TypeApplications -- since GHC 8 and haskell-src-exts-1.19
]
s8_stripPrefix :: ByteString -> ByteString -> Maybe ByteString
s8_stripPrefix bs1@(S.PS _ _ l1) bs2
| bs1 `S.isPrefixOf` bs2 = Just (S.unsafeDrop l1 bs2)
| otherwise = Nothing
--------------------------------------------------------------------------------
-- Extensions stuff stolen from hlint
-- | Consume an extensions list from arguments.
getExtensions :: [Text] -> [Extension]
getExtensions = foldl f defaultExtensions . map T.unpack
where f _ "Haskell98" = []
f a ('N':'o':x)
| Just x' <- readExtension x =
delete x' a
f a x
| Just x' <- readExtension x =
x' :
delete x' a
f _ x = error $ "Unknown extension: " ++ x
--------------------------------------------------------------------------------
-- Comments
-- | Traverse the structure backwards.
traverseInOrder
:: (Monad m, Traversable t, Functor m)
=> (b -> b -> Ordering) -> (b -> m b) -> t b -> m (t b)
traverseInOrder cmp f ast = do
indexed <-
fmap (zip [0 :: Integer ..] . reverse) (execStateT (traverse (modify . (:)) ast) [])
let sorted = sortBy (\(_,x) (_,y) -> cmp x y) indexed
results <-
mapM
(\(i,m) -> do
v <- f m
return (i, v))
sorted
evalStateT
(traverse
(const
(do i <- gets head
modify tail
case lookup i results of
Nothing -> error "traverseInOrder"
Just x -> return x))
ast)
[0 ..]
-- | Collect all comments in the module by traversing the tree. Read
-- this from bottom to top.
collectAllComments :: Module SrcSpanInfo -> State [Comment] (Module NodeInfo)
collectAllComments =
shortCircuit
(traverseBackwards
-- Finally, collect backwards comments which come after each node.
(collectCommentsBy
CommentAfterLine
(\nodeSpan commentSpan ->
fst (srcSpanStart commentSpan) >= fst (srcSpanEnd nodeSpan)))) <=<
shortCircuit addCommentsToTopLevelWhereClauses <=<
shortCircuit
(traverse
-- Collect forwards comments which start at the end line of a
-- node: Does the start line of the comment match the end-line
-- of the node?
(collectCommentsBy
CommentSameLine
(\nodeSpan commentSpan ->
fst (srcSpanStart commentSpan) == fst (srcSpanEnd nodeSpan)))) <=<
shortCircuit
(traverseBackwards
-- Collect backwards comments which are on the same line as a
-- node: Does the start line & end line of the comment match
-- that of the node?
(collectCommentsBy
CommentSameLine
(\nodeSpan commentSpan ->
fst (srcSpanStart commentSpan) == fst (srcSpanStart nodeSpan) &&
fst (srcSpanStart commentSpan) == fst (srcSpanEnd nodeSpan)))) <=<
shortCircuit
(traverse
-- First, collect forwards comments for declarations which both
-- start on column 1 and occur before the declaration.
(collectCommentsBy
CommentBeforeLine
(\nodeSpan commentSpan ->
(snd (srcSpanStart nodeSpan) == 1 &&
snd (srcSpanStart commentSpan) == 1) &&
fst (srcSpanStart commentSpan) < fst (srcSpanStart nodeSpan)))) .
fmap nodify
where
nodify s = NodeInfo s mempty
-- Sort the comments by their end position.
traverseBackwards =
traverseInOrder
(\x y -> on (flip compare) (srcSpanEnd . srcInfoSpan . nodeInfoSpan) x y)
-- Stop traversing if all comments have been consumed.
shortCircuit m v = do
comments <- get
if null comments
then return v
else m v
-- | Collect comments by satisfying the given predicate, to collect a
-- comment means to remove it from the pool of available comments in
-- the State. This allows for a multiple pass approach.
collectCommentsBy
:: (SrcSpan -> SomeComment -> NodeComment)
-> (SrcSpan -> SrcSpan -> Bool)
-> NodeInfo
-> State [Comment] NodeInfo
collectCommentsBy cons predicate nodeInfo@(NodeInfo (SrcSpanInfo nodeSpan _) _) = do
comments <- get
let (others, mine) =
partitionEithers
(map
(\comment@(Comment _ commentSpan _) ->
if predicate nodeSpan commentSpan
then Right comment
else Left comment)
comments)
put others
return $ addCommentsToNode cons mine nodeInfo
-- | Reintroduce comments which were immediately above declarations in where clauses.
-- Affects where clauses of top level declarations only.
addCommentsToTopLevelWhereClauses ::
Module NodeInfo -> State [Comment] (Module NodeInfo)
addCommentsToTopLevelWhereClauses (Module x x' x'' x''' topLevelDecls) =
Module x x' x'' x''' <$>
traverse addCommentsToWhereClauses topLevelDecls
where
addCommentsToWhereClauses ::
Decl NodeInfo -> State [Comment] (Decl NodeInfo)
addCommentsToWhereClauses (PatBind x x' x'' (Just (BDecls x''' whereDecls))) = do
newWhereDecls <- traverse addCommentsToPatBind whereDecls
return $ PatBind x x' x'' (Just (BDecls x''' newWhereDecls))
addCommentsToWhereClauses other = return other
addCommentsToPatBind :: Decl NodeInfo -> State [Comment] (Decl NodeInfo)
addCommentsToPatBind (PatBind bindInfo (PVar x (Ident declNodeInfo declString)) x' x'') = do
bindInfoWithComments <- addCommentsBeforeNode bindInfo
return $
PatBind
bindInfoWithComments
(PVar x (Ident declNodeInfo declString))
x'
x''
addCommentsToPatBind other = return other
addCommentsBeforeNode :: NodeInfo -> State [Comment] NodeInfo
addCommentsBeforeNode nodeInfo = do
comments <- get
let (notAbove, above) = partitionAboveNotAbove comments nodeInfo
put notAbove
return $ addCommentsToNode CommentBeforeLine above nodeInfo
partitionAboveNotAbove :: [Comment] -> NodeInfo -> ([Comment], [Comment])
partitionAboveNotAbove cs (NodeInfo (SrcSpanInfo nodeSpan _) _) =
fst $
foldr'
(\comment@(Comment _ commentSpan _) ((ls, rs), lastSpan) ->
if comment `isAbove` lastSpan
then ((ls, comment : rs), commentSpan)
else ((comment : ls, rs), lastSpan))
(([], []), nodeSpan)
cs
isAbove :: Comment -> SrcSpan -> Bool
isAbove (Comment _ commentSpan _) span =
let (_, commentColStart) = srcSpanStart commentSpan
(commentLnEnd, _) = srcSpanEnd commentSpan
(lnStart, colStart) = srcSpanStart span
in commentColStart == colStart && commentLnEnd + 1 == lnStart
addCommentsToTopLevelWhereClauses other = return other
addCommentsToNode :: (SrcSpan -> SomeComment -> NodeComment)
-> [Comment]
-> NodeInfo
-> NodeInfo
addCommentsToNode mkNodeComment newComments nodeInfo@(NodeInfo (SrcSpanInfo _ _) existingComments) =
nodeInfo
{nodeInfoComments = existingComments <> map mkBeforeNodeComment newComments}
where
mkBeforeNodeComment :: Comment -> NodeComment
mkBeforeNodeComment (Comment multiLine commentSpan commentString) =
mkNodeComment
commentSpan
((if multiLine
then MultiLine
else EndOfLine)
commentString)