Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various PluginError PR suggestions I missed earlier #3737

Merged
merged 34 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
8adb03e
WIP
fendor Jun 15, 2023
4e65cbe
Merge fendor/enhance/plugin-logger-structure
joyfulmantis Jul 13, 2023
d4a9d4e
Make compilable
joyfulmantis Jul 14, 2023
e1b62cb
merge upstream/master
joyfulmantis Jul 14, 2023
e34da52
Flatten error hierarchy and avoid name clashes
joyfulmantis Jul 18, 2023
ca81317
merge upstream/master
joyfulmantis Jul 18, 2023
1776bd8
Replace ResponseError with PluginError for plugins
joyfulmantis Jul 20, 2023
bc093c2
Further support for PluginError in HLS.hs among other enhancements
joyfulmantis Jul 21, 2023
9bc837a
Further improvements
joyfulmantis Jul 22, 2023
40d589c
Fix code-range test
joyfulmantis Jul 22, 2023
fd11649
Merge branch 'master' into plugin-logger
joyfulmantis Jul 22, 2023
15cf638
Fix build error
joyfulmantis Jul 22, 2023
cdc0364
Added note
joyfulmantis Jul 24, 2023
d78f2bc
merge upstream/master
joyfulmantis Jul 26, 2023
9b539d9
address michaelpj's suggestions (1/n)
joyfulmantis Jul 27, 2023
d415379
more improvements
joyfulmantis Jul 28, 2023
e618d7e
window build fix attempt
joyfulmantis Jul 28, 2023
6fee73f
Fix stack and windows builds
joyfulmantis Jul 28, 2023
6cbe6d9
Fix code-range test
joyfulmantis Jul 28, 2023
e0bbec1
refactor splice and eval to remove underscore func
joyfulmantis Jul 28, 2023
6102e6b
Fix hls-tactics-plugin test
joyfulmantis Jul 28, 2023
953003a
Broke up the ghcide test file
joyfulmantis Jul 29, 2023
9bbf421
have CommandFunction use ExceptT
joyfulmantis Jul 29, 2023
afcf19d
add tests for exceptions and PluginError order
joyfulmantis Jul 29, 2023
77d264f
fix tactics build
joyfulmantis Jul 29, 2023
1a22bcc
fix tactics try 2
joyfulmantis Jul 29, 2023
28bdecc
fix tactics build try 3
joyfulmantis Jul 29, 2023
1dd12d4
fix for real this time
joyfulmantis Jul 29, 2023
ba4fa79
Fix hlint rules
joyfulmantis Jul 29, 2023
89bba81
hlint rule fixes try 2
joyfulmantis Jul 29, 2023
82e3ff5
Merge remote-tracking branch 'upstream/master' into plugin-logger
joyfulmantis Jul 31, 2023
a215904
address michealpj's suggestions
joyfulmantis Jul 31, 2023
f15ea45
Add haddocks
joyfulmantis Jul 31, 2023
8329b90
Update HLS.hs
joyfulmantis Jul 31, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 4 additions & 4 deletions ghcide/src/Development/IDE/Core/Shake.hs
Original file line number Diff line number Diff line change
Expand Up @@ -964,7 +964,7 @@ useWithStale key file = runIdentity <$> usesWithStale key (Identity file)
-- |Request a Rule result, it not available return the last computed result
-- which may be stale.
--
-- Throws an `BadDependency` IO exception which is caught by the rule system if
-- Throws an `BadDependency` exception which is caught by the rule system if
-- none available.
--
-- WARNING: Not suitable for PluginHandlers. Use `useWithStaleE` instead.
Expand All @@ -974,7 +974,7 @@ useWithStale_ key file = runIdentity <$> usesWithStale_ key (Identity file)

-- |Plural version of 'useWithStale_'
--
-- Throws an `BadDependency` IO exception which is caught by the rule system if
-- Throws an `BadDependency` exception which is caught by the rule system if
-- none available.
--
-- WARNING: Not suitable for PluginHandlers.
Expand Down Expand Up @@ -1053,7 +1053,7 @@ useNoFile key = use key emptyFilePath

-- Requests a rule if available.
--
-- Throws an `BadDependency` IO exception which is caught by the rule system if
-- Throws an `BadDependency` exception which is caught by the rule system if
-- none available.
--
-- WARNING: Not suitable for PluginHandlers. Use `useE` instead.
Expand All @@ -1065,7 +1065,7 @@ useNoFile_ key = use_ key emptyFilePath

-- |Plural version of `use_`
--
-- Throws an `BadDependency` IO exception which is caught by the rule system if
-- Throws an `BadDependency` exception which is caught by the rule system if
-- none available.
--
-- WARNING: Not suitable for PluginHandlers. Use `usesE` instead.
Expand Down
42 changes: 22 additions & 20 deletions ghcide/src/Development/IDE/Plugin/HLS.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
module Development.IDE.Plugin.HLS
(
asGhcIdePlugin
, toResponseError
, Log(..)
) where

Expand Down Expand Up @@ -80,10 +81,17 @@ prettyResponseError err = errorCode <> ":" <+> errorBody
errorCode = pretty $ show $ err ^. L.code
errorBody = pretty $ err ^. L.message

pluginNotEnabled :: SMethod m -> [(PluginId, b, a)] -> Text
pluginNotEnabled :: SMethod m -> [PluginId] -> Text
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this just be inlined into the function below now?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good catch!

pluginNotEnabled method availPlugins =
"No plugin enabled for " <> T.pack (show method) <> ", potentially available: "
<> (T.intercalate ", " $ map (\(PluginId plid, _, _) -> plid) availPlugins)
<> (T.intercalate ", " $ map (\(PluginId plid) -> plid) availPlugins)

noPluginEnabled :: Recorder (WithPriority Log) -> SMethod m -> [PluginId] -> IO (Either ResponseError c)
noPluginEnabled recorder m fs' = do
logWith recorder Warning (LogNoPluginForMethod $ Some m)
let err = ResponseError (InR ErrorCodes_MethodNotFound) msg Nothing
msg = pluginNotEnabled m fs'
return $ Left err

pluginDoesntExist :: PluginId -> Text
pluginDoesntExist (PluginId pid) = "Plugin " <> pid <> " doesn't exist"
Expand Down Expand Up @@ -113,13 +121,6 @@ logAndReturnError recorder p errCode msg = do
logWith recorder Warning $ LogResponseError p err
pure $ Left err

-- | Logs the provider error before returning it to the caller
logAndReturnError' :: Recorder (WithPriority Log) -> (LSPErrorCodes |? ErrorCodes) -> Log -> LSP.LspT Config IO (Either ResponseError a)
logAndReturnError' recorder errCode msg = do
let err = ResponseError errCode (fromString $ show msg) Nothing
logWith recorder Warning $ msg
pure $ Left err

-- | Map a set of plugins to the underlying ghcide engine.
asGhcIdePlugin :: Recorder (WithPriority Log) -> IdePlugins IdeState -> Plugin Config
asGhcIdePlugin recorder (IdePlugins ls) =
Expand Down Expand Up @@ -219,8 +220,15 @@ executeCommandHandlers recorder ecs = requestHandler SMethod_WorkspaceExecuteCom
Just (PluginCommand _ _ f) -> case A.fromJSON arg of
A.Error err -> logAndReturnError recorder p (InR ErrorCodes_InvalidParams) (failedToParseArgs com p err arg)
A.Success a -> do
(first (toResponseError . (p,)) <$> runExceptT (f ide a)) `catchAny` -- See Note [Exception handling in plugins]
(\e -> logAndReturnError' recorder (InR ErrorCodes_InternalError) (ExceptionInPlugin p (Some SMethod_WorkspaceApplyEdit) e))
res <- runExceptT (f ide a) `catchAny` -- See Note [Exception handling in plugins]
(\e -> pure $ Left $ PluginInternalError (exceptionInPlugin p SMethod_WorkspaceExecuteCommand e))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this method? And it was different before??

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because that's it's actually method, see line 171. WorkspaceApplyEdit before was an error, either autocomplete typo, brain fog, or sloppy mistake on my part.

case res of
(Left (PluginRequestRefused _)) ->
liftIO $ noPluginEnabled recorder SMethod_WorkspaceExecuteCommand (fst <$> ecs)
(Left pluginErr) -> do
liftIO $ logErrors recorder [(p, pluginErr)]
pure $ Left $ toResponseError (p, pluginErr)
(Right result) -> pure $ Right result

-- ---------------------------------------------------------------------

Expand All @@ -242,7 +250,7 @@ extensiblePlugins recorder xs = mempty { P.pluginHandlers = handlers }
let fs = filter (\(_, desc, _) -> pluginEnabled m params desc config) fs'
-- Clients generally don't display ResponseErrors so instead we log any that we come across
case nonEmpty fs of
Nothing -> liftIO $ noPluginEnabled m fs'
Nothing -> liftIO $ noPluginEnabled recorder m ((\(x, _, _) -> x) <$> fs')
Just fs -> do
let handlers = fmap (\(plid,_,handler) -> (plid,handler)) fs
es <- runConcurrently exceptionInPlugin m handlers ide params
Expand All @@ -255,16 +263,11 @@ extensiblePlugins recorder xs = mempty { P.pluginHandlers = handlers }
noRefused (_, _) = True
filteredErrs = filter noRefused errs
case nonEmpty filteredErrs of
Nothing -> liftIO $ noPluginEnabled m fs'
Nothing -> liftIO $ noPluginEnabled recorder m ((\(x, _, _) -> x) <$> fs')
Just xs -> pure $ Left $ combineErrors xs
Just xs -> do
pure $ Right $ combineResponses m config caps params xs
noPluginEnabled :: SMethod m -> [(PluginId, b, a)] -> IO (Either ResponseError c)
noPluginEnabled m fs' = do
logWith recorder Warning (LogNoPluginForMethod $ Some m)
let err = ResponseError (InR ErrorCodes_MethodNotFound) msg Nothing
msg = pluginNotEnabled m fs'
return $ Left err


-- ---------------------------------------------------------------------

Expand Down Expand Up @@ -313,7 +316,6 @@ combineErrors :: NonEmpty (PluginId, PluginError) -> ResponseError
combineErrors (x NE.:| []) = toResponseError x
combineErrors xs = toResponseError $ NE.last $ NE.sortWith (toPriority . snd) xs


toResponseError :: (PluginId, PluginError) -> ResponseError
toResponseError (PluginId plId, err) =
ResponseError (toErrorCode err) (plId <> ": " <> tPretty err) Nothing
Expand Down
125 changes: 34 additions & 91 deletions ghcide/test/exe/ExceptionTests.hs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@

module ExceptionTests (tests) where

import Control.Concurrent.Async
import Control.Exception (ArithException (DivideByZero),
finally, throwIO)
throwIO)
import Control.Lens
import Control.Monad.Error.Class (MonadError (throwError))
import Control.Monad.IO.Class (liftIO)
Expand All @@ -12,6 +11,7 @@ import Data.Text as T
import Development.IDE.Core.Shake (IdeState (..))
import qualified Development.IDE.LSP.Notifications as Notifications
import qualified Development.IDE.Main as IDE
import Development.IDE.Plugin.HLS (toResponseError)
import Development.IDE.Plugin.Test as Test
import Development.IDE.Types.Options
import GHC.Base (coerce)
Expand All @@ -30,8 +30,6 @@ import Language.LSP.Protocol.Types hiding
mkRange)
import Language.LSP.Test
import LogType (Log (..))
import System.Directory
import System.Process.Extra (createPipe)
import Test.Tasty
import Test.Tasty.HUnit
import TestUtils
Expand All @@ -50,7 +48,6 @@ tests recorder logger = do
pure (InL [])
]
}]

testIde recorder (testingLite recorder logger plugins) $ do
doc <- createDoc "A.hs" "haskell" "module A where"
waitForProgressDone
Expand All @@ -60,6 +57,7 @@ tests recorder logger = do
liftIO $ assertBool "We caught an error, but it wasn't ours!"
(T.isInfixOf "divide by zero" _message && T.isInfixOf (coerce pluginId) _message)
_ -> liftIO $ assertFailure $ show lens

, testCase "Commands" $ do
let pluginId = "command-exception"
commandId = CommandId "exception"
Expand All @@ -71,7 +69,6 @@ tests recorder logger = do
pure (InR Null)
]
}]

testIde recorder (testingLite recorder logger plugins) $ do
_ <- createDoc "A.hs" "haskell" "module A where"
waitForProgressDone
Expand All @@ -83,6 +80,7 @@ tests recorder logger = do
liftIO $ assertBool "We caught an error, but it wasn't ours!"
(T.isInfixOf "divide by zero" _message && T.isInfixOf (coerce pluginId) _message)
_ -> liftIO $ assertFailure $ show res

, testCase "Notification Handlers" $ do
let pluginId = "notification-exception"
plugins = pluginDescToIdePlugins $
Expand All @@ -95,101 +93,24 @@ tests recorder logger = do
[ mkPluginHandler SMethod_TextDocumentCodeLens $ \_ _ _-> do
pure (InL [])
]
}
, Notifications.descriptor (cmapWithPrio LogNotifications recorder) "ghcide-core"]

}]
testIde recorder (testingLite recorder logger plugins) $ do
doc <- createDoc "A.hs" "haskell" "module A where"
waitForProgressDone
(view L.result -> lens) <- request SMethod_TextDocumentCodeLens (CodeLensParams Nothing Nothing doc)
case lens of
Right (InL []) ->
-- We don't get error responses from notification handlers, so
-- we can only make sure that the server is still responding
pure ()
_ -> liftIO $ assertFailure $ "We should have had an empty list" <> show lens]

, testGroup "Testing PluginError order..."
[ testCase "InternalError over InvalidParams" $ do
let pluginId = "internal-error-order"
plugins = pluginDescToIdePlugins $
[ (defaultPluginDescriptor pluginId)
{ pluginHandlers = mconcat
[ mkPluginHandler SMethod_TextDocumentCodeLens $ \_ _ _-> do
throwError $ PluginInternalError "error test"
,mkPluginHandler SMethod_TextDocumentCodeLens $ \_ _ _-> do
throwError $ PluginInvalidParams "error test"
]
}
, Notifications.descriptor (cmapWithPrio LogNotifications recorder) "ghcide-core"]

testIde recorder (testingLite recorder logger plugins) $ do
doc <- createDoc "A.hs" "haskell" "module A where"
waitForProgressDone
(view L.result -> lens) <- request SMethod_TextDocumentCodeLens (CodeLensParams Nothing Nothing doc)
case lens of
Left (ResponseError {_code = InR ErrorCodes_InternalError, _message}) ->
liftIO $ assertBool "We caught an error, but it wasn't ours!"
(T.isInfixOf "error test" _message && T.isInfixOf (coerce pluginId) _message)
_ -> liftIO $ assertFailure $ show lens
, testCase "InvalidParams over InvalidUserState" $ do
let pluginId = "invalid-params-order"
plugins = pluginDescToIdePlugins $
[ (defaultPluginDescriptor pluginId)
{ pluginHandlers = mconcat
[ mkPluginHandler SMethod_TextDocumentCodeLens $ \_ _ _-> do
throwError $ PluginInvalidParams "error test"
,mkPluginHandler SMethod_TextDocumentCodeLens $ \_ _ _-> do
throwError $ PluginInvalidUserState "error test"
]
}
, Notifications.descriptor (cmapWithPrio LogNotifications recorder) "ghcide-core"]

testIde recorder (testingLite recorder logger plugins) $ do
doc <- createDoc "A.hs" "haskell" "module A where"
waitForProgressDone
(view L.result -> lens) <- request SMethod_TextDocumentCodeLens (CodeLensParams Nothing Nothing doc)
case lens of
Left (ResponseError {_code = InR ErrorCodes_InvalidParams, _message}) ->
liftIO $ assertBool "We caught an error, but it wasn't ours!"
(T.isInfixOf "error test" _message && T.isInfixOf (coerce pluginId) _message)
_ -> liftIO $ assertFailure $ show lens
, testCase "InvalidUserState over RequestRefused" $ do
let pluginId = "invalid-user-state-order"
plugins = pluginDescToIdePlugins $
[ (defaultPluginDescriptor pluginId)
{ pluginHandlers = mconcat
[ mkPluginHandler SMethod_TextDocumentCodeLens $ \_ _ _-> do
throwError $ PluginInvalidUserState "error test"
,mkPluginHandler SMethod_TextDocumentCodeLens $ \_ _ _-> do
throwError $ PluginRequestRefused "error test"
]
}
, Notifications.descriptor (cmapWithPrio LogNotifications recorder) "ghcide-core"]

testIde recorder (testingLite recorder logger plugins) $ do
doc <- createDoc "A.hs" "haskell" "module A where"
waitForProgressDone
(view L.result -> lens) <- request SMethod_TextDocumentCodeLens (CodeLensParams Nothing Nothing doc)
case lens of
Left (ResponseError {_code = InL LSPErrorCodes_RequestFailed, _message}) ->
liftIO $ assertBool "We caught an error, but it wasn't ours!"
(T.isInfixOf "error test" _message && T.isInfixOf (coerce pluginId) _message)
_ -> liftIO $ assertFailure $ show lens
]]

testIde :: Recorder (WithPriority Log) -> IDE.Arguments -> Session () -> IO ()
testIde recorder arguments session = do
config <- getConfigFromEnv
cwd <- getCurrentDirectory
(hInRead, hInWrite) <- createPipe
(hOutRead, hOutWrite) <- createPipe
let projDir = "."
let server = IDE.defaultMain (cmapWithPrio LogIDEMain recorder) arguments
{ IDE.argsHandleIn = pure hInRead
, IDE.argsHandleOut = pure hOutWrite
}

flip finally (setCurrentDirectory cwd) $ withAsync server $ \_ ->
runSessionWithHandles hInWrite hOutRead config lspTestCaps projDir session
[ pluginOrderTestCase recorder logger "InternalError over InvalidParams" PluginInternalError PluginInvalidParams
, pluginOrderTestCase recorder logger "InvalidParams over InvalidUserState" PluginInvalidParams PluginInvalidUserState
, pluginOrderTestCase recorder logger "InvalidUserState over RequestRefused" PluginInvalidUserState PluginRequestRefused
]
]

testingLite :: Recorder (WithPriority Log) -> Logger -> IdePlugins IdeState -> IDE.Arguments
testingLite recorder logger plugins =
Expand All @@ -210,3 +131,25 @@ testingLite recorder logger plugins =
{ IDE.argsHlsPlugins = hlsPlugins
, IDE.argsIdeOptions = ideOptions
}

pluginOrderTestCase :: Recorder (WithPriority Log) -> Logger -> TestName -> (T.Text -> PluginError) -> (T.Text -> PluginError) -> TestTree
pluginOrderTestCase recorder logger msg err1 err2 =
testCase msg $ do
let pluginId = "error-order-test"
plugins = pluginDescToIdePlugins $
[ (defaultPluginDescriptor pluginId)
{ pluginHandlers = mconcat
[ mkPluginHandler SMethod_TextDocumentCodeLens $ \_ _ _-> do
throwError $ err1 "error test"
,mkPluginHandler SMethod_TextDocumentCodeLens $ \_ _ _-> do
throwError $ err2 "error test"
]
}]
testIde recorder (testingLite recorder logger plugins) $ do
doc <- createDoc "A.hs" "haskell" "module A where"
waitForProgressDone
(view L.result -> lens) <- request SMethod_TextDocumentCodeLens (CodeLensParams Nothing Nothing doc)
case lens of
Left re | toResponseError (pluginId, err1 "error test") == re -> pure ()
| otherwise -> liftIO $ assertFailure "We caught an error, but it wasn't ours!"
_ -> liftIO $ assertFailure $ show lens