Skip to content

Commit 0b2e582

Browse files
Code action: remove redundant constraints for type signature (haskell/ghcide#692)
* Code action: remove redundant constraints for type signature * Handle peculiar formatting Make the content parsing safe for type signature formatted with an arbitrary and unexpected number of spaces and/or line feeds.
1 parent 7e11ace commit 0b2e582

File tree

3 files changed

+158
-1
lines changed

3 files changed

+158
-1
lines changed

ghcide/ghcide.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ library
5959
prettyprinter,
6060
regex-tdfa >= 1.3.1.0,
6161
rope-utf16-splay,
62+
safe,
6263
safe-exceptions,
6364
shake >= 0.18.4,
6465
sorted-list,
@@ -323,6 +324,7 @@ test-suite ghcide-tests
323324
QuickCheck,
324325
quickcheck-instances,
325326
rope-utf16-splay,
327+
safe,
326328
safe-exceptions,
327329
shake,
328330
tasty,

ghcide/src/Development/IDE/Plugin/CodeAction.hs

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ module Development.IDE.Plugin.CodeAction
1818
) where
1919

2020
import Language.Haskell.LSP.Types
21-
import Control.Monad (join)
21+
import Control.Monad (join, guard)
2222
import Development.IDE.Plugin
2323
import Development.IDE.GHC.Compat
2424
import Development.IDE.Core.Rules
@@ -57,6 +57,7 @@ import Data.Function
5757
import Control.Arrow ((>>>))
5858
import Data.Functor
5959
import Control.Applicative ((<|>))
60+
import Safe (atMay)
6061

6162
plugin :: Plugin c
6263
plugin = codeActionPluginWithRules rules codeAction <> Plugin mempty setHandlersCodeLens
@@ -147,6 +148,7 @@ suggestAction dflags packageExports ideOptions parsedModule text diag = concat
147148
, suggestReplaceIdentifier text diag
148149
, suggestSignature True diag
149150
, suggestConstraint text diag
151+
, removeRedundantConstraints text diag
150152
, suggestAddTypeAnnotationToSatisfyContraints text diag
151153
] ++ concat
152154
[ suggestNewDefinition ideOptions pm text diag
@@ -586,6 +588,83 @@ suggestFunctionConstraint contents Diagnostic{..} missingConstraint
586588
actionTitle constraint typeSignatureName = "Add `" <> constraint
587589
<> "` to the context of the type signature for `" <> typeSignatureName <> "`"
588590

591+
-- | Suggests the removal of a redundant constraint for a type signature.
592+
removeRedundantConstraints :: Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
593+
removeRedundantConstraints mContents Diagnostic{..}
594+
-- • Redundant constraint: Eq a
595+
-- • In the type signature for:
596+
-- foo :: forall a. Eq a => a -> a
597+
-- • Redundant constraints: (Monoid a, Show a)
598+
-- • In the type signature for:
599+
-- foo :: forall a. (Num a, Monoid a, Eq a, Show a) => a -> Bool
600+
| Just contents <- mContents
601+
-- Account for both "Redundant constraint" and "Redundant constraints".
602+
, True <- "Redundant constraint" `T.isInfixOf` _message
603+
, Just typeSignatureName <- findTypeSignatureName _message
604+
, Just redundantConstraintList <- findRedundantConstraints _message
605+
, Just constraints <- findConstraints contents typeSignatureName
606+
= let constraintList = parseConstraints constraints
607+
newConstraints = buildNewConstraints constraintList redundantConstraintList
608+
typeSignatureLine = findTypeSignatureLine contents typeSignatureName
609+
typeSignatureFirstChar = T.length $ typeSignatureName <> " :: "
610+
startOfConstraint = Position typeSignatureLine typeSignatureFirstChar
611+
endOfConstraint = Position typeSignatureLine $
612+
typeSignatureFirstChar + T.length (constraints <> " => ")
613+
range = Range startOfConstraint endOfConstraint
614+
in [(actionTitle redundantConstraintList typeSignatureName, [TextEdit range newConstraints])]
615+
| otherwise = []
616+
where
617+
parseConstraints :: T.Text -> [T.Text]
618+
parseConstraints t = t
619+
& (T.strip >>> stripConstraintsParens >>> T.splitOn ",")
620+
<&> T.strip
621+
622+
stripConstraintsParens :: T.Text -> T.Text
623+
stripConstraintsParens constraints =
624+
if "(" `T.isPrefixOf` constraints
625+
then constraints & T.drop 1 & T.dropEnd 1 & T.strip
626+
else constraints
627+
628+
findRedundantConstraints :: T.Text -> Maybe [T.Text]
629+
findRedundantConstraints t = t
630+
& T.lines
631+
& head
632+
& T.strip
633+
& (`matchRegex` "Redundant constraints?: (.+)")
634+
<&> (head >>> parseConstraints)
635+
636+
-- If the type signature is not formatted as expected (arbitrary number of spaces,
637+
-- line feeds...), just fail.
638+
findConstraints :: T.Text -> T.Text -> Maybe T.Text
639+
findConstraints contents typeSignatureName = do
640+
constraints <- contents
641+
& T.splitOn (typeSignatureName <> " :: ")
642+
& (`atMay` 1)
643+
>>= (T.splitOn " => " >>> (`atMay` 0))
644+
guard $ not $ "\n" `T.isInfixOf` constraints || T.strip constraints /= constraints
645+
return constraints
646+
647+
formatConstraints :: [T.Text] -> T.Text
648+
formatConstraints [] = ""
649+
formatConstraints [constraint] = constraint
650+
formatConstraints constraintList = constraintList
651+
& T.intercalate ", "
652+
& \cs -> "(" <> cs <> ")"
653+
654+
formatConstraintsWithArrow :: [T.Text] -> T.Text
655+
formatConstraintsWithArrow [] = ""
656+
formatConstraintsWithArrow cs = cs & formatConstraints & (<> " => ")
657+
658+
buildNewConstraints :: [T.Text] -> [T.Text] -> T.Text
659+
buildNewConstraints constraintList redundantConstraintList =
660+
formatConstraintsWithArrow $ constraintList \\ redundantConstraintList
661+
662+
actionTitle :: [T.Text] -> T.Text -> T.Text
663+
actionTitle constraintList typeSignatureName =
664+
"Remove redundant constraint" <> (if length constraintList == 1 then "" else "s") <> " `"
665+
<> formatConstraints constraintList
666+
<> "` from the context of the type signature for `" <> typeSignatureName <> "`"
667+
589668
-------------------------------------------------------------------------------------------------
590669

591670
suggestNewImport :: PackageExportsMap -> ParsedModule -> Diagnostic -> [(T.Text, [TextEdit])]

ghcide/test/exe/Main.hs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,7 @@ codeActionTests = testGroup "code actions"
486486
, deleteUnusedDefinitionTests
487487
, addInstanceConstraintTests
488488
, addFunctionConstraintTests
489+
, removeRedundantConstraintsTests
489490
, addTypeAnnotationsToLiteralsTest
490491
]
491492

@@ -1553,6 +1554,81 @@ addFunctionConstraintTests = let
15531554
(incompleteConstraintSourceCode2 $ Just "Eq c")
15541555
]
15551556

1557+
removeRedundantConstraintsTests :: TestTree
1558+
removeRedundantConstraintsTests = let
1559+
header =
1560+
[ "{-# OPTIONS_GHC -Wredundant-constraints #-}"
1561+
, "module Testing where"
1562+
, ""
1563+
]
1564+
1565+
redundantConstraintsCode :: Maybe T.Text -> T.Text
1566+
redundantConstraintsCode mConstraint =
1567+
let constraint = maybe "" (\c -> "" <> c <> " => ") mConstraint
1568+
in T.unlines $ header <>
1569+
[ "foo :: " <> constraint <> "a -> a"
1570+
, "foo = id"
1571+
]
1572+
1573+
redundantMixedConstraintsCode :: Maybe T.Text -> T.Text
1574+
redundantMixedConstraintsCode mConstraint =
1575+
let constraint = maybe "(Num a, Eq a)" (\c -> "(Num a, Eq a, " <> c <> ")") mConstraint
1576+
in T.unlines $ header <>
1577+
[ "foo :: " <> constraint <> " => a -> Bool"
1578+
, "foo x = x == 1"
1579+
]
1580+
1581+
typeSignatureSpaces :: T.Text
1582+
typeSignatureSpaces = T.unlines $ header <>
1583+
[ "foo :: (Num a, Eq a, Monoid a) => a -> Bool"
1584+
, "foo x = x == 1"
1585+
]
1586+
1587+
typeSignatureMultipleLines :: T.Text
1588+
typeSignatureMultipleLines = T.unlines $ header <>
1589+
[ "foo :: (Num a, Eq a, Monoid a)"
1590+
, "=> a -> Bool"
1591+
, "foo x = x == 1"
1592+
]
1593+
1594+
check :: T.Text -> T.Text -> T.Text -> TestTree
1595+
check actionTitle originalCode expectedCode = testSession (T.unpack actionTitle) $ do
1596+
doc <- createDoc "Testing.hs" "haskell" originalCode
1597+
_ <- waitForDiagnostics
1598+
actionsOrCommands <- getCodeActions doc (Range (Position 4 0) (Position 4 maxBound))
1599+
chosenAction <- liftIO $ pickActionWithTitle actionTitle actionsOrCommands
1600+
executeCodeAction chosenAction
1601+
modifiedCode <- documentContents doc
1602+
liftIO $ expectedCode @=? modifiedCode
1603+
1604+
checkPeculiarFormatting :: String -> T.Text -> TestTree
1605+
checkPeculiarFormatting title code = testSession title $ do
1606+
doc <- createDoc "Testing.hs" "haskell" code
1607+
_ <- waitForDiagnostics
1608+
actionsOrCommands <- getCodeActions doc (Range (Position 4 0) (Position 4 maxBound))
1609+
liftIO $ assertBool "Found some actions" (null actionsOrCommands)
1610+
1611+
in testGroup "remove redundant function constraints"
1612+
[ check
1613+
"Remove redundant constraint `Eq a` from the context of the type signature for `foo`"
1614+
(redundantConstraintsCode $ Just "Eq a")
1615+
(redundantConstraintsCode Nothing)
1616+
, check
1617+
"Remove redundant constraints `(Eq a, Monoid a)` from the context of the type signature for `foo`"
1618+
(redundantConstraintsCode $ Just "(Eq a, Monoid a)")
1619+
(redundantConstraintsCode Nothing)
1620+
, check
1621+
"Remove redundant constraints `(Monoid a, Show a)` from the context of the type signature for `foo`"
1622+
(redundantMixedConstraintsCode $ Just "Monoid a, Show a")
1623+
(redundantMixedConstraintsCode Nothing)
1624+
, checkPeculiarFormatting
1625+
"should do nothing when constraints contain an arbitrary number of spaces"
1626+
typeSignatureSpaces
1627+
, checkPeculiarFormatting
1628+
"should do nothing when constraints contain line feeds"
1629+
typeSignatureMultipleLines
1630+
]
1631+
15561632
addSigActionTests :: TestTree
15571633
addSigActionTests = let
15581634
header = "{-# OPTIONS_GHC -Wmissing-signatures -Wmissing-pattern-synonym-signatures #-}"

0 commit comments

Comments
 (0)