Skip to content

Commit 463eb2f

Browse files
authored
Rename only if the current module compiles (#3799) (#3848)
* Rename only if the current module compiles (#3799) Prefer `useE` over `useWithStaleE` * Add a rename test that tests for compilation errors
1 parent 9593d04 commit 463eb2f

File tree

3 files changed

+73
-13
lines changed

3 files changed

+73
-13
lines changed

haskell-language-server.cabal

+1
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,7 @@ test-suite hls-rename-plugin-tests
614614
, hls-test-utils == 2.7.0.0
615615
, lens
616616
, lsp-types
617+
, row-types
617618
, text
618619

619620
-----------------------------

plugins/hls-rename-plugin/src/Ide/Plugin/Rename.hs

+17-12
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import Control.Monad
1414
import Control.Monad.Except (ExceptT, throwError)
1515
import Control.Monad.IO.Class (MonadIO, liftIO)
1616
import Control.Monad.Trans.Class (lift)
17-
import Data.Bifunctor (first)
17+
import Data.Either (rights)
1818
import Data.Foldable (fold)
1919
import Data.Generics
2020
import Data.Hashable
@@ -31,14 +31,11 @@ import qualified Data.Text as T
3131
import Development.IDE (Recorder, WithPriority,
3232
usePropertyAction)
3333
import Development.IDE.Core.PluginUtils
34-
import Development.IDE.Core.PositionMapping
3534
import Development.IDE.Core.RuleTypes
3635
import Development.IDE.Core.Service
3736
import Development.IDE.Core.Shake
38-
import Development.IDE.GHC.Compat.Core
37+
import Development.IDE.GHC.Compat
3938
import Development.IDE.GHC.Compat.ExactPrint
40-
import Development.IDE.GHC.Compat.Parser
41-
import Development.IDE.GHC.Compat.Units
4239
import Development.IDE.GHC.Error
4340
import Development.IDE.GHC.ExactPrint
4441
import qualified Development.IDE.GHC.ExactPrint as E
@@ -212,26 +209,29 @@ refsAtName state nfp name = do
212209
)
213210
pure $ nameLocs name ast ++ dbRefs
214211

215-
nameLocs :: Name -> (HieAstResult, PositionMapping) -> [Location]
216-
nameLocs name (HAR _ _ rm _ _, pm) =
217-
concatMap (mapMaybe (toCurrentLocation pm . realSrcSpanToLocation . fst))
212+
nameLocs :: Name -> HieAstResult -> [Location]
213+
nameLocs name (HAR _ _ rm _ _) =
214+
concatMap (map (realSrcSpanToLocation . fst))
218215
(M.lookup (Right name) rm)
219216

220217
---------------------------------------------------------------------------------------------------
221218
-- Util
222219

223220
getNamesAtPos :: MonadIO m => IdeState -> NormalizedFilePath -> Position -> ExceptT PluginError m [Name]
224221
getNamesAtPos state nfp pos = do
225-
(HAR{hieAst}, pm) <- handleGetHieAst state nfp
226-
pure $ getNamesAtPoint hieAst pos pm
222+
HAR{hieAst} <- handleGetHieAst state nfp
223+
pure $ getNamesAtPoint' hieAst pos
227224

228225
handleGetHieAst ::
229226
MonadIO m =>
230227
IdeState ->
231228
NormalizedFilePath ->
232-
ExceptT PluginError m (HieAstResult, PositionMapping)
229+
ExceptT PluginError m HieAstResult
233230
handleGetHieAst state nfp =
234-
fmap (first removeGenerated) $ runActionE "Rename.GetHieAst" state $ useWithStaleE GetHieAst nfp
231+
-- We explicitly do not want to allow a stale version here - we only want to rename if
232+
-- the module compiles, otherwise we can't guarantee that we'll rename everything,
233+
-- which is bad (see https://github.com/haskell/haskell-language-server/issues/3799)
234+
fmap removeGenerated $ runActionE "Rename.GetHieAst" state $ useE GetHieAst nfp
235235

236236
-- | We don't want to rename in code generated by GHC as this gives false positives.
237237
-- So we restrict the HIE file to remove all the generated code.
@@ -246,6 +246,11 @@ removeGenerated HAR{..} = HAR{hieAst = go hieAst,..}
246246
collectWith :: (Hashable a, Eq b) => (a -> b) -> HashSet a -> [(b, HashSet a)]
247247
collectWith f = map (\(a :| as) -> (f a, HS.fromList (a:as))) . groupWith f . HS.toList
248248

249+
-- | A variant 'getNamesAtPoint' that does not expect a 'PositionMapping'
250+
getNamesAtPoint' :: HieASTs a -> Position -> [Name]
251+
getNamesAtPoint' hf pos =
252+
concat $ pointCommand hf pos (rights . M.keys . getNodeIds)
253+
249254
locToUri :: Location -> Uri
250255
locToUri (Location uri _) = uri
251256

plugins/hls-rename-plugin/test/Main.hs

+55-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
{-# LANGUAGE OverloadedLabels #-}
12
{-# LANGUAGE OverloadedStrings #-}
23

34
module Main (main) where
45

56
import Control.Lens ((^.))
67
import Data.Aeson
78
import qualified Data.Map as M
8-
import Data.Text (Text)
9+
import Data.Row ((.+), (.==))
10+
import Data.Text (Text, pack)
911
import Ide.Plugin.Config
1012
import qualified Ide.Plugin.Rename as Rename
1113
import qualified Language.LSP.Protocol.Lens as L
@@ -73,6 +75,40 @@ tests = testGroup "Rename"
7375
"rename: Invalid Params: No symbol to rename at given position"
7476
Nothing
7577
renameExpectError expectedError doc (Position 0 10) "ImpossibleRename"
78+
79+
, testCase "fails when module does not compile" $ runRenameSession "" $ do
80+
doc <- openDoc "FunctionArgument.hs" "haskell"
81+
expectNoMoreDiagnostics 3 doc "typecheck"
82+
83+
-- Update the document so it doesn't compile
84+
let change = TextDocumentContentChangeEvent $ InL $ #range .== Range (Position 2 13) (Position 2 17)
85+
.+ #rangeLength .== Nothing
86+
.+ #text .== "A"
87+
changeDoc doc [change]
88+
diags@(tcDiag : _) <- waitForDiagnosticsFrom doc
89+
90+
-- Make sure there's a typecheck error
91+
liftIO $ do
92+
length diags @?= 1
93+
tcDiag ^. L.range @?= Range (Position 2 13) (Position 2 14)
94+
tcDiag ^. L.severity @?= Just DiagnosticSeverity_Error
95+
tcDiag ^. L.source @?= Just "typecheck"
96+
97+
-- Make sure renaming fails
98+
renameErr <- expectRenameError doc (Position 3 0) "foo'"
99+
liftIO $ do
100+
renameErr ^. L.code @?= InL LSPErrorCodes_RequestFailed
101+
renameErr ^. L.message @?= "rename: Rule Failed: GetHieAst"
102+
103+
-- Update the document so it compiles
104+
let change' = TextDocumentContentChangeEvent $ InL $ #range .== Range (Position 2 13) (Position 2 14)
105+
.+ #rangeLength .== Nothing
106+
.+ #text .== "Int"
107+
changeDoc doc [change']
108+
expectNoMoreDiagnostics 3 doc "typecheck"
109+
110+
-- Make sure renaming succeeds
111+
rename doc (Position 3 0) "foo'"
76112
]
77113

78114
goldenWithRename :: TestName-> FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree
@@ -90,3 +126,21 @@ renameExpectError expectedError doc pos newName = do
90126

91127
testDataDir :: FilePath
92128
testDataDir = "plugins" </> "hls-rename-plugin" </> "test" </> "testdata"
129+
130+
-- | Attempts to renames the term at the specified position, expecting a failure
131+
expectRenameError ::
132+
TextDocumentIdentifier ->
133+
Position ->
134+
String ->
135+
Session ResponseError
136+
expectRenameError doc pos newName = do
137+
let params = RenameParams Nothing doc pos (pack newName)
138+
rsp <- request SMethod_TextDocumentRename params
139+
case rsp ^. L.result of
140+
Left err -> pure err
141+
Right _ -> liftIO $ assertFailure $
142+
"Got unexpected successful rename response for " <> show (doc ^. L.uri)
143+
144+
runRenameSession :: FilePath -> Session a -> IO a
145+
runRenameSession subdir = failIfSessionTimeout
146+
. runSessionWithServerAndCaps def renamePlugin codeActionNoResolveCaps (testDataDir </> subdir)

0 commit comments

Comments
 (0)