Skip to content

Commit e26ef51

Browse files
committed
Migrate change-type-signature-plugin to use structured diagnostics
1 parent 4c7e56a commit e26ef51

File tree

15 files changed

+223
-151
lines changed

15 files changed

+223
-151
lines changed

haskell-language-server.cabal

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,12 +1173,14 @@ library hls-change-type-signature-plugin
11731173
build-depends:
11741174
, ghcide == 2.11.0.0
11751175
, hls-plugin-api == 2.11.0.0
1176+
, lens
11761177
, lsp-types
11771178
, regex-tdfa
11781179
, syb
11791180
, text
11801181
, transformers
11811182
, containers
1183+
, ghc
11821184
default-extensions:
11831185
DataKinds
11841186
ExplicitNamespaces
@@ -1196,6 +1198,7 @@ test-suite hls-change-type-signature-plugin-tests
11961198
build-depends:
11971199
, filepath
11981200
, haskell-language-server:hls-change-type-signature-plugin
1201+
, hls-plugin-api
11991202
, hls-test-utils == 2.11.0.0
12001203
, regex-tdfa
12011204
, text

plugins/hls-change-type-signature-plugin/src/Ide/Plugin/ChangeTypeSignature.hs

Lines changed: 161 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,91 @@
1+
{-# LANGUAGE CPP #-}
12
{-# LANGUAGE LambdaCase #-}
23
{-# LANGUAGE ViewPatterns #-}
34
-- | An HLS plugin to provide code actions to change type signatures
45
module Ide.Plugin.ChangeTypeSignature (descriptor
56
-- * For Unit Tests
7+
, Log(..)
68
, errorMessageRegexes
79
) where
810

9-
import Control.Monad (guard)
10-
import Control.Monad.IO.Class (MonadIO)
11-
import Control.Monad.Trans.Except (ExceptT)
12-
import Data.Foldable (asum)
13-
import qualified Data.Map as Map
14-
import Data.Maybe (mapMaybe)
15-
import Data.Text (Text)
16-
import qualified Data.Text as T
17-
import Development.IDE (realSrcSpanToRange)
11+
import Control.Lens
12+
import Control.Monad (guard)
13+
import Control.Monad.IO.Class (MonadIO)
14+
import Control.Monad.Trans.Class (MonadTrans (lift))
15+
import Control.Monad.Trans.Except (ExceptT (..))
16+
import Control.Monad.Trans.Maybe (MaybeT (..), hoistMaybe)
17+
import Data.Foldable (asum)
18+
import qualified Data.Map as Map
19+
import Data.Maybe (catMaybes)
20+
import Data.Text (Text)
21+
import qualified Data.Text as T
22+
import Development.IDE (FileDiagnostic,
23+
IdeState (..), Pretty (..),
24+
Priority (..), Recorder,
25+
WithPriority,
26+
fdLspDiagnosticL,
27+
fdStructuredMessageL,
28+
logWith, realSrcSpanToRange)
1829
import Development.IDE.Core.PluginUtils
19-
import Development.IDE.Core.RuleTypes (GetParsedModule (GetParsedModule))
20-
import Development.IDE.Core.Service (IdeState)
21-
import Development.IDE.GHC.Compat
22-
import Development.IDE.GHC.Util (printOutputable)
23-
import Generics.SYB (extQ, something)
24-
import Ide.Plugin.Error (PluginError,
25-
getNormalizedFilePathE)
26-
import Ide.Types (PluginDescriptor (..),
27-
PluginId (PluginId),
28-
PluginMethodHandler,
29-
defaultPluginDescriptor,
30-
mkPluginHandler)
30+
import Development.IDE.Core.RuleTypes (GetParsedModule (GetParsedModule))
31+
import Development.IDE.GHC.Compat hiding (vcat)
32+
import Development.IDE.GHC.Compat.Error (_TcRnMessage,
33+
msgEnvelopeErrorL)
34+
import Development.IDE.GHC.Util (printOutputable)
35+
import Development.IDE.Types.Diagnostics (_SomeStructuredMessage)
36+
import Generics.SYB (extQ, something)
37+
import GHC.Tc.Errors.Types (ErrInfo (..),
38+
MismatchMsg (..),
39+
SolverReportWithCtxt (..),
40+
TcRnMessage (..),
41+
TcRnMessageDetailed (..),
42+
TcSolverReportMsg (..))
43+
import qualified Ide.Logger as Logger
44+
import Ide.Plugin.Error (PluginError,
45+
getNormalizedFilePathE)
46+
import Ide.Types (Config, HandlerM,
47+
PluginDescriptor (..),
48+
PluginId (PluginId),
49+
PluginMethodHandler,
50+
defaultPluginDescriptor,
51+
mkPluginHandler)
3152
import Language.LSP.Protocol.Message
3253
import Language.LSP.Protocol.Types
33-
import Text.Regex.TDFA ((=~))
54+
import Text.Regex.TDFA ((=~))
3455

35-
descriptor :: PluginId -> PluginDescriptor IdeState
36-
descriptor plId = (defaultPluginDescriptor plId "Provides a code action to change the type signature of a binding if it is wrong")
37-
{ pluginHandlers = mkPluginHandler SMethod_TextDocumentCodeAction (codeActionHandler plId) }
56+
data Log
57+
= LogErrInfoCtxt ErrInfo
58+
| LogFindSigLocFailure DeclName
3859

39-
codeActionHandler :: PluginId -> PluginMethodHandler IdeState 'Method_TextDocumentCodeAction
40-
codeActionHandler plId ideState _ CodeActionParams {_textDocument = TextDocumentIdentifier uri, _context = CodeActionContext diags _ _} = do
41-
nfp <- getNormalizedFilePathE uri
42-
decls <- getDecls plId ideState nfp
43-
let actions = mapMaybe (generateAction plId uri decls) diags
44-
pure $ InL actions
60+
instance Pretty Log where
61+
pretty = \case
62+
LogErrInfoCtxt (ErrInfo ctxt suppl) ->
63+
Logger.vcat [fromSDoc ctxt, fromSDoc suppl]
64+
LogFindSigLocFailure name ->
65+
pretty ("Lookup signature location failure: " <> name)
66+
where
67+
fromSDoc = pretty . printOutputable
68+
69+
descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState
70+
descriptor recorder plId =
71+
(defaultPluginDescriptor plId "Provides a code action to change the type signature of a binding if it is wrong")
72+
{ pluginHandlers = mkPluginHandler SMethod_TextDocumentCodeAction (codeActionHandler recorder plId)
73+
}
74+
75+
codeActionHandler
76+
:: Recorder (WithPriority Log)
77+
-> PluginId
78+
-> PluginMethodHandler IdeState 'Method_TextDocumentCodeAction
79+
codeActionHandler recorder plId ideState _ CodeActionParams{_textDocument, _range} = do
80+
let TextDocumentIdentifier uri = _textDocument
81+
nfp <- getNormalizedFilePathE uri
82+
decls <- getDecls plId ideState nfp
83+
84+
activeDiagnosticsInRange (shakeExtras ideState) nfp _range >>= \case
85+
Nothing -> pure (InL [])
86+
Just fileDiags -> do
87+
actions <- lift $ mapM (generateAction recorder plId uri decls) fileDiags
88+
pure (InL (catMaybes actions))
4589

4690
getDecls :: MonadIO m => PluginId -> IdeState -> NormalizedFilePath -> ExceptT PluginError m [LHsDecl GhcPs]
4791
getDecls (PluginId changeTypeSignatureId) state =
@@ -67,39 +111,104 @@ data ChangeSignature = ChangeSignature {
67111
-- | the location of the declaration signature
68112
, declSrcSpan :: RealSrcSpan
69113
-- | the diagnostic to solve
70-
, diagnostic :: Diagnostic
114+
, diagnostic :: FileDiagnostic
71115
}
72116

73117
-- | Create a CodeAction from a Diagnostic
74-
generateAction :: PluginId -> Uri -> [LHsDecl GhcPs] -> Diagnostic -> Maybe (Command |? CodeAction)
75-
generateAction plId uri decls diag = changeSigToCodeAction plId uri <$> diagnosticToChangeSig decls diag
118+
generateAction
119+
:: Recorder (WithPriority Log)
120+
-> PluginId
121+
-> Uri
122+
-> [LHsDecl GhcPs]
123+
-> FileDiagnostic
124+
-> HandlerM Config (Maybe (Command |? CodeAction))
125+
generateAction recorder plId uri decls fileDiag = do
126+
changeSig <- diagnosticToChangeSig recorder decls fileDiag
127+
pure $
128+
changeSigToCodeAction plId uri <$> changeSig
76129

77130
-- | Convert a diagnostic into a ChangeSignature and add the proper SrcSpan
78-
diagnosticToChangeSig :: [LHsDecl GhcPs] -> Diagnostic -> Maybe ChangeSignature
79-
diagnosticToChangeSig decls diagnostic = do
80-
-- regex match on the GHC Error Message
81-
(expectedType, actualType, declName) <- matchingDiagnostic diagnostic
82-
-- Find the definition and it's location
83-
declSrcSpan <- findSigLocOfStringDecl decls expectedType (T.unpack declName)
84-
pure $ ChangeSignature{..}
131+
diagnosticToChangeSig
132+
:: Recorder (WithPriority Log)
133+
-> [LHsDecl GhcPs]
134+
-> FileDiagnostic
135+
-> HandlerM Config (Maybe ChangeSignature)
136+
diagnosticToChangeSig recorder decls diagnostic = runMaybeT $ do
137+
-- Extract expected, actual, and extra error info
138+
(expectedType, actualType, errInfo) <- hoistMaybe $ do
139+
msg <- diagnostic ^. fdStructuredMessageL ^? _SomeStructuredMessage
140+
tcRnMsg <- msg ^. msgEnvelopeErrorL ^? _TcRnMessage
141+
(solverReport, errInfo) <- findSolverReport tcRnMsg
142+
mismatch <- findMismatchMessage solverReport
143+
(expectedType', actualType') <- findTypeEqMismatch mismatch
144+
errInfo' <- errInfo
145+
146+
pure (showType expectedType', showType actualType', errInfo')
147+
148+
logWith recorder Debug (LogErrInfoCtxt errInfo)
149+
150+
-- Extract the declName from the extra error text
151+
declName <- hoistMaybe (matchingDiagnostic errInfo)
85152

153+
-- Look up location of declName. If it fails, log it
154+
declSrcSpan <-
155+
case findSigLocOfStringDecl decls expectedType (T.unpack declName) of
156+
Just x -> pure x
157+
Nothing -> do
158+
logWith recorder Debug (LogFindSigLocFailure declName)
159+
hoistMaybe Nothing
160+
161+
pure ChangeSignature{..}
162+
where
163+
showType :: Type -> Text
164+
showType = T.pack . showSDocUnsafe . pprTidiedType
165+
166+
-- TODO: Make this a prism?
167+
findSolverReport :: TcRnMessage -> Maybe (TcSolverReportMsg, Maybe ErrInfo)
168+
findSolverReport (TcRnMessageWithInfo _ (TcRnMessageDetailed errInfo msg)) =
169+
case findSolverReport msg of
170+
Just (mismatch, _) -> Just (mismatch, Just errInfo)
171+
_ -> Nothing
172+
#if MIN_VERSION_ghc(9,10,0)
173+
findSolverReport (TcRnSolverReport (SolverReportWithCtxt _ mismatch) _) =
174+
Just (mismatch, Nothing)
175+
#else
176+
findSolverReport (TcRnSolverReport (SolverReportWithCtxt _ mismatch) _ _) =
177+
Just (mismatch, Nothing)
178+
#endif
179+
findSolverReport _ = Nothing
180+
181+
-- TODO: Make this a prism?
182+
findMismatchMessage :: TcSolverReportMsg -> Maybe MismatchMsg
183+
findMismatchMessage (Mismatch m _ _ _) = Just m
184+
findMismatchMessage (CannotUnifyVariable m _) = Just m
185+
findMismatchMessage _ = Nothing
186+
187+
-- TODO: Make this a prism?
188+
findTypeEqMismatch :: MismatchMsg -> Maybe (Type, Type)
189+
#if MIN_VERSION_ghc(9,12,0)
190+
findTypeEqMismatch (TypeEqMismatch _ _ _ expected actual _ _) = Just (expected, actual)
191+
#else
192+
findTypeEqMismatch (TypeEqMismatch _ _ _ _ expected actual _ _) = Just (expected, actual)
193+
#endif
194+
findTypeEqMismatch _ = Nothing
86195

87196
-- | If a diagnostic has the proper message create a ChangeSignature from it
88-
matchingDiagnostic :: Diagnostic -> Maybe (ExpectedSig, ActualSig, DeclName)
89-
matchingDiagnostic Diagnostic{_message} = asum $ map (unwrapMatch . (=~) _message) errorMessageRegexes
197+
matchingDiagnostic :: ErrInfo -> Maybe DeclName
198+
matchingDiagnostic ErrInfo{errInfoContext} =
199+
asum $ map (unwrapMatch . (=~) errInfoTxt) errorMessageRegexes
90200
where
91-
unwrapMatch :: (Text, Text, Text, [Text]) -> Maybe (ExpectedSig, ActualSig, DeclName)
92-
-- due to using (.|\n) in regex we have to drop the erroneous, but necessary ("." doesn't match newlines), match
93-
unwrapMatch (_, _, _, [expect, actual, _, name]) = Just (expect, actual, name)
94-
unwrapMatch _ = Nothing
201+
unwrapMatch :: (Text, Text, Text, [Text]) -> Maybe DeclName
202+
unwrapMatch (_, _, _, [name]) = Just name
203+
unwrapMatch _ = Nothing
204+
205+
errInfoTxt = printOutputable errInfoContext
95206

96207
-- | List of regexes that match various Error Messages
97208
errorMessageRegexes :: [Text]
98209
errorMessageRegexes = [ -- be sure to add new Error Messages Regexes at the bottom to not fail any existing tests
99-
"Expected type: (.+)\n +Actual type: (.+)\n(.|\n)+In an equation for ‘(.+)’"
100-
, "Couldn't match expected type ‘(.+)’ with actual type ‘(.+)’\n(.|\n)+In an equation for ‘(.+)’"
101-
-- GHC >9.2 version of the first error regex
102-
, "Expected: (.+)\n +Actual: (.+)\n(.|\n)+In an equation for ‘(.+)’"
210+
"In an equation for ‘(.+)’:" -- TODO: Check if this is useful only for tests
211+
, "In an equation for `(.+)':"
103212
]
104213

105214
-- | Given a String with the name of a declaration, GHC's "Expected Type", find the declaration that matches
@@ -147,7 +256,7 @@ changeSigToCodeAction :: PluginId -> Uri -> ChangeSignature -> Command |? CodeAc
147256
changeSigToCodeAction (PluginId changeTypeSignatureId) uri ChangeSignature{..} =
148257
InR CodeAction { _title = mkChangeSigTitle declName actualType
149258
, _kind = Just (CodeActionKind_Custom ("quickfix." <> changeTypeSignatureId))
150-
, _diagnostics = Just [diagnostic]
259+
, _diagnostics = Just [diagnostic ^. fdLspDiagnosticL ]
151260
, _isPreferred = Nothing
152261
, _disabled = Nothing
153262
, _edit = Just $ mkChangeSigEdit uri declSrcSpan (mkNewSignature declName actualType)

plugins/hls-change-type-signature-plugin/test/Main.hs

Lines changed: 18 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import Data.Either (rights)
55
import Data.Text (Text)
66
import qualified Data.Text as T
77
import qualified Data.Text.IO as TIO
8-
import Ide.Plugin.ChangeTypeSignature (errorMessageRegexes)
8+
import Ide.Plugin.ChangeTypeSignature (Log (..), errorMessageRegexes)
99
import qualified Ide.Plugin.ChangeTypeSignature as ChangeTypeSignature
1010
import System.FilePath ((<.>), (</>))
1111
import Test.Hls (CodeAction (..), Command,
@@ -21,8 +21,7 @@ import Test.Hls (CodeAction (..), Command,
2121
getCodeActions,
2222
goldenWithHaskellDoc,
2323
knownBrokenForGhcVersions,
24-
liftIO,
25-
mkPluginTestDescriptor',
24+
liftIO, mkPluginTestDescriptor,
2625
openDoc, runSessionWithServer,
2726
testCase, testGroup, toEither,
2827
type (|?), waitForBuildQueue,
@@ -32,16 +31,19 @@ import Text.Regex.TDFA ((=~))
3231
main :: IO ()
3332
main = defaultTestRunner test
3433

35-
changeTypeSignaturePlugin :: PluginTestDescriptor ()
36-
changeTypeSignaturePlugin = mkPluginTestDescriptor' ChangeTypeSignature.descriptor "changeTypeSignature"
34+
changeTypeSignaturePlugin :: PluginTestDescriptor Log
35+
changeTypeSignaturePlugin =
36+
mkPluginTestDescriptor
37+
ChangeTypeSignature.descriptor
38+
"changeTypeSignature"
3739

3840
test :: TestTree
3941
test = testGroup "changeTypeSignature" [
4042
testRegexes
4143
, codeActionTest "TExpectedActual" 4 11
42-
, knownBrokenForGhcVersions [GHC96 .. GHC912] "Error Message in 9.2+ does not provide enough info" $
44+
, knownBrokenForGhcVersions [GHC96 .. GHC912] "Error Message in 9.6+ does not provide enough info" $
4345
codeActionTest "TRigidType" 4 14
44-
, codeActionTest "TRigidType2" 4 6
46+
, codeActionTest "TRigidType2" 4 8
4547
, codeActionTest "TLocalBinding" 7 22
4648
, codeActionTest "TLocalBindingShadow1" 11 8
4749
, codeActionTest "TLocalBindingShadow2" 7 22
@@ -50,43 +52,17 @@ test = testGroup "changeTypeSignature" [
5052

5153
testRegexes :: TestTree
5254
testRegexes = testGroup "Regex Testing" [
53-
testRegexOne
54-
, testRegexTwo
55-
, testRegex921One
56-
]
57-
58-
testRegexOne :: TestTree
59-
testRegexOne = testGroup "Regex One" [
60-
regexTest "error1.txt" regex True
61-
, regexTest "error2.txt" regex True
62-
, regexTest "error3.txt" regex False
63-
, regexTest "error4.txt" regex True
64-
, regexTest "error5.txt" regex True
55+
regexTest "TExpectedActual.txt" regex True
56+
, regexTest "TLocalBinding.txt" regex True
57+
, regexTest "TLocalBindingShadow1.txt" regex True
58+
, regexTest "TLocalBindingShadow2.txt" regex True
59+
-- Error message from GHC currently does not not provide enough info
60+
, regexTest "TRigidType.txt" regex False
61+
, regexTest "TRigidType2.txt" regex True
6562
]
6663
where
6764
regex = errorMessageRegexes !! 0
6865

69-
testRegexTwo :: TestTree
70-
testRegexTwo = testGroup "Regex Two" [
71-
regexTest "error1.txt" regex False
72-
, regexTest "error2.txt" regex False
73-
, regexTest "error3.txt" regex True
74-
, regexTest "error4.txt" regex False
75-
, regexTest "error5.txt" regex False
76-
]
77-
where
78-
regex = errorMessageRegexes !! 1
79-
80-
-- test ghc-9.2 error message regex
81-
testRegex921One :: TestTree
82-
testRegex921One = testGroup "Regex One" [
83-
regexTest "ghc921-error1.txt" regex True
84-
, regexTest "ghc921-error2.txt" regex True
85-
, regexTest "ghc921-error3.txt" regex True
86-
]
87-
where
88-
regex = errorMessageRegexes !! 2
89-
9066
testDataDir :: FilePath
9167
testDataDir = "plugins" </> "hls-change-type-signature-plugin" </> "test" </> "testdata"
9268

@@ -123,8 +99,8 @@ regexTest :: FilePath -> Text -> Bool -> TestTree
12399
regexTest fp regex shouldPass = testCase fp $ do
124100
msg <- TIO.readFile (testDataDir </> fp)
125101
case (msg =~ regex :: (Text, Text, Text, [Text]), shouldPass) of
126-
((_, _, _, [_, _, _, _]), True) -> pure ()
127-
((_, _, _, [_, _, _, _]), False) -> assertFailure $ "Unexpected match: " <> fp <> " with " <> T.unpack regex
102+
((_, _, _, [_]), True) -> pure ()
103+
((_, _, _, [_]), False) -> assertFailure $ "Unexpected match: " <> fp <> " with " <> T.unpack regex
128104
(_, True) -> assertFailure $ "Failed to match: " <> fp <> " with " <> T.unpack regex
129105
(_, False) -> pure ()
130106

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
In the expression: go
2+
In an equation for ‘fullSig’:
3+
fullSig
4+
= go
5+
where
6+
go = head . reverse
7+
8+

0 commit comments

Comments
 (0)