packages feed

ghcide-2.4.0.0: test/exe/CompletionTests.hs

{-# LANGUAGE GADTs            #-}
{-# LANGUAGE OverloadedLabels #-}

module CompletionTests (tests) where

import           Control.Lens                   ((^.))
import qualified Control.Lens                   as Lens
import           Control.Monad
import           Control.Monad.IO.Class         (liftIO)
import           Data.Default
import           Data.List.Extra
import           Data.Maybe
import           Data.Row
import qualified Data.Text                      as T
import           Development.IDE.GHC.Compat     (GhcVersion (..), ghcVersion)
import           Development.IDE.Test           (waitForTypecheck)
import           Development.IDE.Types.Location
import           Ide.Plugin.Config
import qualified Language.LSP.Protocol.Lens     as L
import           Language.LSP.Protocol.Message
import           Language.LSP.Protocol.Types    hiding
                                                (SemanticTokenAbsolute (..),
                                                 SemanticTokenRelative (..),
                                                 SemanticTokensEdit (..),
                                                 mkRange)
import           Language.LSP.Test
import           System.FilePath
import           Test.Tasty
import           Test.Tasty.HUnit
import           TestUtils


tests :: TestTree
tests
  = testGroup "completion"
    [
    testGroup "non local" nonLocalCompletionTests
    , testGroup "topLevel" topLevelCompletionTests
    , testGroup "local" localCompletionTests
    , testGroup "package" packageCompletionTests
    , testGroup "project" projectCompletionTests
    , testGroup "other" otherCompletionTests
    , testGroup "doc" completionDocTests
    ]

completionTest :: HasCallStack => String -> [T.Text] -> Position -> [(T.Text, CompletionItemKind, T.Text, Bool, Bool, Maybe [TextEdit])] -> TestTree
completionTest name src pos expected = testSessionWait name $ do
    docId <- createDoc "A.hs" "haskell" (T.unlines src)
    _ <- waitForDiagnostics
    compls <- getAndResolveCompletions docId pos
    let compls' = [ (_label, _kind, _insertText, _additionalTextEdits) | CompletionItem{..} <- compls]
    let emptyToMaybe x = if T.null x then Nothing else Just x
    liftIO $ sortOn (Lens.view Lens._1) (take (length expected) compls') @?=
        sortOn (Lens.view Lens._1)
          [ (l, Just k, emptyToMaybe t, at) | (l,k,t,_,_,at) <- expected]
    forM_ (zip compls expected) $ \(CompletionItem{..}, (_,_,_,expectedSig, expectedDocs, _)) -> do
        when expectedSig $
            liftIO $ assertBool ("Missing type signature: " <> T.unpack _label) (isJust _detail)
        when expectedDocs $
            liftIO $ assertBool ("Missing docs: " <> T.unpack _label) (isJust _documentation)


topLevelCompletionTests :: [TestTree]
topLevelCompletionTests = [
    completionTest
        "variable"
        ["bar = xx", "-- | haddock", "xxx :: ()", "xxx = ()", "-- | haddock", "data Xxx = XxxCon"]
        (Position 0 8)
        [("xxx", CompletionItemKind_Function, "xxx", True, True, Nothing)
        ],
    completionTest
        "constructor"
        ["bar = xx", "-- | haddock", "xxx :: ()", "xxx = ()", "-- | haddock", "data Xxx = XxxCon"]
        (Position 0 8)
        [("xxx", CompletionItemKind_Function, "xxx", True, True, Nothing)
        ],
    completionTest
        "class method"
        ["bar = xx", "class Xxx a where", "-- | haddock", "xxx :: ()", "xxx = ()"]
        (Position 0 8)
        [("xxx", CompletionItemKind_Function, "xxx", True, True, Nothing)],
    completionTest
        "type"
        ["bar :: Xz", "zzz = ()", "-- | haddock", "data Xzz = XzzCon"]
        (Position 0 9)
        [("Xzz", CompletionItemKind_Struct, "Xzz", False, True, Nothing)],
    completionTest
        "class"
        ["bar :: Xz", "zzz = ()", "-- | haddock", "class Xzz a"]
        (Position 0 9)
        [("Xzz", CompletionItemKind_Interface, "Xzz", False, True, Nothing)],
    completionTest
        "records"
        ["data Person = Person { _personName:: String, _personAge:: Int}", "bar = Person { _pers }" ]
        (Position 1 19)
        [("_personName", CompletionItemKind_Function, "_personName", False, True, Nothing),
         ("_personAge", CompletionItemKind_Function, "_personAge", False, True, Nothing)],
    completionTest
        "recordsConstructor"
        ["data XxRecord = XyRecord { x:: String, y:: Int}", "bar = Xy" ]
        (Position 1 19)
        [("XyRecord", CompletionItemKind_Constructor, "XyRecord", False, True, Nothing),
         ("XyRecord", CompletionItemKind_Snippet, "XyRecord {x=${1:_x}, y=${2:_y}}", False, True, Nothing)]
    ]

localCompletionTests :: [TestTree]
localCompletionTests = [
    completionTest
        "argument"
        ["bar (Just abcdef) abcdefg = abcd"]
        (Position 0 32)
        [("abcdef", CompletionItemKind_Function, "abcdef", True, False, Nothing),
         ("abcdefg", CompletionItemKind_Function , "abcdefg", True, False, Nothing)
        ],
    completionTest
        "let"
        ["bar = let (Just abcdef) = undefined"
        ,"          abcdefg = let abcd = undefined in undefined"
        ,"        in abcd"
        ]
        (Position 2 15)
        [("abcdef", CompletionItemKind_Function, "abcdef", True, False, Nothing),
         ("abcdefg", CompletionItemKind_Function , "abcdefg", True, False, Nothing)
        ],
    completionTest
        "where"
        ["bar = abcd"
        ,"  where (Just abcdef) = undefined"
        ,"        abcdefg = let abcd = undefined in undefined"
        ]
        (Position 0 10)
        [("abcdef", CompletionItemKind_Function, "abcdef", True, False, Nothing),
         ("abcdefg", CompletionItemKind_Function , "abcdefg", True, False, Nothing)
        ],
    completionTest
        "do/1"
        ["bar = do"
        ,"  Just abcdef <- undefined"
        ,"  abcd"
        ,"  abcdefg <- undefined"
        ,"  pure ()"
        ]
        (Position 2 6)
        [("abcdef", CompletionItemKind_Function, "abcdef", True, False, Nothing)
        ],
    completionTest
        "do/2"
        ["bar abcde = do"
        ,"    Just [(abcdef,_)] <- undefined"
        ,"    abcdefg <- undefined"
        ,"    let abcdefgh = undefined"
        ,"        (Just [abcdefghi]) = undefined"
        ,"    abcd"
        ,"  where"
        ,"    abcdefghij = undefined"
        ]
        (Position 5 8)
        [("abcde", CompletionItemKind_Function, "abcde", True, False, Nothing)
        ,("abcdefghij", CompletionItemKind_Function, "abcdefghij", True, False, Nothing)
        ,("abcdef", CompletionItemKind_Function, "abcdef", True, False, Nothing)
        ,("abcdefg", CompletionItemKind_Function, "abcdefg", True, False, Nothing)
        ,("abcdefgh", CompletionItemKind_Function, "abcdefgh", True, False, Nothing)
        ,("abcdefghi", CompletionItemKind_Function, "abcdefghi", True, False, Nothing)
        ],
    completionTest
        "type family"
        ["{-# LANGUAGE DataKinds, TypeFamilies #-}"
        ,"type family Bar a"
        ,"a :: Ba"
        ]
        (Position 2 7)
        [("Bar", CompletionItemKind_Struct, "Bar", True, False, Nothing)
        ],
    completionTest
        "class method"
        [
          "class Test a where"
        , "    abcd :: a -> ()"
        , "    abcde :: a -> Int"
        , "instance Test Int where"
        , "    abcd = abc"
        ]
        (Position 4 14)
        [("abcd", CompletionItemKind_Function, "abcd", True, False, Nothing)
        ,("abcde", CompletionItemKind_Function, "abcde", True, False, Nothing)
        ],
    testSessionWait "incomplete entries" $ do
        let src a = "data Data = " <> a
        doc <- createDoc "A.hs" "haskell" $ src "AAA"
        void $ waitForTypecheck doc
        let editA rhs =
                changeDoc doc [TextDocumentContentChangeEvent . InR . (.==) #text $ src rhs]
        editA "AAAA"
        void $ waitForTypecheck doc
        editA "AAAAA"
        void $ waitForTypecheck doc

        compls <- getCompletions doc (Position 0 15)
        liftIO $ filter ("AAA" `T.isPrefixOf`) (mapMaybe _insertText compls) @?= ["AAAAA"]
        pure ()
    ]

nonLocalCompletionTests :: [TestTree]
nonLocalCompletionTests =
  [ brokenForWinGhc $ completionTest
      "variable"
      ["module A where", "f = hea"]
      (Position 1 7)
      [("head", CompletionItemKind_Function, "head", True, True, Nothing)],
    completionTest
      "constructor"
      ["{-# OPTIONS_GHC -Wall #-}", "module A where", "f = True"]
      (Position 2 8)
      [ ("True", CompletionItemKind_Constructor, "True", True, True, Nothing)
      ],
    brokenForWinGhc $ completionTest
      "type"
      ["{-# OPTIONS_GHC -Wall #-}", "module A () where", "f :: Boo", "f = True"]
      (Position 2 8)
      [ ("Bool", CompletionItemKind_Struct, "Bool", True, True, Nothing)
      ],
    completionTest
      "qualified"
      ["{-# OPTIONS_GHC -Wunused-binds #-}", "module A () where", "f = Prelude.hea"]
      (Position 2 15)
      [ ("head", CompletionItemKind_Function, "head", True, True, Nothing)
      ],
    completionTest
      "duplicate import"
      ["module A where", "import Data.List", "import Data.List", "f = permu"]
      (Position 3 9)
      [ ("permutations", CompletionItemKind_Function, "permutations", False, False, Nothing)
      ],
    completionTest
       "dont show hidden items"
       [ "{-# LANGUAGE NoImplicitPrelude #-}",
         "module A where",
         "import Control.Monad hiding (join)",
         "f = joi"
       ]
       (Position 3 6)
       [],
    testGroup "ordering"
      [completionTest "qualified has priority"
        ["module A where"
        ,"import qualified Data.ByteString as BS"
        ,"f = BS.read"
        ]
        (Position 2 10)
        [("readFile", CompletionItemKind_Function, "readFile", True, True, Nothing)]
        ],
      -- we need this test to make sure the ghcide completions module does not return completions for language pragmas. this functionality is turned on in hls
     completionTest
      "do not show pragma completions"
      [ "{-# LANGUAGE  ",
        "{module A where}",
        "main = return ()"
      ]
      (Position 0 13)
      []
  ]
  where
    brokenForWinGhc = knownBrokenFor (BrokenSpecific Windows [GHC90, GHC92, GHC94, GHC96, GHC98]) "Windows has strange things in scope for some reason"

otherCompletionTests :: [TestTree]
otherCompletionTests = [
    completionTest
      "keyword"
      ["module A where", "f = newty"]
      (Position 1 9)
      [("newtype", CompletionItemKind_Keyword, "", False, False, Nothing)],
    completionTest
      "type context"
      [ "{-# OPTIONS_GHC -Wunused-binds #-}",
        "module A () where",
        "f = f",
        "g :: Intege"
      ]
      -- At this point the module parses but does not typecheck.
      -- This should be sufficient to detect that we are in a
      -- type context and only show the completion to the type.
      (Position 3 11)
      [("Integer", CompletionItemKind_Struct, "Integer", True, True, Nothing)],

    testSession "duplicate record fields" $ do
      void $
        createDoc "B.hs" "haskell" $
          T.unlines
            [ "{-# LANGUAGE DuplicateRecordFields #-}",
              "module B where",
              "newtype Foo = Foo { member :: () }",
              "newtype Bar = Bar { member :: () }"
            ]
      docA <-
        createDoc "A.hs" "haskell" $
          T.unlines
            [ "module A where",
              "import B",
              "memb"
            ]
      _ <- waitForDiagnostics
      compls <- getCompletions docA $ Position 2 4
      let compls' = [txt | CompletionItem {_insertText = Just txt, ..} <- compls, _label == "member"]
      liftIO $ take 1 compls' @?= ["member"],

    testSessionWait "maxCompletions" $ do
        doc <- createDoc "A.hs" "haskell" $ T.unlines
            [ "{-# OPTIONS_GHC -Wunused-binds #-}",
                "module A () where",
                "a = Prelude."
            ]
        _ <- waitForDiagnostics
        compls <- getCompletions  doc (Position 3 13)
        liftIO $ length compls @?= maxCompletions def
  ]

packageCompletionTests :: [TestTree]
packageCompletionTests =
  [ testSession' "fromList" $ \dir -> do
        liftIO $ writeFile (dir </> "hie.yaml")
            "cradle: {direct: {arguments: [-hide-all-packages, -package, base, A]}}"
        doc <- createDoc "A.hs" "haskell" $ T.unlines
            [ "{-# OPTIONS_GHC -Wunused-binds #-}",
                "module A () where",
                "a = fromList"
            ]
        _ <- waitForDiagnostics
        compls <- getCompletions doc (Position 2 12)
        let compls' =
              [T.drop 1 $ T.dropEnd 3 d
              | CompletionItem {_documentation = Just (InR (MarkupContent MarkupKind_Markdown d)), _label}
                <- compls
              , _label == "fromList"
              ]
        liftIO $ take 3 (sort compls') @?=
          map ("Defined in "<>) (
              [ "'Data.List.NonEmpty"
              , "'GHC.Exts"
              ] ++ if ghcVersion >= GHC94 then [ "'GHC.IsList" ] else [])

  , testSessionWait "Map" $ do
        doc <- createDoc "A.hs" "haskell" $ T.unlines
            [ "{-# OPTIONS_GHC -Wunused-binds #-}",
                "module A () where",
                "a :: Map"
            ]
        _ <- waitForDiagnostics
        compls <- getCompletions doc (Position 2 7)
        let compls' =
              [T.drop 1 $ T.dropEnd 3 d
              | CompletionItem {_documentation = Just (InR (MarkupContent MarkupKind_Markdown d)), _label}
                <- compls
              , _label == "Map"
              ]
        liftIO $ take 3 (sort compls') @?=
          map ("Defined in "<>)
              [ "'Data.Map"
              , "'Data.Map.Lazy"
              , "'Data.Map.Strict"
              ]
  , testSessionWait "no duplicates" $ do
        doc <- createDoc "A.hs" "haskell" $ T.unlines
            [ "{-# OPTIONS_GHC -Wunused-binds #-}",
                "module A () where",
                "import GHC.Exts(fromList)",
                "a = fromList"
            ]
        _ <- waitForDiagnostics
        compls <- getCompletions doc (Position 3 13)
        let duplicate =
              filter
                (\case
                  CompletionItem
                    { _insertText = Just "fromList"
                    , _documentation =
                      Just (InR (MarkupContent MarkupKind_Markdown d))
                    } ->
                    "GHC.Exts" `T.isInfixOf` d
                  _ -> False
                ) compls
        liftIO $ length duplicate @?= 1

  , testSessionWait "non-local before global" $ do
    -- non local completions are more specific
        doc <- createDoc "A.hs" "haskell" $ T.unlines
            [ "{-# OPTIONS_GHC -Wunused-binds #-}",
                "module A () where",
                "import GHC.Exts(fromList)",
                "a = fromList"
            ]
        _ <- waitForDiagnostics
        compls <- getCompletions doc (Position 3 13)
        let compls' =
              [_insertText
              | CompletionItem {_label, _insertText} <- compls
              , _label == "fromList"
              ]
        liftIO $ take 3 compls' @?=
          map Just ["fromList"]
  ]

projectCompletionTests :: [TestTree]
projectCompletionTests =
    [ testSession' "from hiedb" $ \dir-> do
        liftIO $ writeFile (dir </> "hie.yaml")
            "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"A\", \"B\"]}}"
        _ <- createDoc "A.hs" "haskell" $ T.unlines
            [  "module A (anidentifier) where",
               "anidentifier = ()"
            ]
        _ <- waitForDiagnostics
        -- Note that B does not import A
        doc <- createDoc "B.hs" "haskell" $ T.unlines
            [ "module B where",
              "b = anidenti"
            ]
        compls <- getCompletions doc (Position 1 10)
        let compls' =
              [T.drop 1 $ T.dropEnd 3 d
              | CompletionItem {_documentation = Just (InR (MarkupContent MarkupKind_Markdown d)), _label}
                <- compls
              , _label == "anidentifier"
              ]
        liftIO $ compls' @?= ["Defined in 'A"],
      testSession' "auto complete project imports" $ \dir-> do
        liftIO $ writeFile (dir </> "hie.yaml")
            "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"ALocalModule\", \"B\"]}}"
        _ <- createDoc "ALocalModule.hs" "haskell" $ T.unlines
            [  "module ALocalModule (anidentifier) where",
               "anidentifier = ()"
            ]
        _ <- waitForDiagnostics
        -- Note that B does not import A
        doc <- createDoc "B.hs" "haskell" $ T.unlines
            [ "module B where",
              "import ALocal"
            ]
        compls <- getCompletions doc (Position 1 13)
        let item = head $ filter ((== "ALocalModule") . (^. L.label)) compls
        liftIO $ do
          item ^. L.label @?= "ALocalModule",
      testSession' "auto complete functions from qualified imports without alias" $ \dir-> do
        liftIO $ writeFile (dir </> "hie.yaml")
            "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"A\", \"B\"]}}"
        _ <- createDoc "A.hs" "haskell" $ T.unlines
            [  "module A (anidentifier) where",
               "anidentifier = ()"
            ]
        _ <- waitForDiagnostics
        doc <- createDoc "B.hs" "haskell" $ T.unlines
            [ "module B where",
              "import qualified A",
              "A."
            ]
        compls <- getCompletions doc (Position 2 2)
        let item = head compls
        liftIO $ do
          item ^. L.label @?= "anidentifier",
      testSession' "auto complete functions from qualified imports with alias" $ \dir-> do
        liftIO $ writeFile (dir </> "hie.yaml")
            "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"A\", \"B\"]}}"
        _ <- createDoc "A.hs" "haskell" $ T.unlines
            [  "module A (anidentifier) where",
               "anidentifier = ()"
            ]
        _ <- waitForDiagnostics
        doc <- createDoc "B.hs" "haskell" $ T.unlines
            [ "module B where",
              "import qualified A as Alias",
              "foo = Alias."
            ]
        compls <- getCompletions doc (Position 2 12)
        let item = head compls
        liftIO $ do
          item ^. L.label @?= "anidentifier"
    ]

completionDocTests :: [TestTree]
completionDocTests =
  [ testSession "local define" $ do
      doc <- createDoc "A.hs" "haskell" $ T.unlines
        [ "module A where"
        , "foo = ()"
        , "bar = fo"
        ]
      let expected = "*Defined at line 2, column 1 in this module*\n"
      test doc (Position 2 8) "foo" Nothing [expected]
  , testSession "local empty doc" $ do
      doc <- createDoc "A.hs" "haskell" $ T.unlines
        [ "module A where"
        , "foo = ()"
        , "bar = fo"
        ]
      test doc (Position 2 8) "foo" Nothing ["*Defined at line 2, column 1 in this module*\n"]
  , testSession "local single line doc without newline" $ do
      doc <- createDoc "A.hs" "haskell" $ T.unlines
        [ "module A where"
        , "-- |docdoc"
        , "foo = ()"
        , "bar = fo"
        ]
      test doc (Position 3 8) "foo" Nothing ["*Defined at line 3, column 1 in this module*\n* * *\n\n\ndocdoc\n"]
  , testSession "local multi line doc with newline" $ do
      doc <- createDoc "A.hs" "haskell" $ T.unlines
        [ "module A where"
        , "-- | abcabc"
        , "--"
        , "foo = ()"
        , "bar = fo"
        ]
      test doc (Position 4 8) "foo" Nothing ["*Defined at line 4, column 1 in this module*\n* * *\n\n\nabcabc\n"]
  , testSession "local multi line doc without newline" $ do
      doc <- createDoc "A.hs" "haskell" $ T.unlines
        [ "module A where"
        , "-- |     abcabc"
        , "--"
        , "--def"
        , "foo = ()"
        , "bar = fo"
        ]
      test doc (Position 5 8) "foo" Nothing ["*Defined at line 5, column 1 in this module*\n* * *\n\n\nabcabc \n\ndef\n"]
  , testSession "extern empty doc" $ do
      doc <- createDoc "A.hs" "haskell" $ T.unlines
        [ "module A where"
        , "foo = od"
        ]
      let expected = "*Imported from 'Prelude'*\n"
      test doc (Position 1 8) "odd" (Just $ T.length expected) [expected]
  , brokenForMacGhc9 $ brokenForWinGhc90 $ testSession "extern single line doc without '\\n'" $ do
      doc <- createDoc "A.hs" "haskell" $ T.unlines
        [ "module A where"
        , "foo = no"
        ]
      let expected = "*Imported from 'Prelude'*\n* * *\n\n\nBoolean \"not\"\n"
      test doc (Position 1 8) "not" (Just $ T.length expected) [expected]
  , brokenForMacGhc9 $ brokenForWinGhc90 $ testSession "extern mulit line doc" $ do
      doc <- createDoc "A.hs" "haskell" $ T.unlines
        [ "module A where"
        , "foo = i"
        ]
      let expected = "*Imported from 'Prelude'*\n* * *\n\n\nIdentity function. \n```haskell\nid x = x\n```\n"
      test doc (Position 1 7) "id" (Just $ T.length expected) [expected]
  , testSession "extern defined doc" $ do
      doc <- createDoc "A.hs" "haskell" $ T.unlines
        [ "module A where"
        , "foo = i"
        ]
      let expected = "*Imported from 'Prelude'*\n"
      test doc (Position 1 7) "id" (Just $ T.length expected) [expected]
  ]
  where
    brokenForGhc9 = knownBrokenFor (BrokenForGHC [GHC90, GHC92, GHC94, GHC96]) "Completion doc doesn't support ghc9"
    brokenForWinGhc90 = knownBrokenFor (BrokenSpecific Windows [GHC90]) "Extern doc doesn't support Windows for ghc9.2"
    -- https://gitlab.haskell.org/ghc/ghc/-/issues/20903
    brokenForMacGhc9 = knownBrokenFor (BrokenSpecific MacOS [GHC90, GHC92, GHC94, GHC96]) "Extern doc doesn't support MacOS for ghc9"
    test doc pos label mn expected = do
      _ <- waitForDiagnostics
      compls <- getCompletions doc pos
      rcompls <- forM compls $ \item -> do
            if isJust (item ^. L.data_)
            then do
                rsp <- request SMethod_CompletionItemResolve item
                case rsp ^. L.result of
                    Left err -> liftIO $ assertFailure ("completionItem/resolve failed with: " <> show err)
                    Right x -> pure x
            else pure item
      let compls' = [
            -- We ignore doc uris since it points to the local path which determined by specific machines
            case mn of
                Nothing -> txt
                Just n  -> T.take n txt
            | CompletionItem {_documentation = Just (InR (MarkupContent MarkupKind_Markdown txt)), ..} <- rcompls
            , _label == label
            ]
      liftIO $ compls' @?= expected