Skip to content

Commit 0bcdc6a

Browse files
Fix for #45 - remove redundant symbols from imports (#290)
* Test for #45 * Remove redundant symbols from imports Fixes #45 * Update src/Development/IDE/LSP/CodeAction.hs Co-Authored-By: Andreas Herrmann <42969706+aherrmann-da@users.noreply.github.com> * Apply suggestions from code review Co-Authored-By: Andreas Herrmann <42969706+aherrmann-da@users.noreply.github.com> * Add regex-tdfa extra deps to ghc-lib build * Fix for GHC 8.4 (error message prints qualified binding) GHC ticket #14881 changed this to print identifiers unqualified * dropBindingsFromImportLine: make total Co-authored-by: Andreas Herrmann <42969706+aherrmann-da@users.noreply.github.com>
1 parent 359cdf5 commit 0bcdc6a

File tree

7 files changed

+123
-3
lines changed

7 files changed

+123
-3
lines changed

ghcide.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ library
4949
prettyprinter-ansi-terminal,
5050
prettyprinter-ansi-terminal,
5151
prettyprinter,
52+
regex-tdfa >= 1.3.1.0,
5253
rope-utf16-splay,
5354
safe-exceptions,
5455
shake >= 0.17.5,

src/Development/IDE/LSP/CodeAction.hs

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import Data.Char
3030
import Data.Maybe
3131
import Data.List.Extra
3232
import qualified Data.Text as T
33+
import Text.Regex.TDFA ((=~), (=~~))
34+
import Text.Regex.TDFA.Text()
3335

3436
-- | Generate code actions.
3537
codeAction
@@ -85,14 +87,18 @@ executeAddSignatureCommand _lsp _ideState ExecuteCommandParams{..}
8587

8688
suggestAction :: Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
8789
suggestAction contents diag@Diagnostic{_range=_range@Range{..},..}
90+
-- The qualified import of ‘many’ from module ‘Control.Applicative’ is redundant
91+
| Just [_, bindings] <- matchRegex _message "The( qualified)? import of ‘([^’]*)’ from module [^ ]* is redundant"
92+
, Just c <- contents
93+
, importLine <- textInRange _range c
94+
= [( "Remove " <> bindings <> " from import"
95+
, [TextEdit _range (dropBindingsFromImportLine (T.splitOn "," bindings) importLine)])]
8896

8997
-- File.hs:16:1: warning:
9098
-- The import of `Data.List' is redundant
9199
-- except perhaps to import instances from `Data.List'
92100
-- To import instances alone, use: import Data.List()
93-
| "The import of " `T.isInfixOf` _message
94-
|| "The qualified import of " `T.isInfixOf` _message
95-
, " is redundant" `T.isInfixOf` _message
101+
| _message =~ ("The( qualified)? import of [^ ]* is redundant" :: String)
96102
= [("Remove import", [TextEdit (extendToWholeLineIfPossible contents _range) ""])]
97103

98104
-- File.hs:52:41: error:
@@ -293,6 +299,51 @@ textInRange (Range (Position startRow startCol) (Position endRow endCol)) text =
293299
where
294300
linesBeginningWithStartLine = drop startRow (T.splitOn "\n" text)
295301

302+
-- | Drop all occurrences of a binding in an import line.
303+
-- Preserves well-formedness but not whitespace between bindings.
304+
--
305+
-- >>> dropBindingsFromImportLine ["bA", "bC"] "import A(bA, bB,bC ,bA)"
306+
-- "import A(bB)"
307+
--
308+
-- >>> dropBindingsFromImportLine ["+"] "import "P" qualified A as B ((+))"
309+
-- "import "P" qualified A() as B hiding (bB)"
310+
dropBindingsFromImportLine :: [T.Text] -> T.Text -> T.Text
311+
dropBindingsFromImportLine bindings_ importLine =
312+
importPre <> "(" <> importRest'
313+
where
314+
bindings = map (wrapOperatorInParens . removeQualified) bindings_
315+
316+
(importPre, importRest) = T.breakOn "(" importLine
317+
318+
wrapOperatorInParens x = if isAlpha (T.head x) then x else "(" <> x <> ")"
319+
320+
removeQualified x = case T.breakOn "." x of
321+
(_qualifier, T.uncons -> Just (_, unqualified)) -> unqualified
322+
_ -> x
323+
324+
importRest' = case T.uncons importRest of
325+
Just (_, x) ->
326+
T.intercalate ","
327+
$ joinCloseParens
328+
$ mapMaybe (filtering . T.strip)
329+
$ T.splitOn "," x
330+
Nothing -> importRest
331+
332+
filtering x = case () of
333+
() | x `elem` bindings -> Nothing
334+
() | x `elem` map (<> ")") bindings -> Just ")"
335+
_ -> Just x
336+
337+
joinCloseParens (x : ")" : rest) = (x <> ")") : joinCloseParens rest
338+
joinCloseParens (x : rest) = x : joinCloseParens rest
339+
joinCloseParens [] = []
340+
341+
-- | Returns Just (the submatches) for the first capture, or Nothing.
342+
matchRegex :: T.Text -> T.Text -> Maybe [T.Text]
343+
matchRegex message regex = case message =~~ regex of
344+
Just (_ :: T.Text, _ :: T.Text, _ :: T.Text, bindings) -> Just bindings
345+
Nothing -> Nothing
346+
296347
setHandlersCodeAction :: PartialHandlers
297348
setHandlersCodeAction = PartialHandlers $ \WithMessage{..} x -> return x{
298349
LSP.codeActionHandler = withResponse RspCodeAction codeAction

stack-ghc-lib.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ extra-deps:
99
- ghc-lib-parser-8.8.1
1010
- ghc-lib-8.8.1
1111
- fuzzy-0.1.0.0
12+
- regex-base-0.94.0.0
13+
- regex-tdfa-1.3.1.0
1214
nix:
1315
packages: [zlib]
1416
flags:

stack.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@ extra-deps:
77
- lsp-test-0.9.0.0
88
- hie-bios-0.3.0
99
- fuzzy-0.1.0.0
10+
- regex-base-0.94.0.0
11+
- regex-tdfa-1.3.1.0
1012
nix:
1113
packages: [zlib]

stack84.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ extra-deps:
1212
- js-dgtable-0.5.2
1313
- hie-bios-0.3.0
1414
- fuzzy-0.1.0.0
15+
- regex-base-0.94.0.0
16+
- regex-tdfa-1.3.1.0
1517
nix:
1618
packages: [zlib]
1719
allow-newer: true

stack88.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ extra-deps:
77
- lsp-test-0.9.0.0
88
- hie-bios-0.3.0
99
- fuzzy-0.1.0.0
10+
- regex-base-0.94.0.0
11+
- regex-tdfa-1.3.1.0
1012
allow-newer: true
1113
nix:
1214
packages: [zlib]

test/exe/Main.hs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,66 @@ removeImportTests = testGroup "remove import actions"
594594
, "stuffB = 123"
595595
]
596596
liftIO $ expectedContentAfterAction @=? contentAfterAction
597+
, testSession "redundant binding" $ do
598+
let contentA = T.unlines
599+
[ "module ModuleA where"
600+
, "stuffA = False"
601+
, "stuffB :: Integer"
602+
, "stuffB = 123"
603+
]
604+
_docA <- openDoc' "ModuleA.hs" "haskell" contentA
605+
let contentB = T.unlines
606+
[ "{-# OPTIONS_GHC -Wunused-imports #-}"
607+
, "module ModuleB where"
608+
, "import ModuleA (stuffA, stuffB)"
609+
, "main = print stuffB"
610+
]
611+
docB <- openDoc' "ModuleB.hs" "haskell" contentB
612+
_ <- waitForDiagnostics
613+
[CACodeAction action@CodeAction { _title = actionTitle }]
614+
<- getCodeActions docB (Range (Position 2 0) (Position 2 5))
615+
liftIO $ "Remove stuffA from import" @=? actionTitle
616+
executeCodeAction action
617+
contentAfterAction <- documentContents docB
618+
let expectedContentAfterAction = T.unlines
619+
[ "{-# OPTIONS_GHC -Wunused-imports #-}"
620+
, "module ModuleB where"
621+
, "import ModuleA (stuffB)"
622+
, "main = print stuffB"
623+
]
624+
liftIO $ expectedContentAfterAction @=? contentAfterAction
625+
, testSession "redundant symbol binding" $ do
626+
let contentA = T.unlines
627+
[ "module ModuleA where"
628+
, "a !! b = a"
629+
, "stuffB :: Integer"
630+
, "stuffB = 123"
631+
]
632+
_docA <- openDoc' "ModuleA.hs" "haskell" contentA
633+
let contentB = T.unlines
634+
[ "{-# OPTIONS_GHC -Wunused-imports #-}"
635+
, "module ModuleB where"
636+
, "import qualified ModuleA as A ((!!), stuffB, (!!))"
637+
, "main = print A.stuffB"
638+
]
639+
docB <- openDoc' "ModuleB.hs" "haskell" contentB
640+
_ <- waitForDiagnostics
641+
[CACodeAction action@CodeAction { _title = actionTitle }]
642+
<- getCodeActions docB (Range (Position 2 0) (Position 2 5))
643+
#if MIN_GHC_API_VERSION(8,6,0)
644+
liftIO $ "Remove !! from import" @=? actionTitle
645+
#else
646+
liftIO $ "Remove A.!! from import" @=? actionTitle
647+
#endif
648+
executeCodeAction action
649+
contentAfterAction <- documentContents docB
650+
let expectedContentAfterAction = T.unlines
651+
[ "{-# OPTIONS_GHC -Wunused-imports #-}"
652+
, "module ModuleB where"
653+
, "import qualified ModuleA as A (stuffB)"
654+
, "main = print A.stuffB"
655+
]
656+
liftIO $ expectedContentAfterAction @=? contentAfterAction
597657
]
598658

599659
importRenameActionTests :: TestTree

0 commit comments

Comments
 (0)