Skip to content

Commit e9ccff2

Browse files
xsebekJuly541
authored andcommitted
Eval plugin: mark exceptions (haskell#2775)
1 parent b02ce31 commit e9ccff2

File tree

10 files changed

+129
-32
lines changed

10 files changed

+129
-32
lines changed

docs/configuration.md

+3
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ Plugins have a generic config to control their behaviour. The schema of such con
6363
- `haskell.plugin.tactics.config.hole_severity`, default empty: The severity to use when showing hole diagnostics. These are noisy, but some editors don't allow jumping to all severities. One of `error`, `warning`, `info`, `hint`, `none`.
6464
- `haskell.plugin.tactics.config.max_use_ctor_actions`, default 5: Maximum number of `Use constructor <x>` code actions that can appear.
6565
- `haskell.plugin.tactics.config.proofstate_styling`, default true: Should Wingman emit styling markup when showing metaprogram proof states?
66+
- `eval`:
67+
- `haskell.plugin.eval.config.diff`, default true: When reloading haddock test results in changes, mark it with WAS/NOW.
68+
- `haskell.plugin.eval.config.exception`, default false: When the command results in an exception, mark it with `*** Exception:`.
6669
- `ghcide-completions`:
6770
- `haskell.plugin.ghcide-completions.config.snippetsOn`, default true: Inserts snippets when using code completions.
6871
- `haskell.plugin.ghcide-completions.config.autoExtendOn`, default true: Extends the import list automatically when completing a out-of-scope identifier.

plugins/hls-eval-plugin/README.md

+25
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,11 @@ On the contrary, if the test were into a plain comment, the result would simply
242242
-}
243243
```
244244

245+
If you find this WAS/NOW behaviour does not fit your needs, you can turn it off with toggling the configuration option:
246+
```json
247+
"haskell.plugin.eval.config.diff": false
248+
```
249+
245250
# Multiline Output
246251

247252
By default, the output of every expression is returned as a single line.
@@ -274,6 +279,8 @@ To display it properly, we can exploit the fact that the output of an error is d
274279
]
275280
```
276281

282+
This assumes you did not turn on exception marking (see [Marking exceptions](#marking-exceptions) below).
283+
277284
# Differences with doctest
278285

279286
Though the Eval plugin functionality is quite similar to that of [doctest](https://hackage.haskell.org/package/doctest), some doctest's features are not supported.
@@ -287,6 +294,24 @@ Only the value of an IO expression is spliced in, not its output:
287294
()
288295
```
289296

297+
### Marking exceptions
298+
299+
When an exception is thrown it is not prefixed:
300+
301+
```
302+
>>> 1 `div` 0
303+
divide by zero
304+
```
305+
306+
If you want to get the doctest/GHCi behaviour, you can toggle the configuration option:
307+
```json
308+
"haskell.plugin.eval.config.exception": true
309+
```
310+
```
311+
>>> 1 `div` 0
312+
*** Exception: divide by zero
313+
```
314+
290315
### Pattern Matching
291316

292317
The arbitrary content matcher __...__ is unsupported.

plugins/hls-eval-plugin/src/Ide/Plugin/Eval/CodeLens.hs

+30-17
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import Control.Applicative (Alternative ((<|>)))
2727
import Control.Arrow (second, (>>>))
2828
import Control.Exception (try)
2929
import qualified Control.Exception as E
30-
import Control.Lens (_1, _3, (%~), (<&>), (^.))
30+
import Control.Lens (_1, _3, ix, (%~), (<&>), (^.))
3131
import Control.Monad (guard, join, void, when)
3232
import Control.Monad.IO.Class (MonadIO (liftIO))
3333
import Control.Monad.Trans (lift)
@@ -90,7 +90,7 @@ import Ide.Plugin.Eval.Code (Statement, asStatements,
9090
evalSetup, myExecStmt,
9191
propSetup, resultRange,
9292
testCheck, testRanges)
93-
import Ide.Plugin.Eval.Config (getDiffProperty)
93+
import Ide.Plugin.Eval.Config (getEvalConfig, EvalConfig(..))
9494
import Ide.Plugin.Eval.GHC (addImport, addPackages,
9595
hasPackage, showDynFlags)
9696
import Ide.Plugin.Eval.Parse.Comments (commentsToSections)
@@ -292,13 +292,13 @@ runEvalCmd plId st EvalParams{..} =
292292
-- Evaluation takes place 'inside' the module
293293
setContext [Compat.IIModule modName]
294294
Right <$> getSession
295-
diff <- lift $ getDiffProperty plId
295+
evalCfg <- lift $ getEvalConfig plId
296296
edits <-
297297
perf "edits" $
298298
liftIO $
299299
evalGhcEnv hscEnv' $
300300
runTests
301-
diff
301+
evalCfg
302302
(st, fp)
303303
tests
304304

@@ -340,11 +340,11 @@ testsBySection sections =
340340

341341
type TEnv = (IdeState, String)
342342

343-
runTests :: Bool -> TEnv -> [(Section, Test)] -> Ghc [TextEdit]
344-
runTests diff e@(_st, _) tests = do
343+
runTests :: EvalConfig -> TEnv -> [(Section, Test)] -> Ghc [TextEdit]
344+
runTests EvalConfig{..} e@(_st, _) tests = do
345345
df <- getInteractiveDynFlags
346346
evalSetup
347-
when (hasQuickCheck df && needsQuickCheck tests) $ void $ evals e df propSetup
347+
when (hasQuickCheck df && needsQuickCheck tests) $ void $ evals True e df propSetup
348348

349349
mapM (processTest e df) tests
350350
where
@@ -356,7 +356,7 @@ runTests diff e@(_st, _) tests = do
356356
rs <- runTest e df test
357357
dbg "TEST RESULTS" rs
358358

359-
let checkedResult = testCheck diff (section, test) rs
359+
let checkedResult = testCheck eval_cfg_diff (section, test) rs
360360

361361
let edit = asEdit (sectionFormat section) test (map pad checkedResult)
362362
dbg "TEST EDIT" edit
@@ -368,7 +368,7 @@ runTests diff e@(_st, _) tests = do
368368
return $
369369
singleLine
370370
"Add QuickCheck to your cabal dependencies to run this test."
371-
runTest e df test = evals e df (asStatements test)
371+
runTest e df test = evals (eval_cfg_exception && not (isProperty test)) e df (asStatements test)
372372

373373
asEdit :: Format -> Test -> [Text] -> TextEdit
374374
asEdit (MultiLine commRange) test resultLines
@@ -419,15 +419,19 @@ Nothing is returned for an empty line:
419419
A, possibly multi line, error is returned for a wrong declaration, directive or value or an exception thrown by the evaluated code:
420420
421421
>>>:set -XNonExistent
422-
Unknown extension: "NonExistent"
422+
Some flags have not been recognized: -XNonExistent
423423
424424
>>> cls C
425-
Variable not in scope: cls :: t0 -> ()
425+
Variable not in scope: cls :: t0 -> t
426426
Data constructor not in scope: C
427427
428428
>>> "A
429429
lexical error in string/character literal at end of input
430430
431+
Exceptions are shown as if printed, but it can be configured to include prefix like
432+
in GHCi or doctest. This allows it to be used as a hack to simulate print until we
433+
get proper IO support. See #1977
434+
431435
>>> 3 `div` 0
432436
divide by zero
433437
@@ -438,10 +442,10 @@ bad times
438442
Or for a value that does not have a Show instance and can therefore not be displayed:
439443
>>> data V = V
440444
>>> V
441-
No instance for (Show V)
445+
No instance for (Show V) arising from a use of ‘evalPrint’
442446
-}
443-
evals :: TEnv -> DynFlags -> [Statement] -> Ghc [Text]
444-
evals (st, fp) df stmts = do
447+
evals :: Bool -> TEnv -> DynFlags -> [Statement] -> Ghc [Text]
448+
evals mark_exception (st, fp) df stmts = do
445449
er <- gStrictTry $ mapM eval stmts
446450
return $ case er of
447451
Left err -> errorLines err
@@ -488,9 +492,9 @@ evals (st, fp) df stmts = do
488492
do
489493
dbg "{STMT " stmt
490494
res <- exec stmt l
491-
r <- case res of
492-
Left err -> return . Just . errorLines $ err
493-
Right x -> return $ singleLine <$> x
495+
let r = case res of
496+
Left err -> Just . (if mark_exception then exceptionLines else errorLines) $ err
497+
Right x -> singleLine <$> x
494498
dbg "STMT} -> " r
495499
return r
496500
| -- An import
@@ -556,6 +560,15 @@ errorLines =
556560
. T.lines
557561
. T.pack
558562

563+
{- |
564+
Convert exception messages to a list of text lines
565+
Remove unnecessary information and mark it as exception.
566+
We use '*** Exception:' to make it identical to doctest
567+
output, see #2353.
568+
-}
569+
exceptionLines :: String -> [Text]
570+
exceptionLines = (ix 0 %~ ("*** Exception: " <>)) . errorLines
571+
559572
{- |
560573
>>> map (pad_ (T.pack "--")) (map T.pack ["2+2",""])
561574
["--2+2","--<BLANKLINE>"]

plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Config.hs

+20-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
{-# LANGUAGE OverloadedStrings #-}
44
module Ide.Plugin.Eval.Config
55
( properties
6-
, getDiffProperty
6+
, getEvalConfig
7+
, EvalConfig(..)
78
) where
89

910
import Ide.Plugin.Config (Config)
@@ -12,10 +13,25 @@ import Ide.PluginUtils (usePropertyLsp)
1213
import Ide.Types (PluginId)
1314
import Language.LSP.Server (MonadLsp)
1415

15-
properties :: Properties '[ 'PropertyKey "diff" 'TBoolean]
16+
-- | The Eval plugin configuration. (see 'properties')
17+
data EvalConfig = EvalConfig
18+
{ eval_cfg_diff :: Bool
19+
, eval_cfg_exception :: Bool
20+
}
21+
deriving (Eq, Ord, Show)
22+
23+
properties :: Properties
24+
'[ 'PropertyKey "exception" 'TBoolean
25+
, 'PropertyKey "diff" 'TBoolean
26+
]
1627
properties = emptyProperties
1728
& defineBooleanProperty #diff
1829
"Enable the diff output (WAS/NOW) of eval lenses" True
30+
& defineBooleanProperty #exception
31+
"Enable marking exceptions with `*** Exception:` similarly to doctest and GHCi." False
1932

20-
getDiffProperty :: (MonadLsp Config m) => PluginId -> m Bool
21-
getDiffProperty plId = usePropertyLsp #diff plId properties
33+
getEvalConfig :: (MonadLsp Config m) => PluginId -> m EvalConfig
34+
getEvalConfig plId =
35+
EvalConfig
36+
<$> usePropertyLsp #diff plId properties
37+
<*> usePropertyLsp #exception plId properties

plugins/hls-eval-plugin/test/Main.hs

+24-11
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import Control.Lens (_Just, folded, preview, toListOf,
1212
view, (^..))
1313
import Data.Aeson (Value (Object), fromJSON, object,
1414
toJSON, (.=))
15-
import Data.Aeson.Types (Result (Success))
15+
import Data.Aeson.Types (Result (Success), Pair)
1616
import Data.List (isInfixOf)
1717
import Data.List.Extra (nubOrdOn)
1818
import qualified Data.Map as Map
@@ -76,7 +76,7 @@ tests =
7676
| ghcVersion == GHC90 -> "-- No instance for (Num String) arising from a use of ‘+’"
7777
| otherwise -> "-- No instance for (Num [Char]) arising from a use of ‘+’"
7878
evalInFile "T8.hs" "-- >>> \"" "-- lexical error in string/character literal at end of input"
79-
evalInFile "T8.hs" "-- >>> 3 `div` 0" "-- divide by zero"
79+
evalInFile "T8.hs" "-- >>> 3 `div` 0" "-- divide by zero" -- The default for marking exceptions is False
8080
, goldenWithEval "Applies file LANGUAGE extensions" "T9" "hs"
8181
, goldenWithEval' "Evaluate a type with :kind!" "T10" "hs" (if ghcVersion == GHC92 then "ghc92.expected" else "expected")
8282
, goldenWithEval' "Reports an error for an incorrect type with :kind!" "T11" "hs" (if ghcVersion == GHC92 then "ghc92.expected" else "expected")
@@ -133,6 +133,7 @@ tests =
133133
, goldenWithEval "The default language extensions for the eval plugin are the same as those for ghci" "TSameDefaultLanguageExtensionsAsGhci" "hs"
134134
, goldenWithEval "IO expressions are supported, stdout/stderr output is ignored" "TIO" "hs"
135135
, goldenWithEval "Property checking" "TProperty" "hs"
136+
, goldenWithEval "Property checking with exception" "TPropertyError" "hs"
136137
, goldenWithEval "Prelude has no special treatment, it is imported as stated in the module" "TPrelude" "hs"
137138
, goldenWithEval "Don't panic on {-# UNPACK #-} pragma" "TUNPACK" "hs"
138139
, goldenWithEval "Can handle eval inside nested comment properly" "TNested" "hs"
@@ -148,12 +149,12 @@ tests =
148149
]
149150
, goldenWithEval "Works with NoImplicitPrelude" "TNoImplicitPrelude" "hs"
150151
, goldenWithEval "Variable 'it' works" "TIt" "hs"
151-
152-
, goldenWithHaskellDoc evalPlugin "Give 'WAS' by default" testDataDir "TDiff" "expected.default" "hs" executeLensesBackwards
153-
, goldenWithHaskellDoc evalPlugin "Give the result only if diff is off" testDataDir "TDiff" "expected.no-diff" "hs" $ \doc -> do
154-
sendConfigurationChanged (toJSON diffOffConfig)
155-
executeLensesBackwards doc
156-
152+
, testGroup "configuration"
153+
[ goldenWithEval' "Give 'WAS' by default" "TDiff" "hs" "expected.default"
154+
, goldenWithEvalConfig' "Give the result only if diff is off" "TDiff" "hs" "expected.no-diff" diffOffConfig
155+
, goldenWithEvalConfig' "Evaluates to exception (not marked)" "TException" "hs" "expected.nomark" (exceptionConfig False)
156+
, goldenWithEvalConfig' "Evaluates to exception (with mark)" "TException" "hs" "expected.marked" (exceptionConfig True)
157+
]
157158
, testGroup ":info command"
158159
[ testCase ":info reports type, constructors and instances" $ do
159160
[output] <- map (unlines . codeLensTestOutput) <$> evalLenses "TInfo.hs"
@@ -263,16 +264,28 @@ codeLensTestOutput codeLens = do
263264
testDataDir :: FilePath
264265
testDataDir = "test" </> "testdata"
265266

266-
diffOffConfig :: Config
267-
diffOffConfig =
267+
changeConfig :: [Pair] -> Config
268+
changeConfig conf =
268269
def
269270
{ Plugin.plugins = Map.fromList [("eval",
270-
def { Plugin.plcGlobalOn = True, Plugin.plcConfig = unObject $ object ["diff" .= False] }
271+
def { Plugin.plcGlobalOn = True, Plugin.plcConfig = unObject $ object conf }
271272
)] }
272273
where
273274
unObject (Object obj) = obj
274275
unObject _ = undefined
275276

277+
diffOffConfig :: Config
278+
diffOffConfig = changeConfig ["diff" .= False]
279+
280+
exceptionConfig :: Bool -> Config
281+
exceptionConfig exCfg = changeConfig ["exception" .= exCfg]
282+
283+
goldenWithEvalConfig' :: TestName -> FilePath -> FilePath -> FilePath -> Config -> TestTree
284+
goldenWithEvalConfig' title path ext expected cfg =
285+
goldenWithHaskellDoc evalPlugin title testDataDir path expected ext $ \doc -> do
286+
sendConfigurationChanged (toJSON cfg)
287+
executeLensesBackwards doc
288+
276289
evalInFile :: HasCallStack => FilePath -> T.Text -> T.Text -> IO ()
277290
evalInFile fp e expected = runSessionWithServer evalPlugin testDataDir $ do
278291
doc <- openDoc fp "haskell"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module TException where
2+
3+
-- >>> exceptionalCode
4+
-- *** Exception: I am exceptional!
5+
exceptionalCode :: Int
6+
exceptionalCode = error "I am exceptional!"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module TException where
2+
3+
-- >>> exceptionalCode
4+
-- I am exceptional!
5+
exceptionalCode :: Int
6+
exceptionalCode = error "I am exceptional!"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module TException where
2+
3+
-- >>> exceptionalCode
4+
exceptionalCode :: Int
5+
exceptionalCode = error "I am exceptional!"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- Support for property checking
2+
module TProperty where
3+
4+
-- prop> \(l::[Bool]) -> head l
5+
-- *** Failed! Exception: 'Prelude.head: empty list' (after 1 test):
6+
-- []
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- Support for property checking
2+
module TProperty where
3+
4+
-- prop> \(l::[Bool]) -> head l

0 commit comments

Comments
 (0)