1+ {-# LANGUAGE CPP #-}
12{-# LANGUAGE LambdaCase #-}
23{-# LANGUAGE ViewPatterns #-}
34-- | An HLS plugin to provide code actions to change type signatures
45module 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 )
1829import 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 )
3152import Language.LSP.Protocol.Message
3253import 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
4690getDecls :: MonadIO m => PluginId -> IdeState -> NormalizedFilePath -> ExceptT PluginError m [LHsDecl GhcPs ]
4791getDecls (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
97208errorMessageRegexes :: [Text ]
98209errorMessageRegexes = [ -- 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
147256changeSigToCodeAction (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)
0 commit comments