-
-
Notifications
You must be signed in to change notification settings - Fork 391
Draft: Introduce hierarchical error types and error handling for plugins #3659
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
Conversation
Yeah, sorry I only saw the DRAFT marking after commenting 😅
|
7aad4b6
to
1e077da
Compare
526fc9a
to
565e016
Compare
565e016
to
8adb03e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the direction. At a high level my suggestions are:
- Stuff inside
Action
already throws a bunch. Let's just use exceptions there, and have "strict" versions of theuse
functions that can throw if the thing isn't there. - Using
ExceptT
for the "main line" of plugin code seems nice. It would be nice if we could make the error type there fully generic so plugins can provide their own. I'm not sure how to do that because of the normalized file path stuff. The only suggestion I have is that we could also use exceptions there, and provide a throwing variant that runs inMonadIO
. It's not like anyone wants to handle the failures there anyway!
@@ -964,11 +964,15 @@ useWithStale key file = runIdentity <$> usesWithStale key (Identity file) | |||
|
|||
-- | Request a Rule result, it not available return the last computed result which may be stale. | |||
-- Errors out if none available. | |||
-- | |||
-- The thrown error is a 'BadDependency' error which is caught by the rule system. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe link to the definition of Action
, which also explains that it's okay to throw inside Action
s.
) | ||
nfp <- PluginUtils.withPluginError $ getNormalizedFilePath uri | ||
env <- hscEnv . fst <$> | ||
PluginUtils.runAction "codeLens.GhcSession" ideState |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason not to merge these all into one Action
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not want to impose Plugin changes for no reason, to keep it easier to review.
Also, I think some people prefer this style, because otherwise you have to return a big tuple and pattern match on it. I don't think there is any performance impact.
|
||
type PluginHandler e m a = ExceptT e m a | ||
|
||
pluginResponse :: Monad m => ExceptT PluginError m a -> m (Either ResponseError a) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about
handlePluginError :: Pretty e => e -> ResponseError
handlePluginError msg = ResponseError Internalerror (renderPrettyWhatever msg) Nothing
pluginResponse :: Monad m => ExceptT e m a -> m (Either ResponseError a)
pluginResponse'
is more generic still but I'm not sure if people really need that power?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, we can't make it completely generic as it is now because of the attempt to fold the getNormalizedFilePath
functions in...
fmap (first handlePluginError) | ||
. runExceptT | ||
|
||
pluginResponse' :: Monad m => (e -> ResponseError) -> ExceptT e m a -> m (Either ResponseError a) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is nearly pluginResponse . withExceptT handleError
, right? Maybe we should just let people do this inline, withExceptT
is not so exotic...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally, I think if we can nudge people towards just using standard Control.Monad.Except
stuff that seems good
| PluginUriToNormalizedFilePath J.Uri | ||
| PluginErrorMessage T.Text | ||
|
||
prettyPluginError :: PluginError -> T.Text |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instance Pretty PluginError
?
-- | useE is useful to implement functions that aren’t rules but need shortcircuiting | ||
-- e.g. getDefinition. | ||
useWithStaleFast :: IdeRule k v => k -> NormalizedFilePath -> ExceptT GhcidePluginError IdeAction (v, PositionMapping) | ||
useWithStaleFast k = maybeToExceptT (RuleFailed k) . MaybeT . Shake.useWithStaleFast k |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not totally sure why this gets a different error than useWithStale
🤔 More generally I think it would be good for the errors to match the function names. We can rename the functions... but let's keep the two consistent.
join $ shakeEnqueue (shakeExtras ide) (mkDelayedAction herald Logger.Debug $ runExceptT act) | ||
|
||
-- | Request a Rule result, it not available return the last computed result which may be stale. | ||
-- Errors out if none available. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can I request that we not use "errors out", it's very ambiguous when we have both ExceptT
and exceptions in the picture. Maybe just "throws an exception"?
fmap (first (\msg -> ResponseError InternalError (fromString msg) Nothing)) | ||
. runExceptT | ||
withError :: Functor m => (e' -> e) -> ExceptT e' m a -> ExceptT e m a | ||
withError = withExceptT |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, I think I'm broadly in favour of getting people to use "normal" ExceptT
functions as much as possible.
=> k -> NormalizedFilePath -> ExceptT e Action (v, PositionMapping) | ||
useWithStale_ key file = ExceptT $ fmap Right $ Shake.useWithStale_ key file | ||
|
||
useWithStale :: IdeRule k v |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, so serious question: since we're in Action
here, and it's safe to throw exceptions, is there any reason not to throw these errors as exceptions too?
That would give us an (I think) sensible heuristic:
- Functions that run in
Action
throw exceptions - Functions that run outside
Action
useExceptT
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That also suggests that the functions here could just become "stricter" versions of useWithStale
and friends, which throw RuleNotReady
instead of RuleFailed
or whatever. Then this module mostly goes away and we instead add a few functions to Shake.hs
getFirstPragma (PluginId pId) state nfp = handleMaybeM "Could not get NextPragmaInfo" $ do | ||
ghcSession <- liftIO $ runAction (T.unpack pId <> ".GhcSession") state $ useWithStale GhcSession nfp | ||
(_, fileContents) <- liftIO $ runAction (T.unpack pId <> ".GetFileContents") state $ getFileContents nfp | ||
getFirstPragma :: MonadIO m => PluginId -> IdeState -> NormalizedFilePath -> ExceptT GhcidePluginError m NextPragmaInfo |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we did what I suggested, this would "just" be in MonadIO
. I think that's okay: if it's in MonadIO
it can throw!
@@ -263,27 +277,64 @@ allLspCmdIds pid commands = concatMap go commands | |||
|
|||
-- --------------------------------------------------------------------- | |||
|
|||
getNormalizedFilePath :: Monad m => Uri -> ExceptT String m NormalizedFilePath | |||
getNormalizedFilePath uri = handleMaybe errMsg | |||
getNormalizedFilePath :: Monad m => Uri -> ExceptT PluginError m NormalizedFilePath |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So if we did what I'm suggesting, we'd also provide
getNormalizedFilePathIO :: MonadIO m => Uri -> m NormalizedFilePath
I realise that what I'm proposing is actually more radical than I maybe thought. It pretty much amounts to:
Perhaps this is a bit heretical, I'm basically proposing that we embrace exceptions in plugins. But maybe it would be fine? |
I like the idea of embracing exceptions, catching them and providing sensible log messages. |
It would be nice if there was a PluginError type that wrapped ResponseErrors. The reason is that resolve methods will need to throw a ContentModified ResponseError if they are unable to resolve the provided request, and it would be nice to be able to use the available ExceptT pluginResponse infrastructure while at the same time being able to throw specific response errors. |
If you end up going ahead with this, I think it might be a good idea to first merge the general infrastructure as a parallel system to what we have now. Then anyone interested in migrating the plugins, can do that piece by piece. Finally once everything is migrated, the old stuff can be removed. |
Closing this as #3717 was merged |
Essentially, infuse the existing
pluginResponse
infrastructure with steroids: