Skip to content

Commit 849aef8

Browse files
authored
Delete unused top level binding code action (#657)
* Delete unused top level binding code action * Remove redundant brackets according to hlint * Attempt to fix build issue on ghc-8.4 * Fix delete unused binding code action - handle case of top level bindings defined in infix form - when deleting some unused binding expand text deletion range to beginning of next top level binding (if any was found) * Modify delete unused binding code action Sort all inspected bindings by location before processing * Avoid sending top level binding delete action with no TextEdit Happens when there is unused local binding
1 parent 2116138 commit 849aef8

File tree

3 files changed

+111
-1
lines changed

3 files changed

+111
-1
lines changed

src/Development/IDE/GHC/Compat.hs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ module Development.IDE.GHC.Compat(
3232
pattern InstD,
3333
pattern TyClD,
3434
pattern ValD,
35+
pattern SigD,
36+
pattern TypeSig,
3537
pattern ClassOpSig,
3638
pattern IEThingAll,
3739
pattern IEThingWith,
@@ -52,7 +54,7 @@ import Packages
5254

5355
import qualified GHC
5456
import GHC hiding (
55-
ClassOpSig, DerivD, ForD, IEThingAll, IEThingWith, InstD, TyClD, ValD, ModLocation
57+
ClassOpSig, DerivD, ForD, IEThingAll, IEThingWith, InstD, TyClD, ValD, SigD, TypeSig, ModLocation
5658
#if MIN_GHC_API_VERSION(8,6,0)
5759
, getConArgs
5860
#endif
@@ -160,6 +162,22 @@ pattern TyClD x <-
160162
GHC.TyClD x
161163
#endif
162164

165+
pattern SigD :: Sig p -> HsDecl p
166+
pattern SigD x <-
167+
#if MIN_GHC_API_VERSION(8,6,0)
168+
GHC.SigD _ x
169+
#else
170+
GHC.SigD x
171+
#endif
172+
173+
pattern TypeSig :: [Located (IdP p)] -> LHsSigWcType p -> Sig p
174+
pattern TypeSig x y <-
175+
#if MIN_GHC_API_VERSION(8,6,0)
176+
GHC.TypeSig _ x y
177+
#else
178+
GHC.TypeSig x y
179+
#endif
180+
163181
pattern ClassOpSig :: Bool -> [Located (IdP pass)] -> LHsSigType pass -> Sig pass
164182
pattern ClassOpSig a b c <-
165183
#if MIN_GHC_API_VERSION(8,6,0)

src/Development/IDE/Plugin/CodeAction.hs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import Data.List.Extra
4848
import qualified Data.Text as T
4949
import Data.Tuple.Extra ((&&&))
5050
import HscTypes
51+
import SrcLoc
5152
import Parser
5253
import Text.Regex.TDFA ((=~), (=~~))
5354
import Text.Regex.TDFA.Text()
@@ -158,6 +159,7 @@ suggestAction dflags packageExports ideOptions parsedModule text diag = concat
158159
[ suggestNewDefinition ideOptions pm text diag
159160
++ suggestRemoveRedundantImport pm text diag
160161
++ suggestNewImport packageExports pm diag
162+
++ suggestDeleteTopBinding pm diag
161163
| Just pm <- [parsedModule]]
162164

163165

@@ -180,6 +182,33 @@ suggestRemoveRedundantImport ParsedModule{pm_parsed_source = L _ HsModule{hsmod
180182
= [("Remove import", [TextEdit (extendToWholeLineIfPossible contents _range) ""])]
181183
| otherwise = []
182184

185+
suggestDeleteTopBinding :: ParsedModule -> Diagnostic -> [(T.Text, [TextEdit])]
186+
suggestDeleteTopBinding ParsedModule{pm_parsed_source = L _ HsModule{hsmodDecls}} Diagnostic{_range=_range,..}
187+
-- Foo.hs:4:1: warning: [-Wunused-top-binds] Defined but not used: ‘f’
188+
| Just [name] <- matchRegex _message ".*Defined but not used: ‘([^ ]+)’"
189+
, let allTopLevel = filter (isTopLevel . fst)
190+
. map (\(L l b) -> (srcSpanToRange l, b))
191+
. sortLocated
192+
$ hsmodDecls
193+
sameName = filter (matchesBindingName (T.unpack name) . snd) allTopLevel
194+
, not (null sameName)
195+
= [("Delete ‘" <> name <> "", flip TextEdit "" . toNextBinding allTopLevel . fst <$> sameName )]
196+
| otherwise = []
197+
where
198+
isTopLevel l = (_character . _start) l == 0
199+
200+
forwardLines lines r = r {_end = (_end r) {_line = (_line . _end $ r) + lines, _character = 0}}
201+
202+
toNextBinding bindings r@Range { _end = Position {_line = l} }
203+
| Just (Range { _start = Position {_line = l'}}, _) <- find ((> l) . _line . _start . fst) bindings
204+
= forwardLines (l' - l) r
205+
toNextBinding _ r = r
206+
207+
matchesBindingName :: String -> HsDecl GhcPs -> Bool
208+
matchesBindingName b (ValD FunBind {fun_id=L _ x}) = showSDocUnsafe (ppr x) == b
209+
matchesBindingName b (SigD (TypeSig (L _ x:_) _)) = showSDocUnsafe (ppr x) == b
210+
matchesBindingName _ _ = False
211+
183212
suggestReplaceIdentifier :: Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
184213
suggestReplaceIdentifier contents Diagnostic{_range=_range,..}
185214
-- File.hs:52:41: error:

test/exe/Main.hs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,7 @@ codeActionTests = testGroup "code actions"
480480
, fillTypedHoleTests
481481
, addSigActionTests
482482
, insertNewDefinitionTests
483+
, deleteUnusedDefinitionTests
483484
]
484485

485486
codeLensesTests :: TestTree
@@ -1150,6 +1151,68 @@ insertNewDefinitionTests = testGroup "insert new definition actions"
11501151
++ txtB')
11511152
]
11521153

1154+
1155+
deleteUnusedDefinitionTests :: TestTree
1156+
deleteUnusedDefinitionTests = testGroup "delete unused definition action"
1157+
[ testSession "delete unused top level binding" $
1158+
testFor
1159+
(T.unlines [ "{-# OPTIONS_GHC -Wunused-top-binds #-}"
1160+
, "module A (some) where"
1161+
, ""
1162+
, "f :: Int -> Int"
1163+
, "f 1 = let a = 1"
1164+
, " in a"
1165+
, "f 2 = 2"
1166+
, ""
1167+
, "some = ()"
1168+
])
1169+
(4, 0)
1170+
"Delete ‘f’"
1171+
(T.unlines [
1172+
"{-# OPTIONS_GHC -Wunused-top-binds #-}"
1173+
, "module A (some) where"
1174+
, ""
1175+
, "some = ()"
1176+
])
1177+
1178+
, testSession "delete unused top level binding defined in infix form" $
1179+
testFor
1180+
(T.unlines [ "{-# OPTIONS_GHC -Wunused-top-binds #-}"
1181+
, "module A (some) where"
1182+
, ""
1183+
, "myPlus :: Int -> Int -> Int"
1184+
, "a `myPlus` b = a + b"
1185+
, ""
1186+
, "some = ()"
1187+
])
1188+
(4, 2)
1189+
"Delete ‘myPlus’"
1190+
(T.unlines [
1191+
"{-# OPTIONS_GHC -Wunused-top-binds #-}"
1192+
, "module A (some) where"
1193+
, ""
1194+
, "some = ()"
1195+
])
1196+
]
1197+
where
1198+
testFor source pos expectedTitle expectedResult = do
1199+
docId <- createDoc "A.hs" "haskell" source
1200+
expectDiagnostics [ ("A.hs", [(DsWarning, pos, "not used")]) ]
1201+
1202+
(action, title) <- extractCodeAction docId "Delete"
1203+
1204+
liftIO $ title @?= expectedTitle
1205+
executeCodeAction action
1206+
contentAfterAction <- documentContents docId
1207+
liftIO $ contentAfterAction @?= expectedResult
1208+
1209+
extractCodeAction docId actionPrefix = do
1210+
Just (CACodeAction action@CodeAction { _title = actionTitle })
1211+
<- find (\(CACodeAction CodeAction{_title=x}) -> actionPrefix `T.isPrefixOf` x)
1212+
<$> getCodeActions docId (R 0 0 0 0)
1213+
return (action, actionTitle)
1214+
1215+
11531216
fixConstructorImportTests :: TestTree
11541217
fixConstructorImportTests = testGroup "fix import actions"
11551218
[ testSession "fix constructor import" $ template

0 commit comments

Comments
 (0)