forked from agda/fix-whitespace
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFixWhitespace.hs
More file actions
206 lines (178 loc) · 6.68 KB
/
FixWhitespace.hs
File metadata and controls
206 lines (178 loc) · 6.68 KB
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
{-# LANGUAGE OverloadedStrings #-}
module Data.Text.FixWhitespace
( CheckResult(..)
, checkFile
, LineError(..)
, displayLineError
, transform
, transformWithLog
, TabSize
, ConsecutiveEmptyLines
, Verbose
, defaultTabSize
, defaultConsecutiveEmptyLines
)
where
import Control.Monad ( (<=<) )
import Control.Monad.Trans.Writer.Strict ( Writer, runWriter, tell )
import Control.Exception ( IOException, handle )
import Data.Char ( GeneralCategory(Space, Format), generalCategory )
import Data.Text ( Text )
import qualified Data.Text as Text
import qualified Data.Text.IO as Text {- Strict IO -}
import System.IO ( IOMode(ReadMode), hSetEncoding, utf8, withFile )
import Data.List.Extra.Drop ( dropWhileEnd1, dropWhile1 )
type Verbose = Bool
type TabSize = Int
type ConsecutiveEmptyLines = Int
-- | Default tab size.
--
defaultTabSize :: TabSize
defaultTabSize = 8
-- | Maximum consecutive empty lines
defaultConsecutiveEmptyLines :: ConsecutiveEmptyLines
defaultConsecutiveEmptyLines = 0
-- | Result of checking a file against the whitespace policy.
--
data CheckResult
= CheckOK
-- ^ The file satifies the policy.
| CheckViolation Text [LineError]
-- ^ The file violates the policy, a fix and a list of
-- violating lines are returned.
| CheckIOError IOException
-- ^ An I/O error occurred while accessing the file.
-- (E.g., the file is not UTF8 encoded.)
-- | Represents a line of input violating whitespace rules.
-- Stores the index of the line and the line itself.
data LineError = LineError Int Text
-- | Check a file against the whitespace policy,
-- returning a fix if violations occurred.
--
checkFile :: TabSize -> ConsecutiveEmptyLines -> Verbose -> FilePath -> IO CheckResult
checkFile tabSize consecutiveLines verbose f =
handle (\ (e :: IOException) -> return $ CheckIOError e) $
withFile f ReadMode $ \ h -> do
hSetEncoding h utf8
s <- Text.hGetContents h
let (s', lvs)
| verbose = transformWithLog tabSize consecutiveLines s
| otherwise = (transform tabSize consecutiveLines s, [])
return $ if s' == s then CheckOK else CheckViolation s' lvs
transform
:: TabSize -- ^ Expand tab characters to so many spaces. Keep tabs if @<= 0@.
-> ConsecutiveEmptyLines -- ^ Maximum count of consecutive empty lines. Unlimited if @<= 0@.
-> Text -- ^ Text before transformation.
-> Text -- ^ Text after transformation.
transform tabSize consecutiveLines =
Text.unlines .
(if consecutiveLines > 0 then squashConsecutiveEmptyLines 0 else id) .
removeFinalEmptyLinesExceptOne .
map (removeTrailingWhitespace . convertTabs tabSize) .
Text.lines
where
removeFinalEmptyLinesExceptOne =
reverse . dropWhile1 Text.null . reverse
squashConsecutiveEmptyLines :: Int -> [Text] -> [Text]
squashConsecutiveEmptyLines _ [] = []
squashConsecutiveEmptyLines n (l:ls)
| Text.null l
= if n >= consecutiveLines
then squashConsecutiveEmptyLines n ls
else
l : squashConsecutiveEmptyLines (n + 1) ls
| otherwise
= l : squashConsecutiveEmptyLines 0 ls
-- | The transformation monad: maintains info about lines that
-- violate the rules. Used in the verbose mode to build a log.
--
type TransformM = Writer [LineError]
-- | Transforms the contents of a file.
--
transformWithLog
:: TabSize -- ^ Expand tab characters to so many spaces. Keep tabs if @<= 0@.
-> ConsecutiveEmptyLines -- ^ Maximum count of consecutive empty lines. Unlimited if @<= 0@.
-> Text -- ^ Text before transformation.
-> (Text, [LineError]) -- ^ Text after transformation and violating lines if any.
transformWithLog tabSize consecutiveLines =
runWriter .
fmap Text.unlines .
fixAllViolations .
zip [1..] .
Text.lines
where
fixAllViolations :: [(Int,Text)] -> TransformM [Text]
fixAllViolations =
(if consecutiveLines > 0 then squashConsecutiveEmptyLines 1 0 else return)
<=<
removeFinalEmptyLinesExceptOne
<=<
mapM (fixLineWith $ removeTrailingWhitespace . convertTabs tabSize)
removeFinalEmptyLinesExceptOne :: [Text] -> TransformM [Text]
removeFinalEmptyLinesExceptOne ls
| lenLs == lenLs' = pure ls
| otherwise = do
tell $ zipWith LineError [1+lenLs' ..] els
pure ls'
where
ls' = dropWhileEnd1 Text.null ls
lenLs = length ls
lenLs' = length ls'
els = replicate (lenLs - lenLs') ""
squashConsecutiveEmptyLines :: Int -> Int -> [Text] -> TransformM [Text]
squashConsecutiveEmptyLines _ _ [] = return []
squashConsecutiveEmptyLines i n (l:ls)
| Text.null l
= if n >= consecutiveLines
then do
tell [LineError i l]
squashConsecutiveEmptyLinesAfterError (i + 1) ls
else
(l:) <$> squashConsecutiveEmptyLines (i + 1) (n + 1) ls
| otherwise
= (l:) <$> squashConsecutiveEmptyLines (i + 1) 0 ls
squashConsecutiveEmptyLinesAfterError _ [] = return []
squashConsecutiveEmptyLinesAfterError i (l:ls)
| Text.null l = squashConsecutiveEmptyLinesAfterError (i + 1) ls
| otherwise = squashConsecutiveEmptyLines i 0 (l:ls)
fixLineWith :: (Text -> Text) -> (Int, Text) -> TransformM Text
fixLineWith fixer (i, l)
| l == l' = pure l
| otherwise = do
tell [LineError i l]
pure l'
where
l' = fixer l
removeTrailingWhitespace :: Text -> Text
removeTrailingWhitespace =
Text.dropWhileEnd $ \ c -> generalCategory c `elem` [Space,Format] || c == '\t'
convertTabs :: TabSize -> Text -> Text
convertTabs tabSize = if tabSize <= 0 then id else
Text.pack . reverse . fst . foldl (convertOne tabSize) ([], 0) . Text.unpack
convertOne :: TabSize -> (String, Int) -> Char -> (String, Int)
convertOne tabSize (a, p) '\t' = (addSpaces n a, p + n)
where
n = tabSize - p `mod` tabSize -- Here, tabSize > 0 is guaranteed
convertOne _tabSize (a, p) c = (c:a, p+1)
addSpaces :: Int -> String -> String
addSpaces n = (replicate n ' ' ++)
-- | Print a erroneous line with 'visibleSpaces'.
--
displayLineError :: FilePath -> LineError -> Text
displayLineError fname (LineError i l) = Text.concat
[ Text.pack fname
, ":"
, Text.pack $ show i
, ": "
, visibleSpaces l
]
-- | Replace spaces and tabs with visible characters for presentation purposes.
-- Space turns into '·' and tab into '<TAB>'.
--
visibleSpaces :: Text -> Text
visibleSpaces s
| Text.null s = "<NEWLINE>"
| otherwise = flip Text.concatMap s $ \case
'\t' -> "<TAB>"
' ' -> "·"
c -> Text.pack [c]