-
Notifications
You must be signed in to change notification settings - Fork 679
Expand file tree
/
Copy pathDiff.hs
More file actions
383 lines (286 loc) · 9.52 KB
/
Diff.hs
File metadata and controls
383 lines (286 loc) · 9.52 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
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
{-# LANGUAGE OverloadedStrings #-}
module Deps.Diff
( diff
, PackageChanges(..)
, ModuleChanges(..)
, Changes(..)
, moduleChangeMagnitude
, toMagnitude
, bump
, getDocs
)
where
import Control.Monad (zipWithM)
import Data.Function (on)
import qualified Data.List as List
import qualified Data.Map as Map
import qualified Data.Name as Name
import qualified Data.Set as Set
import qualified System.Directory as Dir
import System.FilePath ((</>))
import qualified Deps.Website as Website
import qualified Elm.Compiler.Type as Type
import qualified Elm.Docs as Docs
import qualified Elm.Magnitude as M
import qualified Elm.ModuleName as ModuleName
import qualified Elm.Package as Pkg
import qualified Elm.Version as V
import qualified File
import qualified Http
import qualified Json.Decode as D
import qualified Reporting.Exit as Exit
import qualified Stuff
-- CHANGES
data PackageChanges =
PackageChanges
{ _modules_added :: [ModuleName.Raw]
, _modules_changed :: Map.Map ModuleName.Raw ModuleChanges
, _modules_removed :: [ModuleName.Raw]
}
data ModuleChanges =
ModuleChanges
{ _unions :: Changes Name.Name Docs.Union
, _aliases :: Changes Name.Name Docs.Alias
, _values :: Changes Name.Name Docs.Value
, _binops :: Changes Name.Name Docs.Binop
}
data Changes k v =
Changes
{ _added :: Map.Map k v
, _changed :: Map.Map k (v,v)
, _removed :: Map.Map k v
}
getChanges :: (Ord k) => (v -> v -> Bool) -> Map.Map k v -> Map.Map k v -> Changes k v
getChanges isEquivalent old new =
let
overlap = Map.intersectionWith (,) old new
changed = Map.filter (not . uncurry isEquivalent) overlap
in
Changes (Map.difference new old) changed (Map.difference old new)
-- DIFF
diff :: Docs.Documentation -> Docs.Documentation -> PackageChanges
diff oldDocs newDocs =
let
filterOutPatches chngs =
Map.filter (\chng -> moduleChangeMagnitude chng /= M.PATCH) chngs
(Changes added changed removed) =
getChanges (\_ _ -> False) oldDocs newDocs
in
PackageChanges
(Map.keys added)
(filterOutPatches (Map.map diffModule changed))
(Map.keys removed)
diffModule :: (Docs.Module, Docs.Module) -> ModuleChanges
diffModule (Docs.Module _ _ u1 a1 v1 b1, Docs.Module _ _ u2 a2 v2 b2) =
ModuleChanges
(getChanges isEquivalentUnion u1 u2)
(getChanges isEquivalentAlias a1 a2)
(getChanges isEquivalentValue v1 v2)
(getChanges isEquivalentBinop b1 b2)
-- EQUIVALENCE
isEquivalentUnion :: Docs.Union -> Docs.Union -> Bool
isEquivalentUnion (Docs.Union oldComment oldVars oldCtors) (Docs.Union newComment newVars newCtors) =
length oldCtors == length newCtors
&& and (zipWith (==) (map fst oldCtors) (map fst newCtors))
&& and (Map.elems (Map.intersectionWith equiv (Map.fromList oldCtors) (Map.fromList newCtors)))
where
equiv :: [Type.Type] -> [Type.Type] -> Bool
equiv oldTypes newTypes =
let
allEquivalent =
zipWith
isEquivalentAlias
(map (Docs.Alias oldComment oldVars) oldTypes)
(map (Docs.Alias newComment newVars) newTypes)
in
length oldTypes == length newTypes
&& and allEquivalent
isEquivalentAlias :: Docs.Alias -> Docs.Alias -> Bool
isEquivalentAlias (Docs.Alias _ oldVars oldType) (Docs.Alias _ newVars newType) =
case diffType oldType newType of
Nothing ->
False
Just renamings ->
length oldVars == length newVars
&& isEquivalentRenaming (zip oldVars newVars ++ renamings)
isEquivalentValue :: Docs.Value -> Docs.Value -> Bool
isEquivalentValue (Docs.Value c1 t1) (Docs.Value c2 t2) =
isEquivalentAlias (Docs.Alias c1 [] t1) (Docs.Alias c2 [] t2)
isEquivalentBinop :: Docs.Binop -> Docs.Binop -> Bool
isEquivalentBinop (Docs.Binop c1 t1 a1 p1) (Docs.Binop c2 t2 a2 p2) =
isEquivalentAlias (Docs.Alias c1 [] t1) (Docs.Alias c2 [] t2)
&& a1 == a2
&& p1 == p2
-- DIFF TYPES
diffType :: Type.Type -> Type.Type -> Maybe [(Name.Name,Name.Name)]
diffType oldType newType =
case (oldType, newType) of
(Type.Var oldName, Type.Var newName) ->
Just [(oldName, newName)]
(Type.Lambda a b, Type.Lambda a' b') ->
(++)
<$> diffType a a'
<*> diffType b b'
(Type.Type oldName oldArgs, Type.Type newName newArgs) ->
if not (isSameName oldName newName) || length oldArgs /= length newArgs then
Nothing
else
concat <$> zipWithM diffType oldArgs newArgs
(Type.Record fields maybeExt, Type.Record fields' maybeExt') ->
case (maybeExt, maybeExt') of
(Nothing, Just _) ->
Nothing
(Just _, Nothing) ->
Nothing
(Nothing, Nothing) ->
diffFields fields fields'
(Just oldExt, Just newExt) ->
(:) (oldExt, newExt) <$> diffFields fields fields'
(Type.Unit, Type.Unit) ->
Just []
(Type.Tuple a b cs, Type.Tuple x y zs) ->
if length cs /= length zs then
Nothing
else
do aVars <- diffType a x
bVars <- diffType b y
cVars <- concat <$> zipWithM diffType cs zs
return (aVars ++ bVars ++ cVars)
(_, _) ->
Nothing
-- handle very old docs that do not use qualified names
isSameName :: Name.Name -> Name.Name -> Bool
isSameName oldFullName newFullName =
let
dedot name =
reverse (Name.splitDots name)
in
case ( dedot oldFullName, dedot newFullName ) of
(oldName:[], newName:_) ->
oldName == newName
(oldName:_, newName:[]) ->
oldName == newName
_ ->
oldFullName == newFullName
diffFields :: [(Name.Name, Type.Type)] -> [(Name.Name, Type.Type)] -> Maybe [(Name.Name,Name.Name)]
diffFields oldRawFields newRawFields =
let
sort = List.sortBy (compare `on` fst)
oldFields = sort oldRawFields
newFields = sort newRawFields
in
if length oldRawFields /= length newRawFields then
Nothing
else if or (zipWith ((/=) `on` fst) oldFields newFields) then
Nothing
else
concat <$> zipWithM (diffType `on` snd) oldFields newFields
-- TYPE VARIABLES
isEquivalentRenaming :: [(Name.Name,Name.Name)] -> Bool
isEquivalentRenaming varPairs =
let
renamings =
Map.toList (foldr insert Map.empty varPairs)
insert (old,new) dict =
Map.insertWith (++) old [new] dict
verify (old, news) =
case news of
[] ->
Nothing
new : rest ->
if all (new ==) rest then
Just (old, new)
else
Nothing
allUnique list =
length list == Set.size (Set.fromList list)
in
case mapM verify renamings of
Nothing ->
False
Just verifiedRenamings ->
all compatibleVars verifiedRenamings
&&
allUnique (map snd verifiedRenamings)
compatibleVars :: (Name.Name, Name.Name) -> Bool
compatibleVars (old, new) =
case (categorizeVar old, categorizeVar new) of
(CompAppend, CompAppend) -> True
(Comparable, Comparable) -> True
(Appendable, Appendable) -> True
(Number , Number ) -> True
(Number , Comparable) -> True
(_, Var) -> True
(_, _) -> False
data TypeVarCategory
= CompAppend
| Comparable
| Appendable
| Number
| Var
categorizeVar :: Name.Name -> TypeVarCategory
categorizeVar name
| Name.isCompappendType name = CompAppend
| Name.isComparableType name = Comparable
| Name.isAppendableType name = Appendable
| Name.isNumberType name = Number
| otherwise = Var
-- MAGNITUDE
bump :: PackageChanges -> V.Version -> V.Version
bump changes version =
case toMagnitude changes of
M.PATCH ->
V.bumpPatch version
M.MINOR ->
V.bumpMinor version
M.MAJOR ->
V.bumpMajor version
toMagnitude :: PackageChanges -> M.Magnitude
toMagnitude (PackageChanges added changed removed) =
let
addMag = if null added then M.PATCH else M.MINOR
removeMag = if null removed then M.PATCH else M.MAJOR
changeMags = map moduleChangeMagnitude (Map.elems changed)
in
maximum (addMag : removeMag : changeMags)
moduleChangeMagnitude :: ModuleChanges -> M.Magnitude
moduleChangeMagnitude (ModuleChanges unions aliases values binops) =
maximum
[ changeMagnitude unions
, changeMagnitude aliases
, changeMagnitude values
, changeMagnitude binops
]
changeMagnitude :: Changes k v -> M.Magnitude
changeMagnitude (Changes added changed removed) =
if Map.size removed > 0 || Map.size changed > 0 then
M.MAJOR
else if Map.size added > 0 then
M.MINOR
else
M.PATCH
-- GET DOCS
getDocs :: Stuff.PackageCache -> Http.Manager -> Pkg.Name -> V.Version -> IO (Either Exit.DocsProblem Docs.Documentation)
getDocs cache manager name version =
do let home = Stuff.package cache name version
let path = home </> "docs.json"
exists <- File.exists path
if exists
then
do bytes <- File.readUtf8 path
case D.fromByteString Docs.decoder bytes of
Right docs ->
return $ Right docs
Left _ ->
do File.remove path
return $ Left Exit.DP_Cache
else
do let url = Website.metadata name version "docs.json"
Http.get manager url [] Exit.DP_Http $ \body ->
case D.fromByteString Docs.decoder body of
Right docs ->
do Dir.createDirectoryIfMissing True home
File.writeUtf8 path body
return $ Right docs
Left _ ->
return $ Left $ Exit.DP_Data url body