forked from haskell/haskell-language-server
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathMain.hs
406 lines (318 loc) · 17.1 KB
/
Main.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
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeOperators #-}
module Main
( main
) where
import Control.Lens ((^.))
import Data.Aeson (Value (..), object, toJSON, (.=))
import Data.List (find)
import qualified Data.Map as Map
import Data.Maybe (fromJust, isJust)
import qualified Data.Text as T
import Ide.Plugin.Config (Config (..), PluginConfig (..),
hlintOn)
import qualified Ide.Plugin.Config as Plugin
import qualified Ide.Plugin.Hlint as HLint
import qualified Language.LSP.Types.Lens as L
import System.FilePath ((</>))
import Test.Hls
main :: IO ()
main = defaultTestRunner tests
hlintPlugin :: PluginDescriptor IdeState
hlintPlugin = HLint.descriptor "hlint"
tests :: TestTree
tests = testGroup "hlint" [
suggestionsTests
, configTests
, ignoreHintTests
]
getIgnoreHintText :: T.Text -> T.Text
getIgnoreHintText name = "Ignore hint \"" <> name <> "\" in this module"
ignoreHintTests :: TestTree
ignoreHintTests = testGroup "hlint ignore hint tests"
[
ignoreGoldenTest
"Ignore hint in this module inserts -Wno-unrecognised-pragmas and hlint ignore pragma if warn unrecognized pragmas is off"
"UnrecognizedPragmasOff"
(Point 3 8)
"Eta reduce"
, ignoreGoldenTest
"Ignore hint in this module inserts only hlint ignore pragma if warn unrecognized pragmas is on"
"UnrecognizedPragmasOn"
(Point 3 9)
"Eta reduce"
]
suggestionsTests :: TestTree
suggestionsTests =
testGroup "hlint suggestions" [
testCase "provides 3.8 code actions including apply all" $ runHlintSession "" $ do
doc <- openDoc "Base.hs" "haskell"
diags@(reduceDiag:_) <- waitForDiagnosticsFromSource doc "hlint"
liftIO $ do
length diags @?= 2 -- "Eta Reduce" and "Redundant Id"
reduceDiag ^. L.range @?= Range (Position 1 0) (Position 1 12)
reduceDiag ^. L.severity @?= Just DsInfo
reduceDiag ^. L.code @?= Just (InR "refact:Eta reduce")
reduceDiag ^. L.source @?= Just "hlint"
cas <- map fromAction <$> getAllCodeActions doc
let redundantIdHintName = "Redundant id"
let etaReduceHintName = "Eta reduce"
let applyAll = find (\ca -> "Apply all hints" `T.isSuffixOf` (ca ^. L.title)) cas
let redId = find (\ca -> redundantIdHintName `T.isInfixOf` (ca ^. L.title)) cas
let redEta = find (\ca -> etaReduceHintName `T.isInfixOf` (ca ^. L.title)) cas
let ignoreRedundantIdInThisModule = find (\ca -> getIgnoreHintText redundantIdHintName == (ca ^.L.title)) cas
let ignoreEtaReduceThisModule = find (\ca -> getIgnoreHintText etaReduceHintName == (ca ^.L.title)) cas
liftIO $ isJust applyAll @? "There is Apply all hints code action"
liftIO $ isJust redId @? "There is Redundant id code action"
liftIO $ isJust redEta @? "There is Eta reduce code action"
liftIO $ isJust ignoreRedundantIdInThisModule @? "There is ignore Redundant id code action"
liftIO $ isJust ignoreEtaReduceThisModule @? "There is ignore Eta reduce code action"
executeCodeAction (fromJust redId)
contents <- skipManyTill anyMessage $ getDocumentEdit doc
liftIO $ contents @?= "main = undefined\nfoo x = x\n"
, testCase "falls back to pre 3.8 code actions" $ runSessionWithServer' [hlintPlugin] def def noLiteralCaps "test/testdata" $ do
doc <- openDoc "Base.hs" "haskell"
_ <- waitForDiagnosticsFromSource doc "hlint"
cars <- getAllCodeActions doc
etaReduce <- liftIO $ inspectCommand cars ["Eta reduce"]
executeCommand etaReduce
contents <- skipManyTill anyMessage $ getDocumentEdit doc
liftIO $ contents @?= "main = undefined\nfoo = id\n"
, testCase ".hlint.yaml fixity rules are applied" $ runHlintSession "fixity" $ do
doc <- openDoc "FixityUse.hs" "haskell"
expectNoMoreDiagnostics 3 doc "hlint"
, testCase "changing document contents updates hlint diagnostics" $ runHlintSession "" $ do
doc <- openDoc "Base.hs" "haskell"
testHlintDiagnostics doc
let change = TextDocumentContentChangeEvent
(Just (Range (Position 1 8) (Position 1 12)))
Nothing "x"
changeDoc doc [change]
expectNoMoreDiagnostics 3 doc "hlint"
let change' = TextDocumentContentChangeEvent
(Just (Range (Position 1 8) (Position 1 12)))
Nothing "id x"
changeDoc doc [change']
testHlintDiagnostics doc
, knownBrokenForHlintOnGhcLib "hlint doesn't take in account cpp flag as ghc -D argument" $
testCase "[#554] hlint diagnostics works with CPP via ghc -XCPP argument" $ runHlintSession "cpp" $ do
doc <- openDoc "CppCond.hs" "haskell"
testHlintDiagnostics doc
, knownBrokenForHlintOnGhcLib "hlint doesn't take in account cpp flag as ghc -D argument" $
testCase "[#554] hlint diagnostics works with CPP via language pragma" $ runHlintSession "" $ do
doc <- openDoc "CppCond.hs" "haskell"
testHlintDiagnostics doc
, testCase "[#554] hlint diagnostics works with CPP via -XCPP argument and flag via #include header" $ runHlintSession "cpp" $ do
doc <- openDoc "CppHeader.hs" "haskell"
testHlintDiagnostics doc
, testCase "[#590] apply-refact works with -XLambdaCase argument" $ runHlintSession "lambdacase" $ do
testRefactor "LambdaCase.hs" "Redundant bracket"
expectedLambdaCase
, testCase "[#1242] apply-refact works with -XTypeApplications argument" $ runHlintSession "typeapps" $ do
testRefactor "TypeApplication.hs" "Redundant bracket"
expectedTypeApp
, testCase "apply hints works with LambdaCase via language pragma" $ runHlintSession "" $ do
testRefactor "LambdaCase.hs" "Redundant bracket"
("{-# LANGUAGE LambdaCase #-}" : expectedLambdaCase)
, expectFailBecause "apply-refact doesn't work with cpp" $
testCase "apply hints works with CPP via -XCPP argument" $ runHlintSession "cpp" $ do
testRefactor "CppCond.hs" "Redundant bracket"
expectedCPP
, expectFailBecause "apply-refact doesn't work with cpp" $
testCase "apply hints works with CPP via language pragma" $ runHlintSession "" $ do
testRefactor "CppCond.hs" "Redundant bracket"
("{-# LANGUAGE CPP #-}" : expectedCPP)
, testCase "hlint diagnostics ignore hints honouring .hlint.yaml" $ runHlintSession "ignore" $ do
doc <- openDoc "CamelCase.hs" "haskell"
expectNoMoreDiagnostics 3 doc "hlint"
, testCase "hlint diagnostics ignore hints honouring ANN annotations" $ runHlintSession "" $ do
doc <- openDoc "IgnoreAnn.hs" "haskell"
expectNoMoreDiagnostics 3 doc "hlint"
, testCase "hlint diagnostics ignore hints honouring HLINT annotations" $ runHlintSession "" $ do
doc <- openDoc "IgnoreAnnHlint.hs" "haskell"
expectNoMoreDiagnostics 3 doc "hlint"
, testCase "apply-refact preserve regular comments" $ runHlintSession "" $ do
testRefactor "Comments.hs" "Redundant bracket" expectedComments
, testCase "[#2290] apply all hints works with a trailing comment" $ runHlintSession "" $ do
testRefactor "TwoHintsAndComment.hs" "Apply all hints" expectedComments2
, testCase "applyAll is shown only when there is at least one diagnostic in range" $ runHlintSession "" $ do
doc <- openDoc "TwoHints.hs" "haskell"
_ <- waitForDiagnosticsFromSource doc "hlint"
firstLine <- map fromAction <$> getCodeActions doc (mkRange 0 0 0 0)
secondLine <- map fromAction <$> getCodeActions doc (mkRange 1 0 1 0)
thirdLine <- map fromAction <$> getCodeActions doc (mkRange 2 0 2 0)
multiLine <- map fromAction <$> getCodeActions doc (mkRange 0 0 2 0)
let hasApplyAll = isJust . find (\ca -> "Apply all hints" `T.isSuffixOf` (ca ^. L.title))
liftIO $ hasApplyAll firstLine @? "Missing apply all code action"
liftIO $ hasApplyAll secondLine @? "Missing apply all code action"
liftIO $ not (hasApplyAll thirdLine) @? "Unexpected apply all code action"
liftIO $ hasApplyAll multiLine @? "Missing apply all code action"
, testCase "hlint should warn about unused extensions" $ runHlintSession "unusedext" $ do
doc <- openDoc "UnusedExtension.hs" "haskell"
diags@(unusedExt:_) <- waitForDiagnosticsFromSource doc "hlint"
liftIO $ do
length diags @?= 1
unusedExt ^. L.code @?= Just (InR "refact:Unused LANGUAGE pragma")
, knownBrokenForHlintOnGhcLib "[#1279] hlint uses a fixed set of extensions" $
testCase "hlint should not activate extensions like PatternSynonyms" $ runHlintSession "" $ do
doc <- openDoc "PatternKeyword.hs" "haskell"
waitForAllProgressDone
-- hlint will report a parse error if PatternSynonyms is enabled
expectNoMoreDiagnostics 3 doc "hlint"
, testCase "hlint should not warn about redundant irrefutable pattern with LANGUAGE Strict" $ runHlintSession "" $ do
doc <- openDoc "StrictData.hs" "haskell"
waitForAllProgressDone
expectNoMoreDiagnostics 3 doc "hlint"
]
where
testRefactor file caTitle expected = do
doc <- openDoc file "haskell"
testHlintDiagnostics doc
cas <- map fromAction <$> getAllCodeActions doc
let ca = find (\ca -> caTitle `T.isInfixOf` (ca ^. L.title)) cas
liftIO $ isJust ca @? ("There is '" ++ T.unpack caTitle ++"' code action")
executeCodeAction (fromJust ca)
contents <- skipManyTill anyMessage $ getDocumentEdit doc
liftIO $ contents @?= T.unlines expected
expectedLambdaCase = [ "module LambdaCase where", ""
, "f = \\case \"true\" -> True"
, " _ -> False"
]
expectedCPP = [ "module CppCond where", ""
, "#ifdef FLAG"
, "f = 1"
, "#else"
, "g = 2"
, "#endif", ""
]
expectedComments = [ "-- comment before header"
, "module Comments where", ""
, "{-# standalone annotation #-}", ""
, "-- standalone comment", ""
, "-- | haddock comment"
, "f = {- inline comment -}{- inline comment inside refactored code -} 1 -- ending comment", ""
, "-- final comment"
]
expectedComments2 = [ "module TwoHintsAndComment where"
, "biggest = foldr1 max -- the line above will show two hlint hints, \"eta reduce\" and \"use maximum\""
]
expectedTypeApp = [ "module TypeApplication where", ""
, "a = id @Int 1"
]
configTests :: TestTree
configTests = testGroup "hlint plugin config" [
testCase "changing hlintOn configuration enables or disables hlint diagnostics" $ runHlintSession "" $ do
let config = def { hlintOn = True }
sendConfigurationChanged (toJSON config)
doc <- openDoc "Base.hs" "haskell"
testHlintDiagnostics doc
let config' = def { hlintOn = False }
sendConfigurationChanged (toJSON config')
diags' <- waitForDiagnosticsFrom doc
liftIO $ noHlintDiagnostics diags'
, testCase "changing hlint plugin configuration enables or disables hlint diagnostics" $ runHlintSession "" $ do
let config = def { hlintOn = True }
sendConfigurationChanged (toJSON config)
doc <- openDoc "Base.hs" "haskell"
testHlintDiagnostics doc
let config' = pluginGlobalOn config "hlint" False
sendConfigurationChanged (toJSON config')
diags' <- waitForDiagnosticsFrom doc
liftIO $ noHlintDiagnostics diags'
, testCase "adding hlint flags to plugin configuration removes hlint diagnostics" $ runHlintSession "" $ do
let config = def { hlintOn = True }
sendConfigurationChanged (toJSON config)
doc <- openDoc "Base.hs" "haskell"
testHlintDiagnostics doc
let config' = hlintConfigWithFlags ["--ignore=Redundant id", "--hint=test-hlint-config.yaml"]
sendConfigurationChanged (toJSON config')
diags' <- waitForDiagnosticsFrom doc
liftIO $ noHlintDiagnostics diags'
, testCase "adding hlint flags to plugin configuration adds hlint diagnostics" $ runHlintSession "" $ do
let config = def { hlintOn = True }
sendConfigurationChanged (toJSON config)
doc <- openDoc "Generalise.hs" "haskell"
expectNoMoreDiagnostics 3 doc "hlint"
let config' = hlintConfigWithFlags ["--with-group=generalise"]
sendConfigurationChanged (toJSON config')
diags' <- waitForDiagnosticsFromSource doc "hlint"
d <- liftIO $ inspectDiagnostic diags' ["Use <>"]
liftIO $ do
length diags' @?= 1
d ^. L.range @?= Range (Position 1 10) (Position 1 21)
d ^. L.severity @?= Just DsInfo
]
testDir :: FilePath
testDir = "test/testdata"
runHlintSession :: FilePath -> Session a -> IO a
runHlintSession subdir =
failIfSessionTimeout . runSessionWithServer hlintPlugin (testDir </> subdir)
noHlintDiagnostics :: [Diagnostic] -> Assertion
noHlintDiagnostics diags =
Just "hlint" `notElem` map (^. L.source) diags @? "There are no hlint diagnostics"
testHlintDiagnostics :: TextDocumentIdentifier -> Session ()
testHlintDiagnostics doc = do
diags <- waitForDiagnosticsFromSource doc "hlint"
liftIO $ length diags > 0 @? "There are hlint diagnostics"
pluginGlobalOn :: Config -> T.Text -> Bool -> Config
pluginGlobalOn config pid state = config'
where
pluginConfig = def { plcGlobalOn = state }
config' = def { plugins = Map.insert pid pluginConfig (plugins config) }
hlintConfigWithFlags :: [T.Text] -> Config
hlintConfigWithFlags flags =
def
{ hlintOn = True
, Plugin.plugins = Map.fromList [("hlint",
def { Plugin.plcConfig = unObject $ object ["flags" .= flags] }
)] }
where
unObject (Object obj) = obj
unObject _ = undefined
-- We have two main code paths in the plugin depending on how hlint interacts with ghc:
-- * One when hlint uses ghc-lib (all ghc versions but the last version supported by hlint)
-- * Another one when hlint uses directly ghc (only one version, which not have to be the last version supported by ghcide)
-- As we always are using ghc through ghcide the code to get the ghc parsed AST differs
-- So the issues and bugs usually only affects to one code path or the other.
-- Although a given hlint version supports one direct ghc, we could use several versions of hlint
-- each one supporting a different ghc version. It should be a temporary situation though.
knownBrokenForHlintOnGhcLib :: String -> TestTree -> TestTree
knownBrokenForHlintOnGhcLib = knownBrokenForGhcVersions [GHC88, GHC86]
knownBrokenForHlintOnRawGhc :: String -> TestTree -> TestTree
knownBrokenForHlintOnRawGhc = knownBrokenForGhcVersions [GHC810, GHC90]
-- 1's based
data Point = Point {
line :: !Int,
column :: !Int
}
makePoint line column
| line >= 1 && column >= 1 = Point line column
| otherwise = error "Line or column is less than 1."
pointToRange :: Point -> Range
pointToRange Point {..}
| line <- subtract 1 line
, column <- subtract 1 column =
Range (Position line column) (Position line $ column + 1)
getCodeActionTitle :: (Command |? CodeAction) -> Maybe T.Text
getCodeActionTitle commandOrCodeAction
| InR CodeAction {_title} <- commandOrCodeAction = Just _title
| otherwise = Nothing
makeCodeActionNotFoundAtString :: Point -> String
makeCodeActionNotFoundAtString Point {..} =
"CodeAction not found at line: " <> show line <> ", column: " <> show column
makeCodeActionFoundAtString :: Point -> String
makeCodeActionFoundAtString Point {..} =
"CodeAction found at line: " <> show line <> ", column: " <> show column
ignoreGoldenTest :: TestName -> FilePath -> Point -> T.Text -> TestTree
ignoreGoldenTest testCaseName goldenFilename point hintName =
setupGoldenHlintTest testCaseName goldenFilename $ \document -> do
waitForDiagnosticsFromSource document "hlint"
actions <- getCodeActions document $ pointToRange point
case find ((== Just (getIgnoreHintText hintName)) . getCodeActionTitle) actions of
Just (InR codeAction) -> executeCodeAction codeAction
_ -> liftIO $ assertFailure $ makeCodeActionNotFoundAtString point
setupGoldenHlintTest :: TestName -> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree
setupGoldenHlintTest testName path =
goldenWithHaskellDoc hlintPlugin testName testDir path "expected" "hs"