Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Renaming of indirect references (RecordFieldPuns) #3013

Merged
merged 15 commits into from
Jul 7, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
79 changes: 45 additions & 34 deletions plugins/hls-rename-plugin/src/Ide/Plugin/Rename.hs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import Data.Generics
import Data.Hashable
import Data.HashSet (HashSet)
import qualified Data.HashSet as HS
import Data.List.Extra
import Data.List.Extra hiding (length)
import qualified Data.Map as M
import Data.Maybe
import Data.Mod.Word
Expand All @@ -42,7 +42,6 @@ import Development.IDE.GHC.ExactPrint
import Development.IDE.Spans.AtPoint
import Development.IDE.Types.Location
import HieDb.Query
import Ide.Plugin.Config
import Ide.Plugin.Properties
import Ide.PluginUtils
import Ide.Types
Expand All @@ -65,16 +64,27 @@ descriptor pluginId = (defaultPluginDescriptor pluginId)
renameProvider :: PluginMethodHandler IdeState TextDocumentRename
renameProvider state pluginId (RenameParams (TextDocumentIdentifier uri) pos _prog newNameText) =
pluginResponse $ do
nfp <- safeUriToNfp uri
oldName <- getNameAtPos state nfp pos
refLocs <- refsAtName state nfp oldName
nfp <- handleUriToNfp uri
directOldNames <- getNamesAtPos state nfp pos
directRefs <- concat <$> mapM (refsAtName state nfp) directOldNames

{- References in HieDB are not necessarily transitive. With `NamedFieldPuns`, we can have
indirect references through punned names. To find the transitive closure, we do a pass of
OliverMadine marked this conversation as resolved.
Show resolved Hide resolved
the direct references to find the references for any punned names. -}
indirectOldNames <- concat . filter ((>1) . Prelude.length) <$>
mapM (uncurry (getNamesAtPos state) . locToFilePos) directRefs
let oldNames = indirectOldNames ++ directOldNames
refs <- HS.fromList . concat <$> mapM (refsAtName state nfp) oldNames

-- Validate rename
crossModuleEnabled <- lift $ usePropertyLsp #crossModule pluginId properties
unless crossModuleEnabled $ failWhenImportOrExport state nfp refLocs oldName
when (isBuiltInSyntax oldName) $
throwE ("Invalid rename of built-in syntax: \"" ++ showName oldName ++ "\"")
unless crossModuleEnabled $ failWhenImportOrExport state nfp refs oldNames
when (any isBuiltInSyntax oldNames) $ throwE "Invalid rename of built-in syntax"

-- Perform rename
let newName = mkTcOcc $ T.unpack newNameText
filesRefs = collectWith locToUri refLocs
getFileEdit = flip $ getSrcEdit state . renameRefs newName
filesRefs = collectWith locToUri refs
getFileEdit = flip $ getSrcEdit state . replaceRefs newName
fileEdits <- mapM (uncurry getFileEdit) filesRefs
pure $ foldl' (<>) mempty fileEdits

Expand All @@ -84,16 +94,16 @@ failWhenImportOrExport ::
IdeState ->
NormalizedFilePath ->
HashSet Location ->
Name ->
[Name] ->
ExceptT String m ()
failWhenImportOrExport state nfp refLocs name = do
failWhenImportOrExport state nfp refLocs names = do
pm <- handleMaybeM ("No parsed module for: " ++ show nfp) $ liftIO $ runAction
"Rename.GetParsedModule"
state
(use GetParsedModule nfp)
let hsMod = unLoc $ pm_parsed_source pm
case (unLoc <$> hsmodName hsMod, hsmodExports hsMod) of
(mbModName, _) | not $ nameIsLocalOrFrom (replaceModName name mbModName) name
(mbModName, _) | not $ any (\n -> nameIsLocalOrFrom (replaceModName n mbModName) n) names
-> throwE "Renaming of an imported name is unsupported"
(_, Just (L _ exports)) | any ((`HS.member` refLocs) . unsafeSrcSpanToLoc . getLoc) exports
-> throwE "Renaming of an exported name is unsupported"
Expand All @@ -112,7 +122,7 @@ getSrcEdit ::
ExceptT String m WorkspaceEdit
getSrcEdit state updatePs uri = do
ccs <- lift getClientCapabilities
nfp <- safeUriToNfp uri
nfp <- handleUriToNfp uri
annAst <- handleMaybeM ("No parsed source for: " ++ show nfp) $ liftIO $ runAction
"Rename.GetAnnotatedParsedSource"
state
Expand All @@ -128,13 +138,13 @@ getSrcEdit state updatePs uri = do
pure $ diffText ccs (uri, src) res IncludeDeletions

-- | Replace names at every given `Location` (in a given `ParsedSource`) with a given new name.
renameRefs ::
replaceRefs ::
OccName ->
HashSet Location ->
ParsedSource ->
ParsedSource
#if MIN_VERSION_ghc(9,2,1)
renameRefs newName refs = everywhere $
replaceRefs newName refs = everywhere $
-- there has to be a better way...
mkT (replaceLoc @AnnListItem) `extT`
-- replaceLoc @AnnList `extT` -- not needed
Expand All @@ -149,14 +159,13 @@ renameRefs newName refs = everywhere $
| isRef (locA srcSpan) = L srcSpan $ replace oldRdrName
replaceLoc lOldRdrName = lOldRdrName
#else
renameRefs newName refs = everywhere $ mkT replaceLoc
replaceRefs newName refs = everywhere $ mkT replaceLoc
where
replaceLoc :: Located RdrName -> Located RdrName
replaceLoc (L srcSpan oldRdrName)
| isRef srcSpan = L srcSpan $ replace oldRdrName
replaceLoc lOldRdrName = lOldRdrName
#endif

replace :: RdrName -> RdrName
replace (Qual modName _) = Qual modName newName
replace _ = Unqual newName
Expand All @@ -173,10 +182,10 @@ refsAtName ::
IdeState ->
NormalizedFilePath ->
Name ->
ExceptT String m (HashSet Location)
ExceptT String m [Location]
refsAtName state nfp name = do
ShakeExtras{withHieDb} <- liftIO $ runAction "Rename.HieDb" state getShakeExtras
ast <- safeGetHieAst state nfp
ast <- handleGetHieAst state nfp
dbRefs <- case nameModule_maybe name of
Nothing -> pure []
Just mod -> liftIO $ mapMaybe rowToLoc <$> withHieDb (\hieDb ->
Expand All @@ -188,32 +197,32 @@ refsAtName state nfp name = do
(Just $ moduleUnit mod)
[fromNormalizedFilePath nfp]
)
pure $ HS.fromList $ getNameLocs name ast ++ dbRefs
pure $ nameLocs name ast ++ dbRefs

getNameLocs :: Name -> (HieAstResult, PositionMapping) -> [Location]
getNameLocs name (HAR _ _ rm _ _, pm) =
nameLocs :: Name -> (HieAstResult, PositionMapping) -> [Location]
nameLocs name (HAR _ _ rm _ _, pm) =
mapMaybe (toCurrentLocation pm . realSrcSpanToLocation . fst)
(concat $ M.lookup (Right name) rm)

---------------------------------------------------------------------------------------------------
-- Util

getNameAtPos :: IdeState -> NormalizedFilePath -> Position -> ExceptT String (LspT Config IO) Name
getNameAtPos state nfp pos = do
(HAR{hieAst}, pm) <- safeGetHieAst state nfp
handleMaybe ("No name at " ++ showPos pos) $ listToMaybe $ getNamesAtPoint hieAst pos pm
getNamesAtPos :: MonadIO m => IdeState -> NormalizedFilePath -> Position -> ExceptT String m [Name]
getNamesAtPos state nfp pos = do
(HAR{hieAst}, pm) <- handleGetHieAst state nfp
pure $ getNamesAtPoint hieAst pos pm

safeGetHieAst ::
handleGetHieAst ::
MonadIO m =>
IdeState ->
NormalizedFilePath ->
ExceptT String m (HieAstResult, PositionMapping)
safeGetHieAst state nfp = handleMaybeM
handleGetHieAst state nfp = handleMaybeM
("No AST for file: " ++ show nfp)
(liftIO $ runAction "Rename.GetHieAst" state $ useWithStale GetHieAst nfp)

safeUriToNfp :: (Monad m) => Uri -> ExceptT String m NormalizedFilePath
safeUriToNfp uri = handleMaybe
handleUriToNfp :: (Monad m) => Uri -> ExceptT String m NormalizedFilePath
handleUriToNfp uri = handleMaybe
("No filepath for uri: " ++ show uri)
(toNormalizedFilePath <$> uriToFilePath uri)

Expand All @@ -230,15 +239,17 @@ nfpToUri = filePathToUri . fromNormalizedFilePath
showName :: Name -> String
showName = occNameString . getOccName

showPos :: Position -> String
showPos Position{_line, _character} = "line: " ++ show _line ++ " - character: " ++ show _character

unsafeSrcSpanToLoc :: SrcSpan -> Location
unsafeSrcSpanToLoc srcSpan =
case srcSpanToLocation srcSpan of
Nothing -> error "Invalid conversion from UnhelpfulSpan to Location"
Just location -> location

locToFilePos :: Location -> (NormalizedFilePath, Position)
locToFilePos (Location uri (Range pos _)) = (nfp, pos)
where
Just nfp = (uriToNormalizedFilePath . toNormalizedUri) uri

replaceModName :: Name -> Maybe ModuleName -> Module
replaceModName name mbModName =
mkModule (moduleUnit $ nameModule name) (fromMaybe (mkModuleName "Main") mbModName)
Expand Down
9 changes: 8 additions & 1 deletion plugins/hls-rename-plugin/test/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ tests = testGroup "Rename"
rename doc (Position 0 15) "Op"
, goldenWithRename "Exported function" "ExportedFunction" $ \doc ->
rename doc (Position 2 1) "quux"
, ignoreForGhcVersions [GHC90, GHC92] "Record field refs broken for ghc > 9" $
goldenWithRename "Field Puns" "FieldPuns" $ \doc ->
rename doc (Position 7 13) "bleh"
OliverMadine marked this conversation as resolved.
Show resolved Hide resolved
, goldenWithRename "Function argument" "FunctionArgument" $ \doc ->
rename doc (Position 3 4) "y"
, goldenWithRename "Function name" "FunctionName" $ \doc ->
Expand All @@ -33,6 +36,9 @@ tests = testGroup "Rename"
rename doc (Position 3 8) "baz"
, goldenWithRename "Import hiding" "ImportHiding" $ \doc ->
rename doc (Position 0 22) "hiddenFoo"
, ignoreForGhcVersions [GHC90, GHC92] "Record field refs broken for ghc > 9" $
goldenWithRename "Indirect Puns" "IndirectPuns" $ \doc ->
rename doc (Position 4 23) "blah"
, goldenWithRename "Let expression" "LetExpression" $ \doc ->
rename doc (Position 5 11) "foobar"
, goldenWithRename "Qualified as" "QualifiedAs" $ \doc ->
Expand All @@ -43,7 +49,8 @@ tests = testGroup "Rename"
rename doc (Position 3 12) "baz"
, goldenWithRename "Realigns do block indentation" "RealignDo" $ \doc ->
rename doc (Position 0 2) "fooBarQuux"
, goldenWithRename "Record field" "RecordField" $ \doc ->
, ignoreForGhcVersions [GHC90, GHC92] "Record field refs broken for ghc > 9" $
goldenWithRename "Record field" "RecordField" $ \doc ->
rename doc (Position 6 9) "number"
, goldenWithRename "Shadowed name" "ShadowedName" $ \doc ->
rename doc (Position 1 1) "baz"
Expand Down
8 changes: 8 additions & 0 deletions plugins/hls-rename-plugin/test/testdata/FieldPuns.expected.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{-# LANGUAGE NamedFieldPuns #-}

module FieldPun () where

newtype Foo = Foo { bleh :: Int }

unFoo :: Foo -> Int
unFoo Foo{bleh} = bleh
8 changes: 8 additions & 0 deletions plugins/hls-rename-plugin/test/testdata/FieldPuns.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{-# LANGUAGE NamedFieldPuns #-}

module FieldPun () where

newtype Foo = Foo { field :: Int }

unFoo :: Foo -> Int
unFoo Foo{field} = field
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{-# LANGUAGE NamedFieldPuns #-}

module IndirectPuns () where

newtype Foo = Foo { blah :: Int }

unFoo :: Foo -> Int
unFoo Foo{blah} = blah
8 changes: 8 additions & 0 deletions plugins/hls-rename-plugin/test/testdata/IndirectPuns.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{-# LANGUAGE NamedFieldPuns #-}

module IndirectPuns () where

newtype Foo = Foo { field :: Int }

unFoo :: Foo -> Int
unFoo Foo{field} = field
2 changes: 2 additions & 0 deletions plugins/hls-rename-plugin/test/testdata/hie.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ cradle:
arguments:
- "DataConstructor"
- "ExportedFunction"
- "FieldPuns"
- "Foo"
- "FunctionArgument"
- "FunctionName"
- "Gadt"
- "HiddenFunction"
- "ImportHiding"
- "ImportedFunction"
- "IndirectPuns"
- "LetExpression"
- "QualifiedAs"
- "QualifiedFunction"
Expand Down