forked from koalaman/shellcheck
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDiff.hs
More file actions
258 lines (216 loc) · 8.82 KB
/
Diff.hs
File metadata and controls
258 lines (216 loc) · 8.82 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
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
{-
Copyright 2019 Vidar 'koala_man' Holen
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Formatter.Diff (format, ShellCheck.Formatter.Diff.runTests) where
import ShellCheck.Interface
import ShellCheck.Fixer
import ShellCheck.Formatter.Format
import Control.Monad
import Data.Algorithm.Diff
import Data.Array
import Data.IORef
import Data.List
import qualified Data.Monoid as Monoid
import Data.Maybe
import qualified Data.Map as M
import GHC.Exts (sortWith)
import System.IO
import System.FilePath
import Test.QuickCheck
import Debug.Trace
ltt x = trace (show x) x
format :: FormatterOptions -> IO Formatter
format options = do
foundIssues <- newIORef False
reportedIssues <- newIORef False
shouldColor <- shouldOutputColor (foColorOption options)
let color = if shouldColor then colorize else nocolor
return Formatter {
header = return (),
footer = checkFooter foundIssues reportedIssues color,
onFailure = reportFailure color,
onResult = reportResult foundIssues reportedIssues color
}
contextSize = 3
red = 31
green = 32
yellow = 33
cyan = 36
bold = 1
nocolor n = id
colorize n s = (ansi n) ++ s ++ (ansi 0)
ansi n = "\x1B[" ++ show n ++ "m"
printErr :: ColorFunc -> String -> IO ()
printErr color = hPutStrLn stderr . color bold . color red
reportFailure color file msg = printErr color $ file ++ ": " ++ msg
checkFooter foundIssues reportedIssues color = do
found <- readIORef foundIssues
output <- readIORef reportedIssues
when (found && not output) $
printErr color "Issues were detected, but none were auto-fixable. Use another format to see them."
type ColorFunc = (Int -> String -> String)
data LFStatus = LinefeedMissing | LinefeedOk
data DiffDoc a = DiffDoc String LFStatus [DiffRegion a]
data DiffRegion a = DiffRegion (Int, Int) (Int, Int) [Diff a]
reportResult :: (IORef Bool) -> (IORef Bool) -> ColorFunc -> CheckResult -> SystemInterface IO -> IO ()
reportResult foundIssues reportedIssues color result sys = do
let comments = crComments result
unless (null comments) $ writeIORef foundIssues True
let suggestedFixes = mapMaybe pcFix comments
let fixmap = buildFixMap suggestedFixes
mapM_ output $ M.toList fixmap
where
output (name, fix) = do
file <- (siReadFile sys) name
case file of
Right contents -> do
putStrLn $ formatDoc color $ makeDiff name contents fix
writeIORef reportedIssues True
Left msg -> reportFailure color name msg
hasTrailingLinefeed str =
case str of
[] -> True
_ -> last str == '\n'
coversLastLine regions =
case regions of
[] -> False
_ -> (fst $ last regions)
-- TODO: Factor this out into a unified diff library because we're doing a lot
-- of the heavy lifting anyways.
makeDiff :: String -> String -> Fix -> DiffDoc String
makeDiff name contents fix = do
let hunks = groupDiff $ computeDiff contents fix
let lf = if coversLastLine hunks && not (hasTrailingLinefeed contents)
then LinefeedMissing
else LinefeedOk
DiffDoc name lf $ findRegions hunks
computeDiff :: String -> Fix -> [Diff String]
computeDiff contents fix =
let old = lines contents
array = listArray (1, fromIntegral $ (length old)) old
new = applyFix fix array
in getDiff old new
-- Group changes into hunks
groupDiff :: [Diff a] -> [(Bool, [Diff a])]
groupDiff = filter (\(_, l) -> not (null l)) . hunt []
where
-- Churn through 'Both's until we find a difference
hunt current [] = [(False, reverse current)]
hunt current (x@Both {}:rest) = hunt (x:current) rest
hunt current list =
let (context, previous) = splitAt contextSize current
in (False, reverse previous) : gather context 0 list
-- Pick out differences until we find a run of Both's
gather current n [] =
let (extras, patch) = splitAt (max 0 $ n - contextSize) current
in [(True, reverse patch), (False, reverse extras)]
gather current n list@(Both {}:_) | n == contextSize*2 =
let (context, previous) = splitAt contextSize current
in (True, reverse previous) : hunt context list
gather current n (x@Both {}:rest) = gather (x:current) (n+1) rest
gather current n (x:rest) = gather (x:current) 0 rest
-- Get line numbers for hunks
findRegions :: [(Bool, [Diff String])] -> [DiffRegion String]
findRegions = find' 1 1
where
find' _ _ [] = []
find' left right ((output, run):rest) =
let (dl, dr) = countDelta run
remainder = find' (left+dl) (right+dr) rest
in
if output
then DiffRegion (left, dl) (right, dr) run : remainder
else remainder
-- Get left/right line counts for a hunk
countDelta :: [Diff a] -> (Int, Int)
countDelta = count' 0 0
where
count' left right [] = (left, right)
count' left right (x:rest) =
case x of
Both {} -> count' (left+1) (right+1) rest
First {} -> count' (left+1) right rest
Second {} -> count' left (right+1) rest
formatRegion :: ColorFunc -> LFStatus -> DiffRegion String -> String
formatRegion color lf (DiffRegion left right diffs) =
let header = color cyan ("@@ -" ++ (tup left) ++ " +" ++ (tup right) ++" @@")
in
unlines $ header : reverse (getStrings lf (reverse diffs))
where
noLF = "\\ No newline at end of file"
getStrings LinefeedOk list = map format list
getStrings LinefeedMissing list@((Both _ _):_) = noLF : map format list
getStrings LinefeedMissing list@((First _):_) = noLF : map format list
getStrings LinefeedMissing (last:rest) = format last : getStrings LinefeedMissing rest
tup (a,b) = (show a) ++ "," ++ (show b)
format (Both x _) = ' ':x
format (First x) = color red $ '-':x
format (Second x) = color green $ '+':x
splitLast [] = ([], [])
splitLast x =
let (last, rest) = splitAt 1 $ reverse x
in (reverse rest, last)
formatDoc color (DiffDoc name lf regions) =
let (most, last) = splitLast regions
in
(color bold $ "--- " ++ ("a" </> name)) ++ "\n" ++
(color bold $ "+++ " ++ ("b" </> name)) ++ "\n" ++
concatMap (formatRegion color LinefeedOk) most ++
concatMap (formatRegion color lf) last
-- Create a Map from filename to Fix
buildFixMap :: [Fix] -> M.Map String Fix
buildFixMap fixes = perFile
where
splitFixes = concatMap splitFixByFile fixes
perFile = groupByMap (posFile . repStartPos . head . fixReplacements) splitFixes
-- There are currently no multi-file fixes, but let's handle it anyways
splitFixByFile :: Fix -> [Fix]
splitFixByFile fix = map makeFix $ groupBy sameFile (fixReplacements fix)
where
sameFile rep1 rep2 = (posFile $ repStartPos rep1) == (posFile $ repStartPos rep2)
makeFix reps = newFix { fixReplacements = reps }
groupByMap :: (Ord k, Monoid v) => (v -> k) -> [v] -> M.Map k v
groupByMap f = M.fromListWith Monoid.mappend . map (\x -> (f x, x))
-- For building unit tests
b n = Both n n
l = First
r = Second
prop_identifiesProperContext = groupDiff [b 1, b 2, b 3, b 4, l 5, b 6, b 7, b 8, b 9] ==
[(False, [b 1]), -- Omitted
(True, [b 2, b 3, b 4, l 5, b 6, b 7, b 8]), -- A change with three lines of context
(False, [b 9])] -- Omitted
prop_includesContextFromStartIfNecessary = groupDiff [b 4, l 5, b 6, b 7, b 8, b 9] ==
[ -- Nothing omitted
(True, [b 4, l 5, b 6, b 7, b 8]), -- A change with three lines of context
(False, [b 9])] -- Omitted
prop_includesContextUntilEndIfNecessary = groupDiff [b 4, l 5] ==
[ -- Nothing omitted
(True, [b 4, l 5])
] -- Nothing Omitted
prop_splitsIntoMultipleHunks = groupDiff [l 1, b 1, b 2, b 3, b 4, b 5, b 6, b 7, r 8] ==
[ -- Nothing omitted
(True, [l 1, b 1, b 2, b 3]),
(False, [b 4]),
(True, [b 5, b 6, b 7, r 8])
] -- Nothing Omitted
prop_splitsIntoMultipleHunksUnlessTouching = groupDiff [l 1, b 1, b 2, b 3, b 4, b 5, b 6, r 7] ==
[
(True, [l 1, b 1, b 2, b 3, b 4, b 5, b 6, r 7])
]
prop_countDeltasWorks = countDelta [b 1, l 2, r 3, r 4, b 5] == (3,4)
prop_countDeltasWorks2 = countDelta [] == (0,0)
return []
runTests = $quickCheckAll