-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathFixWhitespace.hs
More file actions
164 lines (142 loc) · 5.07 KB
/
FixWhitespace.hs
File metadata and controls
164 lines (142 loc) · 5.07 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
{-# LANGUAGE OverloadedStrings #-}
module Data.Text.FixWhitespace
( CheckResult(..)
, checkFile
, LineError(..)
, displayLineError
, transform
, transformWithLog
, TabSize
, Verbose
, defaultTabSize
)
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.Maybe ( isJust )
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 = Maybe Int
type TabSize = Int
-- | Default tab size.
--
defaultTabSize :: TabSize
defaultTabSize = 8
-- | 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 -> Verbose -> FilePath -> IO CheckResult
checkFile tabSize verbose f =
handle (\ (e :: IOException) -> return $ CheckIOError e) $
withFile f ReadMode $ \ h -> do
hSetEncoding h utf8
s <- Text.hGetContents h
let (s', lvs)
| isJust verbose = transformWithLog tabSize s
| otherwise = (transform tabSize s, [])
return $ if s' == s then CheckOK else CheckViolation s' lvs
transform
:: TabSize -- ^ Expand tab characters to so many spaces. Keep tabs if @<= 0@.
-> Text -- ^ Text before transformation.
-> Text -- ^ Text after transformation.
transform tabSize =
Text.unlines .
removeFinalEmptyLinesExceptOne .
map (removeTrailingWhitespace . convertTabs tabSize) .
Text.lines
where
removeFinalEmptyLinesExceptOne =
reverse . dropWhile1 Text.null . reverse
-- | 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@.
-> Text -- ^ Text before transformation.
-> (Text, [LineError]) -- ^ Text after transformation and violating lines if any.
transformWithLog tabSize =
runWriter .
fmap Text.unlines .
fixAllViolations .
zip [1..] .
Text.lines
where
fixAllViolations :: [(Int,Text)] -> TransformM [Text]
fixAllViolations =
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') ""
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]