This repository was archived by the owner on Oct 7, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 205
Expand file tree
/
Copy pathApplyRefact.hs
More file actions
339 lines (292 loc) · 14.4 KB
/
ApplyRefact.hs
File metadata and controls
339 lines (292 loc) · 14.4 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
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}
module Haskell.Ide.Engine.Plugin.ApplyRefact where
import Control.Arrow
import Control.Exception ( IOException
, ErrorCall
, Handler(..)
, catches
, try
)
import Control.Lens hiding ( List )
import Control.Monad.IO.Class
import Control.Monad.Trans.Except
import Data.Aeson hiding (Error)
import Data.Maybe
import Data.Monoid ((<>))
import qualified Data.Text as T
import GHC.Generics
import qualified GhcModCore as GM ( mkRevRedirMapFunc, withMappedFile )
import Haskell.Ide.Engine.MonadFunctions
import Haskell.Ide.Engine.MonadTypes
import Haskell.Ide.Engine.PluginUtils
import Language.Haskell.Exts.SrcLoc
import Language.Haskell.Exts.Parser
import Language.Haskell.Exts.Extension
import Language.Haskell.HLint4 as Hlint
import qualified Language.Haskell.LSP.Types as LSP
import qualified Language.Haskell.LSP.Types.Lens as LSP
import Refact.Apply
-- ---------------------------------------------------------------------
{-# ANN module ("HLint: ignore Eta reduce" :: String) #-}
{-# ANN module ("HLint: ignore Redundant do" :: String) #-}
{-# ANN module ("HLint: ignore Reduce duplication" :: String) #-}
-- ---------------------------------------------------------------------
type HintTitle = T.Text
applyRefactDescriptor :: PluginId -> PluginDescriptor
applyRefactDescriptor plId = PluginDescriptor
{ pluginId = plId
, pluginName = "ApplyRefact"
, pluginDesc = "apply-refact applies refactorings specified by the refact package. It is currently integrated into hlint to enable the automatic application of suggestions."
, pluginCommands =
[ PluginCommand "applyOne" "Apply a single hint" applyOneCmd
, PluginCommand "applyAll" "Apply all hints to the file" applyAllCmd
, PluginCommand "lint" "Run hlint on the file to generate hints" lintCmd
]
, pluginCodeActionProvider = Just codeActionProvider
, pluginDiagnosticProvider = Nothing
, pluginHoverProvider = Nothing
, pluginSymbolProvider = Nothing
, pluginFormattingProvider = Nothing
}
-- ---------------------------------------------------------------------
data ApplyOneParams = AOP
{ file :: Uri
, start_pos :: Position
-- | There can be more than one hint suggested at the same position, so HintTitle is used to distinguish between them.
, hintTitle :: HintTitle
} deriving (Eq,Show,Generic,FromJSON,ToJSON)
data OneHint = OneHint
{ oneHintPos :: Position
, oneHintTitle :: HintTitle
} deriving (Eq, Show)
applyOneCmd :: CommandFunc ApplyOneParams WorkspaceEdit
applyOneCmd = CmdSync $ \(AOP uri pos title) -> do
applyOneCmd' uri (OneHint pos title)
applyOneCmd' :: Uri -> OneHint -> IdeGhcM (IdeResult WorkspaceEdit)
applyOneCmd' uri oneHint = pluginGetFile "applyOne: " uri $ \fp -> do
revMapp <- GM.mkRevRedirMapFunc
res <- GM.withMappedFile fp $ \file' -> liftToGhc $ applyHint file' (Just oneHint) revMapp
logm $ "applyOneCmd:file=" ++ show fp
logm $ "applyOneCmd:res=" ++ show res
case res of
Left err -> return $ IdeResultFail (IdeError PluginError
(T.pack $ "applyOne: " ++ show err) Null)
Right fs -> return (IdeResultOk fs)
-- ---------------------------------------------------------------------
applyAllCmd :: CommandFunc Uri WorkspaceEdit
applyAllCmd = CmdSync $ \uri -> do
applyAllCmd' uri
applyAllCmd' :: Uri -> IdeGhcM (IdeResult WorkspaceEdit)
applyAllCmd' uri = pluginGetFile "applyAll: " uri $ \fp -> do
revMapp <- GM.mkRevRedirMapFunc
res <- GM.withMappedFile fp $ \file' -> liftToGhc $ applyHint file' Nothing revMapp
logm $ "applyAllCmd:res=" ++ show res
case res of
Left err -> return $ IdeResultFail (IdeError PluginError
(T.pack $ "applyAll: " ++ show err) Null)
Right fs -> return (IdeResultOk fs)
-- ---------------------------------------------------------------------
lintCmd :: CommandFunc Uri PublishDiagnosticsParams
lintCmd = CmdSync $ \uri -> do
lintCmd' uri
-- AZ:TODO: Why is this in IdeGhcM?
lintCmd' :: Uri -> IdeGhcM (IdeResult PublishDiagnosticsParams)
lintCmd' uri = pluginGetFile "lintCmd: " uri $ \fp -> do
eitherErrorResult <- GM.withMappedFile fp $ \file' ->
liftIO (try $ runExceptT $ runLintCmd file' [] :: IO (Either IOException (Either [Diagnostic] [Idea])))
case eitherErrorResult of
Left err ->
return
$ IdeResultFail (IdeError PluginError
(T.pack $ "lintCmd: " ++ show err) Null)
Right res -> case res of
Left diags ->
return
(IdeResultOk
(PublishDiagnosticsParams (filePathToUri fp) $ List diags)
)
Right fs ->
return
$ IdeResultOk
$ PublishDiagnosticsParams (filePathToUri fp)
$ List (map hintToDiagnostic $ stripIgnores fs)
runLintCmd :: FilePath -> [String] -> ExceptT [Diagnostic] IO [Idea]
runLintCmd fp args = do
(flags,classify,hint) <- liftIO $ argsSettings args
let myflags = flags { hseFlags = (hseFlags flags) { extensions = EnableExtension TypeApplications:extensions (hseFlags flags)}}
res <- bimapExceptT parseErrorToDiagnostic id $ ExceptT $ parseModuleEx myflags fp Nothing
pure $ applyHints classify hint [res]
parseErrorToDiagnostic :: Hlint.ParseError -> [Diagnostic]
parseErrorToDiagnostic (Hlint.ParseError l msg contents) =
[Diagnostic
{ _range = srcLoc2Range l
, _severity = Just DsInfo -- Not displayed
, _code = Just (LSP.StringValue "parser")
, _source = Just "hlint"
, _message = T.unlines [T.pack msg,T.pack contents]
, _relatedInformation = Nothing
}]
{-
-- | An idea suggest by a 'Hint'.
data Idea = Idea
{ideaModule :: String -- ^ The module the idea applies to, may be @\"\"@ if the module cannot be determined or is a result of cross-module hints.
,ideaDecl :: String -- ^ The declaration the idea applies to, typically the function name, but may be a type name.
,ideaSeverity :: Severity -- ^ The severity of the idea, e.g. 'Warning'.
,ideaHint :: String -- ^ The name of the hint that generated the idea, e.g. @\"Use reverse\"@.
,ideaSpan :: SrcSpan -- ^ The source code the idea relates to.
,ideaFrom :: String -- ^ The contents of the source code the idea relates to.
,ideaTo :: Maybe String -- ^ The suggested replacement, or 'Nothing' for no replacement (e.g. on parse errors).
,ideaNote :: [Note] -- ^ Notes about the effect of applying the replacement.
,ideaRefactoring :: [Refactoring R.SrcSpan] -- ^ How to perform this idea
}
deriving (Eq,Ord)
-}
-- | Map over both failure and success.
bimapExceptT :: Functor m => (e -> f) -> (a -> b) -> ExceptT e m a -> ExceptT f m b
bimapExceptT f g (ExceptT m) = ExceptT (fmap h m) where
h (Left e) = Left (f e)
h (Right a) = Right (g a)
{-# INLINE bimapExceptT #-}
-- ---------------------------------------------------------------------
stripIgnores :: [Idea] -> [Idea]
stripIgnores ideas = filter notIgnored ideas
where
notIgnored idea = ideaSeverity idea /= Ignore
-- ---------------------------------------------------------------------
hintToDiagnostic :: Idea -> Diagnostic
hintToDiagnostic idea
= Diagnostic
{ _range = ss2Range (ideaSpan idea)
, _severity = Just (hintSeverityMap $ ideaSeverity idea)
, _code = Just (LSP.StringValue $ T.pack $ ideaHint idea)
, _source = Just "hlint"
, _message = idea2Message idea
, _relatedInformation = Nothing
}
-- ---------------------------------------------------------------------
idea2Message :: Idea -> T.Text
idea2Message idea = T.unlines $ [T.pack $ ideaHint idea, "Found:", " " <> T.pack (ideaFrom idea)]
<> toIdea <> map (T.pack . show) (ideaNote idea)
where
toIdea :: [T.Text]
toIdea = case ideaTo idea of
Nothing -> []
Just i -> [T.pack "Why not:", T.pack $ " " ++ i]
-- ---------------------------------------------------------------------
-- | Maps hlint severities to LSP severities
-- | We want to lower the severities so HLint errors and warnings
-- | don't mix with GHC errors and warnings:
-- | as per https://github.com/haskell/haskell-ide-engine/issues/375
hintSeverityMap :: Severity -> DiagnosticSeverity
hintSeverityMap Ignore = DsInfo -- cannot really happen after stripIgnores
hintSeverityMap Suggestion = DsHint
hintSeverityMap Warning = DsInfo
hintSeverityMap Error = DsInfo
-- ---------------------------------------------------------------------
srcLoc2Range :: SrcLoc -> Range
srcLoc2Range (SrcLoc _ l c) = Range ps pe
where
ps = Position (l-1) (c-1)
pe = Position (l-1) 100000
-- ---------------------------------------------------------------------
ss2Range :: SrcSpan -> Range
ss2Range ss = Range ps pe
where
ps = Position (srcSpanStartLine ss - 1) (srcSpanStartColumn ss - 1)
pe = Position (srcSpanEndLine ss - 1) (srcSpanEndColumn ss - 1)
-- ---------------------------------------------------------------------
applyHint :: FilePath -> Maybe OneHint -> (FilePath -> FilePath) -> IdeM (Either String WorkspaceEdit)
applyHint fp mhint fileMap = do
runExceptT $ do
ideas <- getIdeas fp mhint
let commands = map (show &&& ideaRefactoring) ideas
liftIO $ logm $ "applyHint:apply=" ++ show commands
-- set Nothing as "position" for "applyRefactorings" because
-- applyRefactorings expects the provided position to be _within_ the scope
-- of each refactoring it will apply.
-- But "Idea"s returned by HLint pont to starting position of the expressions
-- that contain refactorings, so they are often outside the refactorings' boundaries.
-- Example:
-- Given an expression "hlintTest = reid $ (myid ())"
-- Hlint returns an idea at the position (1,13)
-- That contains "Redundant brackets" refactoring at position (1,20):
--
-- [("src/App/Test.hs:5:13: Warning: Redundant bracket\nFound:\n reid $ (myid ())\nWhy not:\n reid $ myid ()\n",[Replace {rtype = Expr, pos = SrcSpan {startLine = 5, startCol = 20, endLine = 5, endCol = 29}, subts = [("x",SrcSpan {startLine = 5, startCol = 21, endLine = 5, endCol = 28})], orig = "x"}])]
--
-- If we provide "applyRefactorings" with "Just (1,13)" then
-- the "Redundant bracket" hint will never be executed
-- because SrcSpan (1,20,??,??) doesn't contain position (1,13).
res <- liftIO $ (Right <$> applyRefactorings Nothing commands fp) `catches`
[ Handler $ \e -> return (Left (show (e :: IOException)))
, Handler $ \e -> return (Left (show (e :: ErrorCall)))
]
case res of
Right appliedFile -> do
diff <- ExceptT $ Right <$> makeDiffResult fp (T.pack appliedFile) fileMap
liftIO $ logm $ "applyHint:diff=" ++ show diff
return diff
Left err ->
throwE (show err)
-- | Gets HLint ideas for
getIdeas :: MonadIO m => FilePath -> Maybe OneHint -> ExceptT String m [Idea]
getIdeas lintFile mhint = do
let hOpts = hlintOpts lintFile (oneHintPos <$> mhint)
ideas <- runHlint lintFile hOpts
pure $ maybe ideas (`filterIdeas` ideas) mhint
-- | If we are only interested in applying a particular hint then
-- let's filter out all the irrelevant ideas
filterIdeas :: OneHint -> [Idea] -> [Idea]
filterIdeas (OneHint (Position l c) title) ideas =
let
title' = T.unpack title
ideaPos = (srcSpanStartLine &&& srcSpanStartColumn) . ideaSpan
in filter (\i -> ideaHint i == title' && ideaPos i == (l+1, c+1)) ideas
hlintOpts :: FilePath -> Maybe Position -> [String]
hlintOpts lintFile mpos =
let
posOpt (Position l c) = " --pos " ++ show (l+1) ++ "," ++ show (c+1)
opts = maybe "" posOpt mpos
in [lintFile, "--quiet", "--refactor", "--refactor-options=" ++ opts ]
runHlint :: MonadIO m => FilePath -> [String] -> ExceptT String m [Idea]
runHlint fp args =
do (flags,classify,hint) <- liftIO $ argsSettings args
let myflags = flags { hseFlags = (hseFlags flags) { extensions = EnableExtension TypeApplications:extensions (hseFlags flags)}}
res <- bimapExceptT showParseError id $ ExceptT $ liftIO $ parseModuleEx myflags fp Nothing
pure $ applyHints classify hint [res]
showParseError :: Hlint.ParseError -> String
showParseError (Hlint.ParseError location message content) =
unlines [show location, message, content]
-- ---------------------------------------------------------------------
codeActionProvider :: CodeActionProvider
codeActionProvider plId docId _ context = IdeResultOk <$> hlintActions
where
hlintActions :: IdeM [LSP.CodeAction]
hlintActions = do
actions <- mapM mkHlintAction (filter validCommand diags)
applyAll <- mkApplyAllAction
return (catMaybes $ applyAll:actions)
-- |Some hints do not have an associated refactoring
validCommand (LSP.Diagnostic _ _ (Just (LSP.StringValue code)) (Just "hlint") _ _) =
case code of
"Eta reduce" -> False
_ -> True
validCommand _ = False
LSP.List diags = context ^. LSP.diagnostics
mkHlintAction :: LSP.Diagnostic -> IdeM (Maybe LSP.CodeAction)
mkHlintAction diag@(LSP.Diagnostic (LSP.Range start _) _s (Just (LSP.StringValue code)) (Just "hlint") m _) =
Just . codeAction <$> mkLspCommand plId "applyOne" title (Just args)
where
codeAction cmd = LSP.CodeAction title (Just LSP.CodeActionRefactor) (Just (LSP.List [diag])) Nothing (Just cmd)
title = "Apply hint:" <> head (T.lines m)
-- need 'file', 'start_pos' and hint title (to distinguish between alternative suggestions at the same location)
args = [toJSON (AOP (docId ^. LSP.uri) start code)]
mkHlintAction (LSP.Diagnostic _r _s _c _source _m _) = return Nothing
mkApplyAllAction :: IdeM (Maybe LSP.CodeAction)
mkApplyAllAction =
Just . codeAction <$> mkLspCommand plId "applyAll" title (Just [toJSON (docId ^. LSP.uri)])
where
title = "Apply all hints"
codeAction cmd = LSP.CodeAction title (Just LSP.CodeActionRefactor) Nothing Nothing (Just cmd)