Skip to content

Commit 91990de

Browse files
authored
Eval Plugin: Proper handling of flags in :set (#1343)
* More precise flag parsing * Avoid collision between ghc 8.6 and above * Error-message compatibility
1 parent 11d0d51 commit 91990de

File tree

10 files changed

+208
-96
lines changed

10 files changed

+208
-96
lines changed

haskell-language-server.cabal

+4-1
Original file line numberDiff line numberDiff line change
@@ -426,9 +426,11 @@ test-suite func-test
426426
, tasty-ant-xml >=1.1.6
427427
, tasty-golden
428428
, tasty-rerun
429+
, megaparsec
430+
, deepseq
429431
, ghcide
430432

431-
hs-source-dirs: test/functional plugins/hls-tactics-plugin/src plugins/hls-eval-plugin/test plugins/hls-splice-plugin/src
433+
hs-source-dirs: test/functional plugins/hls-tactics-plugin/src plugins/hls-eval-plugin/test plugins/hls-splice-plugin/src plugins/hls-eval-plugin/src
432434

433435
main-is: Main.hs
434436
other-modules:
@@ -457,6 +459,7 @@ test-suite func-test
457459
HaddockComments
458460
Ide.Plugin.Splice.Types
459461
Ide.Plugin.Tactic.TestTypes
462+
Ide.Plugin.Eval.Types
460463

461464
ghc-options:
462465
-Wall -Wno-name-shadowing -threaded -rtsopts -with-rtsopts=-N

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

+57-33
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
{-# LANGUAGE TypeApplications #-}
2+
{-# LANGUAGE ViewPatterns #-}
13
{-# LANGUAGE LambdaCase #-}
24
{-# LANGUAGE DeriveAnyClass #-}
35
{-# LANGUAGE DeriveGeneric #-}
@@ -24,11 +26,11 @@ module Ide.Plugin.Eval.CodeLens (
2426
) where
2527

2628
import Control.Applicative (Alternative ((<|>)))
27-
import Control.Arrow (second)
29+
import Control.Arrow (second, (>>>))
2830
import qualified Control.Exception as E
2931
import Control.Monad
3032
( void,
31-
when,
33+
when, guard
3234
)
3335
import Control.Monad.IO.Class (MonadIO (liftIO))
3436
import Control.Monad.Trans.Except
@@ -44,7 +46,7 @@ import Data.Either (isRight)
4446
import qualified Data.HashMap.Strict as HashMap
4547
import Data.List
4648
(dropWhileEnd,
47-
find
49+
find, intercalate
4850
)
4951
import qualified Data.Map.Strict as Map
5052
import Data.Maybe
@@ -75,9 +77,9 @@ import Development.IDE
7577
toNormalizedUri,
7678
uriToFilePath',
7779
useWithStale_,
78-
use_,
80+
use_, prettyPrint
7981
)
80-
import Development.IDE.GHC.Compat (AnnotationComment(AnnBlockComment, AnnLineComment), GenLocated (L), HscEnv, ParsedModule (..), SrcSpan (RealSrcSpan), srcSpanFile)
82+
import Development.IDE.GHC.Compat (AnnotationComment(AnnBlockComment, AnnLineComment), GenLocated (L), HscEnv, ParsedModule (..), SrcSpan (RealSrcSpan, UnhelpfulSpan), srcSpanFile, GhcException, setInteractiveDynFlags)
8183
import DynamicLoading (initializePlugins)
8284
import FastString (unpackFS)
8385
import GHC
@@ -125,7 +127,7 @@ import GhcPlugins
125127
updateWays,
126128
wayGeneralFlags,
127129
wayUnsetGeneralFlags,
128-
xopt_set,
130+
xopt_set, parseDynamicFlagsCmdLine
129131
)
130132
import HscTypes
131133
( InteractiveImport (IIModule),
@@ -153,7 +155,7 @@ import Ide.Plugin.Eval.GHC
153155
showDynFlags,
154156
)
155157
import Ide.Plugin.Eval.Parse.Comments (commentsToSections)
156-
import Ide.Plugin.Eval.Parse.Option (langOptions)
158+
import Ide.Plugin.Eval.Parse.Option (langOptions, parseSetFlags)
157159
import Ide.Plugin.Eval.Types
158160
import Ide.Plugin.Eval.Util
159161
( asS,
@@ -216,8 +218,11 @@ import Text.Read (readMaybe)
216218
import Util (OverridingBool (Never))
217219
import Development.IDE.Core.PositionMapping (toCurrentRange)
218220
import qualified Data.DList as DL
219-
import Control.Lens ((^.))
221+
import Control.Lens ((^.), _1, (%~), (<&>), _3)
220222
import Language.Haskell.LSP.Types.Lens (line, end)
223+
import Control.Exception (try)
224+
import CmdLineParser
225+
import qualified Development.IDE.GHC.Compat as SrcLoc
221226

222227
{- | Code Lens provider
223228
NOTE: Invoked every time the document is modified, not just when the document is saved.
@@ -272,9 +277,9 @@ codeLens _lsp st plId CodeLensParams{_textDocument} =
272277
cmd <- liftIO $ mkLspCommand plId evalCommandName "Evaluate=..." (Just [])
273278
let lenses =
274279
[ CodeLens testRange (Just cmd') Nothing
275-
| (section, test) <- tests
280+
| (section, ident, test) <- tests
276281
, let (testRange, resultRange) = testRanges test
277-
args = EvalParams (setupSections ++ [section]) _textDocument
282+
args = EvalParams (setupSections ++ [section]) _textDocument ident
278283
cmd' =
279284
(cmd :: Command)
280285
{ _arguments = Just (List [toJSON args])
@@ -308,19 +313,14 @@ evalCommandName = "evalCommand"
308313
evalCommand :: PluginCommand IdeState
309314
evalCommand = PluginCommand evalCommandName "evaluate" runEvalCmd
310315

311-
-- | Specify the test section to execute
312-
data EvalParams = EvalParams
313-
{ sections :: [Section]
314-
, module_ :: !TextDocumentIdentifier
315-
}
316-
deriving (Eq, Show, Generic, FromJSON, ToJSON)
316+
type EvalId = Int
317317

318318
runEvalCmd :: CommandFunction IdeState EvalParams
319319
runEvalCmd lsp st EvalParams{..} =
320320
let dbg = logWith st
321321
perf = timed dbg
322322
cmd = do
323-
let tests = testsBySection sections
323+
let tests = map (\(a,_,b) -> (a,b)) $ testsBySection sections
324324

325325
let TextDocumentIdentifier{_uri} = module_
326326
fp <- handleMaybe "uri" $ uriToFilePath' _uri
@@ -444,9 +444,12 @@ moduleText lsp uri =
444444
lsp
445445
(toNormalizedUri uri)
446446

447-
testsBySection :: [Section] -> [(Section, Test)]
447+
testsBySection :: [Section] -> [(Section, EvalId, Test)]
448448
testsBySection sections =
449-
[(section, test) | section <- sections, test <- sectionTests section]
449+
[(section, ident, test)
450+
| (ident, section) <- zip [0..] sections
451+
, test <- sectionTests section
452+
]
450453

451454
type TEnv = (IdeState, String)
452455

@@ -560,20 +563,36 @@ evals (st, fp) df stmts = do
560563
dbg = logWith st
561564
eval :: Statement -> Ghc (Maybe [Text])
562565
eval (Located l stmt)
563-
| -- A :set -XLanguageOption directive
564-
isRight (langOptions stmt) =
565-
either
566-
(return . Just . errorLines)
567-
( \es -> do
568-
dbg "{:SET" es
569-
ndf <- getInteractiveDynFlags
570-
dbg "pre set" $ showDynFlags ndf
571-
mapM_ addExtension es
572-
ndf <- getInteractiveDynFlags
573-
dbg "post set" $ showDynFlags ndf
574-
return Nothing
575-
)
576-
$ ghcOptions stmt
566+
| -- GHCi flags
567+
Just (words -> flags) <- parseSetFlags stmt = do
568+
dbg "{:SET" flags
569+
ndf <- getInteractiveDynFlags
570+
dbg "pre set" $ showDynFlags ndf
571+
eans <-
572+
liftIO $ try @GhcException $
573+
parseDynamicFlagsCmdLine ndf
574+
(map (L $ UnhelpfulSpan "<interactive>") flags)
575+
dbg "parsed flags" $ eans
576+
<&> (_1 %~ showDynFlags >>> _3 %~ map warnMsg)
577+
case eans of
578+
Left err -> pure $ Just $ errorLines $ show err
579+
Right (df', ignoreds, warns) -> do
580+
let warnings = do
581+
guard $ not $ null warns
582+
pure $ errorLines $
583+
unlines $
584+
map prettyWarn warns
585+
igns = do
586+
guard $ not $ null ignoreds
587+
pure
588+
["Some flags have not been recognized: "
589+
<> T.pack (intercalate ", " $ map SrcLoc.unLoc ignoreds)
590+
]
591+
dbg "post set" $ showDynFlags df'
592+
_ <- setSessionDynFlags df'
593+
sessDyns <- getSessionDynFlags
594+
setInteractiveDynFlags sessDyns
595+
pure $ warnings <> igns
577596
| -- A type/kind command
578597
Just (cmd, arg) <- parseGhciLikeCmd $ T.pack stmt =
579598
evalGhciLikeCmd cmd arg
@@ -616,6 +635,11 @@ evals (st, fp) df stmts = do
616635
let opts = execOptions{execSourceFile = fp, execLineNumber = l}
617636
in execStmt stmt opts
618637

638+
prettyWarn :: Warn -> String
639+
prettyWarn Warn{..} =
640+
prettyPrint (SrcLoc.getLoc warnMsg) <> ": warning:\n"
641+
<> " " <> SrcLoc.unLoc warnMsg
642+
619643
runGetSession :: MonadIO m => IdeState -> NormalizedFilePath -> m HscEnvEq
620644
runGetSession st nfp =
621645
liftIO $

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import Development.IDE.GHC.Compat
2020
import qualified EnumSet
2121
import GHC.LanguageExtensions.Type (Extension (..))
2222
import GhcMonad (modifySession)
23-
import GhcPlugins (DefUnitId (..), InstalledUnitId (..), fsLit, hsc_IC)
23+
import GhcPlugins (DefUnitId (..), InstalledUnitId (..), fsLit, hsc_IC, pprHsString)
2424
import HscTypes (InteractiveContext (ic_dflags))
2525
import Ide.Plugin.Eval.Util (asS, gStrictTry)
2626
import qualified Lexer
@@ -36,6 +36,7 @@ import Outputable (
3636
import qualified Parser
3737
import SrcLoc (mkRealSrcLoc)
3838
import StringBuffer (stringToStringBuffer)
39+
import Data.String (fromString)
3940

4041
{- $setup
4142
>>> import GHC
@@ -192,6 +193,7 @@ showDynFlags df =
192193
[ ("extensions", ppr . extensions $ df)
193194
, ("extensionFlags", ppr . EnumSet.toList . extensionFlags $ df)
194195
, ("importPaths", vList $ importPaths df)
196+
, ("generalFlags", pprHsString . fromString . show . EnumSet.toList . generalFlags $ df)
195197
, -- , ("includePaths", text . show $ includePaths df)
196198
-- ("packageEnv", ppr $ packageEnv df)
197199
("pkgNames", vcat . map text $ pkgNames df)

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

+8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
-- | GHC language options parser
44
module Ide.Plugin.Eval.Parse.Option (
55
langOptions,
6+
parseSetFlags,
67
) where
78

89
import Control.Monad.Combinators (many)
@@ -26,6 +27,13 @@ langOptions =
2627
left errorBundlePretty
2728
. parse (space *> languageOpts <* eof) ""
2829

30+
parseSetFlags :: String -> Maybe String
31+
parseSetFlags = parseMaybe
32+
(hspace *> chunk ":set"
33+
*> hspace1 *> takeRest
34+
:: Parsec Void String String
35+
)
36+
2937
-- >>> parseMaybe languageOpts ":set -XBinaryLiterals -XOverloadedStrings"
3038
-- Just ["BinaryLiterals","OverloadedStrings"]
3139
languageOpts :: Parsec Void String [String]

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

+12
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ module Ide.Plugin.Eval.Types
2525
RawLineComment (..),
2626
unLoc,
2727
Txt,
28+
EvalParams(..),
2829
)
2930
where
3031

@@ -37,6 +38,7 @@ import Data.String (IsString (..))
3738
import Development.IDE (Range)
3839
import GHC.Generics (Generic)
3940
import qualified Text.Megaparsec as P
41+
import Language.Haskell.LSP.Types (TextDocumentIdentifier)
4042

4143
-- | A thing with a location attached.
4244
data Located l a = Located {location :: l, located :: a}
@@ -148,3 +150,13 @@ data LineChunk = LineChunk String | WildCardChunk
148150

149151
instance IsString LineChunk where
150152
fromString = LineChunk
153+
154+
type EvalId = Int
155+
156+
-- | Specify the test section to execute
157+
data EvalParams = EvalParams
158+
{ sections :: [Section]
159+
, module_ :: !TextDocumentIdentifier
160+
, evalId :: !EvalId -- ^ unique group id; for test uses
161+
}
162+
deriving (Eq, Show, Generic, FromJSON, ToJSON)

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

+19-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
{-# LANGUAGE ViewPatterns #-}
2+
{-# LANGUAGE RecordWildCards #-}
13
{-# LANGUAGE OverloadedStrings #-}
24
{-# LANGUAGE ScopedTypeVariables #-}
35

@@ -26,7 +28,7 @@ import Language.Haskell.LSP.Test (
2628
import Language.Haskell.LSP.Types (
2729
ApplyWorkspaceEditRequest,
2830
CodeLens (CodeLens, _command, _range),
29-
Command (Command, _title),
31+
Command (Command, _title, _arguments),
3032
Position (..),
3133
Range (..),
3234
TextDocumentIdentifier,
@@ -43,12 +45,16 @@ import Test.Tasty (
4345
)
4446
import Test.Tasty.ExpectedFailure (
4547
expectFailBecause,
46-
ignoreTestBecause,
4748
)
4849
import Test.Tasty.HUnit (
4950
testCase,
5051
(@?=),
5152
)
53+
import Data.List.Extra (nubOrdOn)
54+
import Development.IDE (List(List))
55+
import Ide.Plugin.Eval.Types (EvalParams(..))
56+
import Data.Aeson (fromJSON)
57+
import Data.Aeson.Types (Result(Success))
5258

5359
tests :: TestTree
5460
tests =
@@ -140,9 +146,11 @@ tests =
140146
, testCase "Local Modules imports are accessible in a test" $
141147
goldenTest "TLocalImport.hs"
142148
, -- , testCase "Local Modules can be imported in a test" $ goldenTest "TLocalImportInTest.hs"
143-
ignoreTestBecause "Unexplained but minor issue" $
149+
expectFailBecause "Unexplained but minor issue" $
144150
testCase "Setting language option TupleSections" $
145151
goldenTest "TLanguageOptionsTupleSections.hs"
152+
, testCase ":set accepts ghci flags" $
153+
goldenTest "TFlags.hs"
146154
, testCase "IO expressions are supported, stdout/stderr output is ignored" $
147155
goldenTest "TIO.hs"
148156
, testCase "Property checking" $ goldenTest "TProperty.hs"
@@ -187,10 +195,11 @@ goldenTestBy fltr input = runSession hlsCommand fullCaps evalPath $ do
187195
codeLenses <- reverse <$> getCodeLensesBy fltr doc
188196
-- liftIO $ print codeLenses
189197

190-
-- Execute sequentially, waiting for a moment to
191-
-- avoid mis-insertion due to staled location info.
198+
-- Execute sequentially, nubbing elements to avoid
199+
-- evaluating the same section with multiple tests
200+
-- more than twice
192201
mapM_ executeCmd
193-
[c | CodeLens{_command = Just c} <- codeLenses]
202+
$ nubOrdOn actSectionId [c | CodeLens{_command = Just c} <- codeLenses]
194203

195204
edited <- replaceUnicodeQuotes <$> documentContents doc
196205
-- liftIO $ T.putStrLn edited
@@ -204,6 +213,10 @@ goldenTestBy fltr input = runSession hlsCommand fullCaps evalPath $ do
204213
expected <- T.readFile expectedFile
205214
edited @?= expected
206215

216+
actSectionId :: Command -> Int
217+
actSectionId Command{_arguments = Just (List [fromJSON -> Success EvalParams{..}])} = evalId
218+
actSectionId _ = error "Invalid CodeLens"
219+
207220
getEvalCodeLenses :: TextDocumentIdentifier -> Session [CodeLens]
208221
getEvalCodeLenses = getCodeLensesBy isEvalTest
209222

plugins/hls-eval-plugin/test/testdata/TLanguageOptions.hs renamed to plugins/hls-eval-plugin/test/testdata/TFlags.hs

+28-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
-- Support for language options
22

33
{-# LANGUAGE ScopedTypeVariables #-}
4-
module TLanguageOptions where
4+
module TFlags where
55

66
-- Language options set in the module source (ScopedTypeVariables)
77
-- also apply to tests so this works fine
@@ -38,6 +38,31 @@ It still works
3838
>>> class F
3939
-}
4040

41-
{- Wrong option names are reported.
42-
>>> :set -XWrong
41+
{- Now -package flag is handled correctly:
42+
43+
>>> :set -package ghc-prim
44+
>>> import GHC.Prim
45+
46+
-}
47+
48+
{- -fprint-* families
49+
50+
>>> import Data.Proxy
51+
>>> :set -XPolyKinds
52+
>>> :t Proxy
53+
Proxy :: forall k (t :: k). Proxy t
54+
55+
>>> :set -fprint-explicit-foralls
56+
>>> :t Proxy
57+
Proxy :: forall {k} {t :: k}. Proxy t
58+
-}
59+
60+
{- Invalid option/flags are reported, but valid ones will be reflected
61+
62+
>>> :set -XRank2Types -XAbsent -XDatatypeContexts -XWrong -fprint-nothing-at-all
63+
64+
Still, Rank2Types is enabled, as in GHCi:
65+
66+
>>> f = const 42 :: (forall x. x) -> Int
67+
>>> f undefined
4368
-}

0 commit comments

Comments
 (0)