packages feed

haskell-language-server-2.15.0.0: ghcide-test/exe/EpsPollutionTests.hs

-- | Regression test: edits that break typechecking can leave HLS's
-- shared 'ExternalPackageState' ('EPS') polluted with interfaces and
-- instances from /home-package/ modules. The next successful typecheck
-- of a module that also legitimately has those home modules in its HPT
-- reports \"Overlapping instance\" with both matches pointing at the
-- same source location, because 'tcGetInstEnvs' returns the same
-- 'ClsInst' twice (once via @ie_global@ from the EPS, once via
-- @ie_local@ from 'hptInstancesBelow').
--
-- The pollution entered through 'Development.IDE.Spans.Documentation.mkDocMap'.
-- Its 'Rules.GetDocMap' rule read three inputs via independent
-- @useWithStale_@ calls: 'TypeCheck', 'GhcSessionDeps' and 'GetHieAst'.
-- These three can diverge: an edit that merely changes imports lets
-- 'GhcSessionDeps' re-evaluate (fresh, with a different HPT) while
-- 'TypeCheck' and 'GetHieAst' fall back to their last-successful values.
-- If the stale 'RefMap' references a name whose module is no longer in
-- the fresh HPT, 'mkDocMap' asks 'getDocsBatch' for its docs;
-- 'loadSysInterface' does not find the module in the HUG and calls
-- 'loadInterface', which puts the home-module interface -- /with its
-- instance environment/ -- into the shared EPS @IORef@. The EPS never
-- evicts anything, so the pollution is permanent for the session.
module EpsPollutionTests (tests) where

import           Config                      (Expect (ExpectHoverText),
                                              runWithExtraFiles)
import           Control.Lens                ((^.))
import           Control.Monad               (void)
import qualified Data.Text                   as T
import           Development.IDE.GHC.Util    (readFileUtf8)
import           Hover
import qualified Language.LSP.Protocol.Lens  as L
import           Language.LSP.Protocol.Types
import           Language.LSP.Test
import           System.FilePath
import           Test.Hls

tests :: TestTree
tests = testGroup "eps-pollution"
    [ staleHieProvokesOverlapping
    ]

-- The fixture at ghcide-test/data/multi-unit-eps-pollution/ sets up two
-- home units: unit @a@ provides module @A@ which defines @MyClass@ and
-- @instance MyClass AType@; unit @c@ provides module @C@ which imports
-- @A@ and uses @myMethod@ on an @AType@ value (forcing instance
-- resolution).

staleHieProvokesOverlapping :: TestTree
staleHieProvokesOverlapping =
  testCase "Stale RefMap must not provoke overlapping-instance error" $
  runWithExtraFiles "multi-unit-eps-pollution" $ \dir -> do
    let cPath = dir </> "c" </> "C.hs"
    originalC <- liftIO $ readFileUtf8 cPath
    let brokenC = T.replace "import A" "" originalC
    cdoc <- openDoc cPath "haskell"
    void $ waitForTypecheck cdoc
    -- Hovering triggers the hover pipeline, which forces GetDocMap.
    -- While C is healthy this populates GetHieAst with a RefMap
    -- referencing A's names -- the stale value we rely on below.
    hover <- getHover cdoc (hoverOnMyMethod originalC)
    checkHover hover [ExpectHoverText ["myMethod", "MyClass"]]

    -- Break C's import of A. C fails to typecheck, but GhcSessionDeps
    -- re-evaluates successfully (it only needs the import list) with an
    -- HPT that no longer contains A. A further hover forces GetDocMap
    -- to run with the fresh GhcSessionDeps alongside the stale RefMap;
    -- loadSysInterface(A) then runs and pollutes the EPS.
    changeDoc cdoc [TextDocumentContentChangeEvent . InR .
      TextDocumentContentChangeWholeDocument $ brokenC]

    void $ waitForDiagnosticsFrom cdoc
    void $ getHover cdoc (hoverOnMyMethod brokenC)
    -- Repair C. The next typecheck legitimately has A in its HPT; with
    -- the polluted EPS it also has A's ClsInst in eps_inst_env, so
    -- instance resolution for 'myMethod x :: AType -> String' finds
    -- two matches with identical source locations.
    changeDoc cdoc [TextDocumentContentChangeEvent . InR .
      TextDocumentContentChangeWholeDocument $ originalC]
    diags <- waitForDiagnosticsFrom cdoc
    liftIO $ assertBool
      ("Expected no overlapping-instance errors, got diagnostics:\n"
        ++ unlines (map (T.unpack . (^. L.message)) diags))
      (not (any isOverlappingInstance diags))
  where
    isOverlappingInstance d =
      "Overlapping instance" `T.isInfixOf` (d ^. L.message)

-- | 'Position' at the first occurrence of @myMethod@ in the given source.
--   Computed rather than hard-coded because the broken variant has one
--   fewer line than the original.
hoverOnMyMethod :: T.Text -> Position
hoverOnMyMethod src =
    case [ Position row (fromIntegral (T.length prefix))
         | (row, line) <- zip [0..] (T.lines src)
         , let (prefix, rest) = T.breakOn "myMethod" line
         , not (T.null rest)
         ] of
      p : _ -> p
      []    -> error "hoverOnMyMethod: no occurrence of 'myMethod'"