Skip to content

Commit 856cd92

Browse files
committed
Use Set to remove duplicates from CollectLiterals Result.
Certain AST representations are traversed multiple times with the switch to SYB. Using Set allows to easily remove duplicate Literals from our result. Added a test suite to monitor for regressions.
1 parent 9dd1ba8 commit 856cd92

27 files changed

+195
-47
lines changed

plugins/hls-alternate-number-format-plugin/hls-alternate-number-format-plugin.cabal

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ library
2020
build-depends:
2121
aeson
2222
, base >=4.12 && < 5
23+
, containers
2324
, ghcide >=1.5.0 && <1.6
2425
, ghc-boot-th
2526
, hls-graph
@@ -54,8 +55,9 @@ test-suite tests
5455
, filepath
5556
, hls-alternate-number-format-plugin
5657
, hls-test-utils >=1.0 && <1.2
57-
, regex-tdfa
58+
, lsp
5859
, QuickCheck
60+
, regex-tdfa
5961
, tasty-quickcheck
6062
, text
6163

plugins/hls-alternate-number-format-plugin/src/Ide/Plugin/AlternateNumberFormat.hs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import qualified Data.HashMap.Strict as HashMap
1212
import Data.Maybe (fromMaybe, isJust)
1313
import Data.Text (Text)
1414
import qualified Data.Text as T
15+
import Debug.Trace (traceM)
1516
import Development.IDE (GetParsedModule (GetParsedModule),
1617
IdeState, RuleResult,
1718
Rules, define, ideLogger,
18-
noRange,
1919
realSrcSpanToRange,
2020
runAction, use,
2121
useWithStale)
@@ -83,8 +83,7 @@ codeActionHandler state _ (CodeActionParams _ _ docId currRange _) = response $
8383
-- make a code action for every literal and its' alternates (then flatten the result)
8484
actions = concatMap (\(lit, alts) -> map (mkCodeAction nfp lit) alts) literalPairs
8585

86-
logIO state "Literals: "
87-
mapM_ (logIO state) literals
86+
logIO state $ "Literals: " <> show literals
8887

8988
pure $ List actions
9089
where
@@ -108,7 +107,6 @@ codeActionHandler state _ (CodeActionParams _ _ docId currRange _) = response $
108107
mkWorkspaceEdit :: NormalizedFilePath -> Literal -> Text -> WorkspaceEdit
109108
mkWorkspaceEdit nfp lit alt = WorkspaceEdit changes Nothing Nothing
110109
where
111-
-- NOTE: currently our logic filters our any noRange possibilities
112110
txtEdit = TextEdit (realSrcSpanToRange $ getSrcSpan lit) alt
113111
changes = Just $ HashMap.fromList [( filePathToUri $ fromNormalizedFilePath nfp, List [txtEdit])]
114112

@@ -119,12 +117,6 @@ contains Range {_start, _end} x = isInsideRealSrcSpan _start x || isInsideRealSr
119117
isInsideRealSrcSpan :: Position -> RealSrcSpan -> Bool
120118
p `isInsideRealSrcSpan` r = let (Range sp ep) = realSrcSpanToRange r in sp <= p && p <= ep
121119

122-
-- a source span that provides no meaningful information is NOT a valid source span for our use case
123-
isRealSrcSpan :: SrcSpan -> Bool
124-
isRealSrcSpan (UnhelpfulSpan _) = False
125-
isRealSrcSpan _ = True
126-
127-
128120
getNormalizedFilePath :: Monad m => TextDocumentIdentifier -> ExceptT String m NormalizedFilePath
129121
getNormalizedFilePath docId = handleMaybe "Error: converting to NormalizedFilePath"
130122
$ uriToNormalizedFilePath
@@ -138,5 +130,5 @@ requestLiterals state = handleMaybeM "Error: Could not get ParsedModule"
138130

139131

140132
logIO :: (MonadIO m, Show a) => IdeState -> a -> m ()
141-
logIO state = liftIO . Logger.logDebug (ideLogger state) . T.pack . show
133+
logIO state = liftIO . Logger.logInfo (ideLogger state) . T.pack . show
142134

plugins/hls-alternate-number-format-plugin/src/Ide/Plugin/Literals.hs

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
{-# LANGUAGE RankNTypes #-}
55
module Ide.Plugin.Literals where
66
import Data.Maybe (fromMaybe, maybeToList)
7+
import Data.Set (Set)
8+
import qualified Data.Set as S
79
import Data.Text (Text)
810
import qualified Data.Text as T
911
import Development.IDE.GHC.Compat
@@ -18,7 +20,7 @@ import Generics.SYB (Data, Typeable, cast,
1820
-- we currently don't have any use for PrimLiterals. They never have source text so we always drop them
1921
data Literal = IntLiteral RealSrcSpan Text Integer
2022
| FracLiteral RealSrcSpan Text Rational
21-
deriving (GHC.Generic, Show)
23+
deriving (GHC.Generic, Show, Ord, Eq)
2224

2325
instance NFData RealSrcSpan where
2426
rnf x = x `seq` ()
@@ -37,46 +39,49 @@ getSrcSpan = \case
3739

3840
-- | Find all literals in a Parsed Source File
3941
collectLiterals :: (Data ast, Typeable ast) => ast -> [Literal]
40-
collectLiterals = everything (<>) (mkQ2 ([] :: [Literal]) traverseLExpr traverseLPat)
42+
collectLiterals = S.toList . collectLiterals'
4143

42-
-- Located Patterns for whatever reason don't get picked up when using `(mkQ ([] :: [Literal]) traverseLExpr)
44+
collectLiterals' :: (Data ast, Typeable ast) => ast -> Set Literal
45+
collectLiterals' = everything (<>) (mkQ2 (S.empty :: Set Literal) traverseLExpr traverseLPat)
46+
47+
-- Located Patterns for whatever reason don't get picked up when using `(mkQ (S.empty :: Set Literal) traverseLExpr)
4348
-- as such we need to explicit traverse those in order to pull out any literals
4449
mkQ2 :: (Typeable a, Typeable b, Typeable c) => r -> (b -> r) -> (c -> r) -> a -> r
4550
mkQ2 def left right datum = case cast datum of
46-
Just datum' -> left datum'
47-
Nothing -> maybe def right (cast datum)
51+
Just datum' -> left datum'
52+
Nothing -> maybe def right (cast datum)
4853

49-
traverseLPat :: GenLocated SrcSpan (Pat GhcPs) -> [Literal]
54+
traverseLPat :: GenLocated SrcSpan (Pat GhcPs) -> Set Literal
5055
traverseLPat (L sSpan pat) = traversePat sSpan pat
5156

52-
traversePat :: SrcSpan -> Pat GhcPs -> [Literal]
57+
traversePat :: SrcSpan -> Pat GhcPs -> Set Literal
5358
traversePat sSpan = \case
5459
LitPat _ lit -> getLiteralAsList sSpan lit
5560
NPat _ (L olSpan overLit) sexpr1 sexpr2 -> getOverLiteralAsList olSpan overLit
56-
<> collectLiterals sexpr1
57-
<> collectLiterals sexpr2
61+
<> collectLiterals' sexpr1
62+
<> collectLiterals' sexpr2
5863
NPlusKPat _ _ (L olSpan loverLit) overLit sexpr1 sexpr2 -> getOverLiteralAsList olSpan loverLit
5964
<> getOverLiteralAsList sSpan overLit
60-
<> collectLiterals sexpr1
61-
<> collectLiterals sexpr2
62-
ast -> collectLiterals ast
65+
<> collectLiterals' sexpr1
66+
<> collectLiterals' sexpr2
67+
ast -> collectLiterals' ast
6368

64-
traverseLExpr :: GenLocated SrcSpan (HsExpr GhcPs) -> [Literal]
69+
traverseLExpr :: GenLocated SrcSpan (HsExpr GhcPs) -> Set Literal
6570
traverseLExpr (L sSpan hsExpr) = traverseExpr sSpan hsExpr
6671

67-
traverseExpr :: SrcSpan -> HsExpr GhcPs -> [Literal]
72+
traverseExpr :: SrcSpan -> HsExpr GhcPs -> Set Literal
6873
traverseExpr sSpan = \case
6974
HsOverLit _ overLit -> getOverLiteralAsList sSpan overLit
7075
HsLit _ lit -> getLiteralAsList sSpan lit
71-
expr -> collectLiterals expr
76+
expr -> collectLiterals' expr
7277

73-
getLiteralAsList :: SrcSpan -> HsLit GhcPs -> [Literal]
78+
getLiteralAsList :: SrcSpan -> HsLit GhcPs -> Set Literal
7479
getLiteralAsList sSpan lit = case sSpan of
7580
RealSrcSpan rss _ -> getLiteralAsList' lit rss
76-
_ -> []
81+
_ -> S.empty
7782

78-
getLiteralAsList' :: HsLit GhcPs -> RealSrcSpan -> [Literal]
79-
getLiteralAsList' lit = maybeToList . flip getLiteral lit
83+
getLiteralAsList' :: HsLit GhcPs -> RealSrcSpan -> Set Literal
84+
getLiteralAsList' lit = maybe S.empty S.singleton . flip getLiteral lit
8085

8186
-- Translate from Hs Type to our Literal type
8287
getLiteral :: RealSrcSpan -> HsLit GhcPs -> Maybe Literal
@@ -87,13 +92,13 @@ getLiteral sSpan = \case
8792
HsRat _ val _ -> fromFractionalLit sSpan val
8893
_ -> Nothing
8994

90-
getOverLiteralAsList :: SrcSpan -> HsOverLit GhcPs -> [Literal]
95+
getOverLiteralAsList :: SrcSpan -> HsOverLit GhcPs -> Set Literal
9196
getOverLiteralAsList sSpan lit = case sSpan of
9297
RealSrcSpan rss _ -> getOverLiteralAsList' lit rss
93-
_ -> []
98+
_ -> S.empty
9499

95-
getOverLiteralAsList' :: HsOverLit GhcPs -> RealSrcSpan -> [Literal]
96-
getOverLiteralAsList' lit = maybeToList . flip getOverLiteral lit
100+
getOverLiteralAsList' :: HsOverLit GhcPs -> RealSrcSpan -> Set Literal
101+
getOverLiteralAsList' lit sSpan = maybe S.empty S.singleton (getOverLiteral sSpan lit)
97102

98103
getOverLiteral :: RealSrcSpan -> HsOverLit GhcPs -> Maybe Literal
99104
getOverLiteral sSpan OverLit{..} = case ol_val of

plugins/hls-alternate-number-format-plugin/test/Main.hs

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
{-# LANGUAGE ViewPatterns #-}
44
module Main ( main ) where
55

6-
import Control.Concurrent (threadDelay)
6+
import Data.Either (rights)
77
import Data.List (find)
88
import Data.Text (Text)
99
import qualified Data.Text as T
1010
import Debug.Trace
1111
import qualified Ide.Plugin.AlternateNumberFormat as AlternateNumberFormat
1212
import qualified Ide.Plugin.Conversion as Conversion
13-
import Properties.Conversion
13+
import Language.LSP.Types (toEither)
14+
import Language.LSP.Types.Lens (kind)
15+
import Properties.Conversion (conversions)
1416
import System.FilePath ((</>))
1517
import Test.Hls
1618
import Text.Regex.TDFA ((=~))
@@ -21,32 +23,87 @@ main = defaultTestRunner test
2123
alternateNumberFormatPlugin :: PluginDescriptor IdeState
2224
alternateNumberFormatPlugin = AlternateNumberFormat.descriptor "alternateNumberFormat"
2325

26+
27+
-- NOTE: For whatever reason, this plugin does not play nice with creating Code Actions on time.
28+
-- As a result tests will mostly pass if `import Prelude` is added at the top. We (mostly fendor) surmise this has something
29+
-- to do with how
2430
test :: TestTree
2531
test = testGroup "alternateNumberFormat" [
26-
-- codeActionHex "TIntDtoH" 4 13
27-
conversions ]
32+
codeActionHex "TIntDtoH" 3 13
33+
, codeActionOctal "TIntDtoO" 3 13
34+
, codeActionBinary "TIntDtoB" 4 13
35+
, codeActionNumDecimal "TIntDtoND" 5 13
36+
, codeActionFracExp "TFracDtoE" 3 13
37+
, codeActionFloatHex "TFracDtoHF" 4 13
38+
, codeActionDecimal "TIntHtoD" 3 13
39+
, codeActionDecimal "TFracHFtoD" 4 13
40+
, codeActionProperties "TFindLiteralIntPattern" [(3, 25), (4,25)] $ \actions -> do
41+
liftIO $ length actions @?= 4
42+
, codeActionProperties "TFindLiteralIntCase" [(3, 29)] $ \actions -> do
43+
liftIO $ length actions @?= 2
44+
, codeActionProperties "TFindLiteralIntCase2" [(4, 21)] $ \actions -> do
45+
liftIO $ length actions @?= 2
46+
, codeActionProperties "TFindLiteralDoReturn" [(5, 10)] $ \actions -> do
47+
liftIO $ length actions @?= 2
48+
, codeActionProperties "TFindLiteralDoLet" [(5, 13), (6, 13)] $ \actions -> do
49+
liftIO $ length actions @?= 4
50+
, codeActionProperties "TFindLiteralList" [(3, 28)] $ \actions -> do
51+
liftIO $ length actions @?= 2
52+
, conversions
53+
]
54+
55+
codeActionProperties :: TestName -> [(Int, Int)] -> ([CodeAction] -> Session ()) -> TestTree
56+
codeActionProperties fp locs assertions = testCase fp $ do
57+
runSessionWithServer alternateNumberFormatPlugin testDataDir $ do
58+
openDoc (fp <> ".hs") "haskell" >>= codeActionsFromLocs >>= findAlternateNumberActions >>= assertions
59+
where
60+
-- similar to codeActionTest
61+
codeActionsFromLocs doc = concat <$> mapM (getCodeActions doc . uncurry pointRange) locs
62+
63+
findAlternateNumberActions :: [Command |? CodeAction] -> Session [CodeAction]
64+
findAlternateNumberActions = pure . filter isAlternateNumberCodeAction . rights . map toEither
65+
where
66+
isAlternateNumberCodeAction CodeAction{_kind} = case _kind of
67+
Nothing -> False
68+
Just kind -> case kind of
69+
CodeActionUnknown txt -> txt == "alternate.style"
70+
_ -> False
71+
72+
-- most helpers derived from explicit-imports-plugin Main Test file
2873

2974
testDataDir :: FilePath
3075
testDataDir = "test" </> "testdata"
3176

32-
-- most helpers derived from explicit-imports-plugin Main Test file
33-
3477
goldenAlternateFormat :: FilePath -> (TextDocumentIdentifier -> Session ()) -> TestTree
3578
goldenAlternateFormat fp = goldenWithHaskellDoc alternateNumberFormatPlugin (fp <> " (golden)") testDataDir fp "expected" "hs"
3679

3780
codeActionTest :: (Maybe Text -> Bool) -> FilePath -> Int -> Int -> TestTree
3881
codeActionTest filter' fp line col = goldenAlternateFormat fp $ \doc -> do
39-
-- _ <- waitForDiagnostics
4082
actions <- getCodeActions doc (pointRange line col)
41-
traceM $ "Code actions: " ++ show actions
4283
-- can't generate code actions?
4384
case find (filter' . codeActionTitle) actions of
4485
Just (InR x) -> executeCodeAction x
4586
_ -> liftIO $ assertFailure "Unable to find CodeAction"
4687

88+
89+
codeActionDecimal :: FilePath -> Int -> Int -> TestTree
90+
codeActionDecimal = codeActionTest isDecimalCodeAction
91+
4792
codeActionHex :: FilePath -> Int -> Int -> TestTree
4893
codeActionHex = codeActionTest isHexCodeAction
4994

95+
codeActionOctal :: FilePath -> Int -> Int -> TestTree
96+
codeActionOctal = codeActionTest isOctalCodeAction
97+
98+
codeActionBinary :: FilePath -> Int -> Int -> TestTree
99+
codeActionBinary = codeActionTest isBinaryCodeAction
100+
101+
codeActionNumDecimal :: FilePath -> Int -> Int -> TestTree
102+
codeActionNumDecimal = codeActionTest isNumDecimalCodeAction
103+
104+
codeActionFracExp :: FilePath -> Int -> Int -> TestTree
105+
codeActionFracExp = codeActionTest isNumDecimalCodeAction
106+
50107
codeActionFloatHex :: FilePath -> Int -> Int -> TestTree
51108
codeActionFloatHex = codeActionTest isHexFloatCodeAction
52109

@@ -86,6 +143,8 @@ isBinaryCodeAction = isCodeAction binaryRegex
86143
isOctalCodeAction :: Maybe Text -> Bool
87144
isOctalCodeAction = isCodeAction octalRegex
88145

146+
-- This can match EITHER an integer as NumDecimal extension or a Fractional
147+
-- as in 1.23e-3 (so anything with an exponent really)
89148
isNumDecimalCodeAction :: Maybe Text -> Bool
90149
isNumDecimalCodeAction = isCodeAction numDecimalRegex
91150

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module TFindLiteralDoLet where
2+
3+
doLet :: IO ()
4+
doLet = do
5+
let x = 199
6+
y = 144
7+
pure ()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module TFindLiteralDoReturn where
2+
3+
doReturn :: IO Integer
4+
doReturn = do
5+
pure 54
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module TFindLiteralIntCase where
2+
3+
caseExpression x = case x + 34 of
4+
_ -> "testing if we find a literal in the case statement"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module TFindLiteralIntCase where
2+
3+
caseExpression x = case x of
4+
57 -> "testing to find literals in matching cases"
5+
_ -> ""
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module TFindLiteralIntPattern where
2+
3+
patternMatchingFunction 1 = "one"
4+
patternMatchingFunction 2 = "two"
5+
patternMatchingFunction _ = "the rest of the numbers"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module TFindLiteralList where
2+
3+
listTest = [reverse $ show 57]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module TFracDtoE where
2+
3+
convertMe = 1.2345e2
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module TFracDtoE where
2+
3+
convertMe = 123.45
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{-# LANGUAGE HexFloatLiterals #-}
2+
module TFracDtoHF where
3+
4+
convertMe = 0x1.edccccccccccdp6
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{-# LANGUAGE HexFloatLiterals #-}
2+
module TFracDtoHF where
3+
4+
convertMe = 123.45
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{-# LANGUAGE HexFloatLiterals #-}
2+
module TFracDtoHF where
3+
4+
convertMe = 123.45
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{-# LANGUAGE HexFloatLiterals #-}
2+
module TFracDtoHF where
3+
4+
convertMe = 0x1.edccccccccccdp6
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{-# LANGUAGE BinaryLiterals #-}
2+
module TIntDtoB where
3+
4+
convertMe = 0b1100
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{-# LANGUAGE BinaryLiterals #-}
2+
module TIntDtoB where
3+
4+
convertMe = 12
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
module TIntDtoH where
22

3-
import Prelude
4-
53
convertMe = 0xC
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
module TIntDtoH where
22

3-
import Prelude
4-
53
convertMe = 12
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{-# LANGUAGE NumDecimals #-}
2+
module TIntDtoND where
3+
4+
convertMe :: Integer
5+
convertMe = 125.345e3
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{-# LANGUAGE NumDecimals #-}
2+
module TIntDtoND where
3+
4+
convertMe :: Integer
5+
convertMe = 125345
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module TIntDtoO where
2+
3+
convertMe = 0o14
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module TIntDtoO where
2+
3+
convertMe = 12

0 commit comments

Comments
 (0)