Skip to content
This repository has been archived by the owner on Jan 2, 2021. It is now read-only.

Fix diagnostics update bug #959

Merged
merged 13 commits into from
Dec 21, 2020
8 changes: 7 additions & 1 deletion bench/lib/Experiments.hs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,13 @@ experiments =
)
( \p doc -> do
changeDoc doc [hygienicEdit]
whileM (null <$> waitForDiagnostics)
waitForProgressDone
-- NOTE ghcide used to clear and reinstall the diagnostics here
-- new versions no longer do, but keep this logic around
-- to benchmark old versions sucessfully
diags <- getCurrentDiagnostics doc
when (null diags) $
whileM (null <$> waitForDiagnostics)
not . null <$> getCodeActions doc (Range p p)
)
]
Expand Down
2 changes: 2 additions & 0 deletions exe/Arguments.hs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ data Arguments = Arguments
,argsShakeProfiling :: Maybe FilePath
,argsOTMemoryProfiling :: Bool
,argsTesting :: Bool
,argsDisableKick :: Bool
,argsThreads :: Int
,argsVerbose :: Bool
}
Expand All @@ -35,5 +36,6 @@ arguments = Arguments
<*> optional (strOption $ long "shake-profiling" <> metavar "DIR" <> help "Dump profiling reports to this directory")
<*> switch (long "ot-memory-profiling" <> help "Record OpenTelemetry info to the eventlog. Needs the -l RTS flag to have an effect")
<*> switch (long "test" <> help "Enable additional lsp messages used by the testsuite")
<*> switch (long "test-no-kick" <> help "Disable kick. Useful for testing cancellation")
<*> option auto (short 'j' <> help "Number of threads (0: automatic)" <> metavar "NUM" <> value 0 <> showDefault)
<*> switch (long "verbose" <> help "Include internal events in logging output")
10 changes: 9 additions & 1 deletion exe/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,15 @@ main = do
}
logLevel = if argsVerbose then minBound else Info
debouncer <- newAsyncDebouncer
initialise caps (mainRule >> pluginRules plugins >> action kick)
let rules = do
-- install the main and ghcide-plugin rules
mainRule
pluginRules plugins
-- install the kick action, which triggers a typecheck on every
-- Shake database restart, i.e. on every user edit.
unless argsDisableKick $
action kick
initialise caps rules
getLspId event wProg wIndefProg (logger logLevel) debouncer options vfs
else do
-- GHC produces messages with UTF8 in them, so make sure the terminal doesn't error
Expand Down
1 change: 1 addition & 0 deletions ghcide.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ library
base == 4.*,
binary,
bytestring,
case-insensitive,
containers,
data-default,
deepseq,
Expand Down
1 change: 0 additions & 1 deletion src/Development/IDE.hs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import Development.IDE.Core.Shake as X
ShakeExtras,
IdeRule,
define, defineEarlyCutoff,
GetModificationTime(GetModificationTime),
use, useNoFile, uses, useWithStale, useWithStaleFast, useWithStaleFast',
FastResult(..),
use_, useNoFile_, uses_, useWithStale_,
Expand Down
10 changes: 0 additions & 10 deletions src/Development/IDE/Core/FileStore.hs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import Control.Monad.Extra
import Development.Shake
import Development.Shake.Classes
import Control.Exception
import GHC.Generics
import Data.Either.Extra
import Data.Int (Int64)
import Data.Time
Expand Down Expand Up @@ -100,15 +99,6 @@ isFileOfInterestRule = defineEarlyCutoff $ \IsFileOfInterest f -> do
let res = maybe NotFOI IsFOI $ f `HM.lookup` filesOfInterest
return (Just $ BS.pack $ show $ hash res, ([], Just res))

-- | Get the contents of a file, either dirty (if the buffer is modified) or Nothing to mean use from disk.
type instance RuleResult GetFileContents = (FileVersion, Maybe T.Text)

data GetFileContents = GetFileContents
deriving (Eq, Show, Generic)
instance Hashable GetFileContents
instance NFData GetFileContents
instance Binary GetFileContents

getModificationTimeRule :: VFSHandle -> Rules ()
getModificationTimeRule vfs =
defineEarlyCutoff $ \(GetModificationTime_ missingFileDiags) file -> do
Expand Down
52 changes: 52 additions & 0 deletions src/Development/IDE/Core/RuleTypes.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
-- SPDX-License-Identifier: Apache-2.0

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DerivingStrategies #-}

Expand Down Expand Up @@ -37,6 +38,8 @@ import Language.Haskell.LSP.Types (NormalizedFilePath)
import TcRnMonad (TcGblEnv)
import qualified Data.ByteString.Char8 as BS
import Development.IDE.Types.Options (IdeGhcSession)
import Data.Text (Text)
import Data.Int (Int64)

data LinkableType = ObjectLinkable | BCOLinkable
deriving (Eq,Ord,Show)
Expand Down Expand Up @@ -190,6 +193,55 @@ type instance RuleResult GetModIface = HiFileResult
-- For better early cuttoff
type instance RuleResult GetModIfaceWithoutLinkable = HiFileResult

-- | Get the contents of a file, either dirty (if the buffer is modified) or Nothing to mean use from disk.
type instance RuleResult GetFileContents = (FileVersion, Maybe Text)

-- The Shake key type for getModificationTime queries
data GetModificationTime = GetModificationTime_
{ missingFileDiagnostics :: Bool
-- ^ If false, missing file diagnostics are not reported
}
deriving (Show, Generic)

instance Eq GetModificationTime where
-- Since the diagnostics are not part of the answer, the query identity is
-- independent from the 'missingFileDiagnostics' field
_ == _ = True

instance Hashable GetModificationTime where
-- Since the diagnostics are not part of the answer, the query identity is
-- independent from the 'missingFileDiagnostics' field
hashWithSalt salt _ = salt

instance NFData GetModificationTime
instance Binary GetModificationTime

pattern GetModificationTime :: GetModificationTime
pattern GetModificationTime = GetModificationTime_ {missingFileDiagnostics=True}

-- | Get the modification time of a file.
type instance RuleResult GetModificationTime = FileVersion

data FileVersion
= VFSVersion !Int
| ModificationTime
!Int64 -- ^ Large unit (platform dependent, do not make assumptions)
!Int64 -- ^ Small unit (platform dependent, do not make assumptions)
deriving (Show, Generic)

instance NFData FileVersion

vfsVersion :: FileVersion -> Maybe Int
vfsVersion (VFSVersion i) = Just i
vfsVersion ModificationTime{} = Nothing

data GetFileContents = GetFileContents
deriving (Eq, Show, Generic)
instance Hashable GetFileContents
instance NFData GetFileContents
instance Binary GetFileContents


data FileOfInterestStatus = OnDisk | Modified
deriving (Eq, Show, Typeable, Generic)
instance Hashable FileOfInterestStatus
Expand Down
14 changes: 8 additions & 6 deletions src/Development/IDE/Core/Rules.hs
Original file line number Diff line number Diff line change
Expand Up @@ -734,24 +734,26 @@ getModSummaryRule = do
getModSummaryFromImports session fp modTime (textToStringBuffer <$> mFileContent)
case modS of
Right res@(ms,_) -> do
let fingerPrint = hash (computeFingerprint f dflags ms, hashUTC modTime)
let fingerPrint = hash (computeFingerprint f (fromJust $ ms_hspp_buf ms) dflags ms, hashUTC modTime)
return ( Just (BS.pack $ show fingerPrint) , ([], Just res))
Left diags -> return (Nothing, (diags, Nothing))

defineEarlyCutoff $ \GetModSummaryWithoutTimestamps f -> do
ms <- use GetModSummary f
case ms of
Just res@(msWithTimestamps,_) -> do
let ms = msWithTimestamps { ms_hs_date = error "use GetModSummary instead of GetModSummaryWithoutTimestamps" }
let ms = msWithTimestamps {
ms_hs_date = error "use GetModSummary instead of GetModSummaryWithoutTimestamps",
ms_hspp_buf = error "use GetModSummary instead of GetModSummaryWithoutTimestamps"
wz1000 marked this conversation as resolved.
Show resolved Hide resolved
}
dflags <- hsc_dflags . hscEnv <$> use_ GhcSession f
-- include the mod time in the fingerprint
let fp = BS.pack $ show $ hash (computeFingerprint f dflags ms)
let fp = BS.pack $ show $ hash (computeFingerprint f (fromJust $ ms_hspp_buf msWithTimestamps) dflags ms)
return (Just fp, ([], Just res))
Nothing -> return (Nothing, ([], Nothing))
where
-- Compute a fingerprint from the contents of `ModSummary`,
-- eliding the timestamps and other non relevant fields.
computeFingerprint f dflags ModSummary{..} =
computeFingerprint f sb dflags ModSummary{..} =
let fingerPrint =
( moduleNameString (moduleName ms_mod)
, ms_hspp_file
Expand All @@ -761,7 +763,7 @@ getModSummaryRule = do
, fingerPrintImports ms_textual_imps
)
fingerPrintImports = map (fmap uniq *** (moduleNameString . unLoc))
opts = Hdr.getOptions dflags (fromJust ms_hspp_buf) (fromNormalizedFilePath f)
opts = Hdr.getOptions dflags sb (fromNormalizedFilePath f)
in fingerPrint

hashUTC UTCTime{..} = (fromEnum utctDay, fromEnum utctDayTime)
Expand Down
2 changes: 1 addition & 1 deletion src/Development/IDE/Core/Service.hs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module Development.IDE.Core.Service(
IdeState, initialise, shutdown,
runAction,
writeProfile,
getDiagnostics, unsafeClearDiagnostics,
getDiagnostics,
ideLogger,
updatePositionMapping,
) where
Expand Down
100 changes: 28 additions & 72 deletions src/Development/IDE/Core/Shake.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
{-# LANGUAGE RecursiveDo #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE PatternSynonyms #-}

-- | A Shake implementation of the compiler service.
--
Expand Down Expand Up @@ -38,7 +37,7 @@ module Development.IDE.Core.Shake(
useWithStale, usesWithStale,
useWithStale_, usesWithStale_,
define, defineEarlyCutoff, defineOnDisk, needOnDisk, needOnDisks,
getDiagnostics, unsafeClearDiagnostics,
getDiagnostics,
getHiddenDiagnostics,
IsIdeGlobal, addIdeGlobal, addIdeGlobalExtras, getIdeGlobalState, getIdeGlobalAction,
getIdeGlobalExtras,
Expand Down Expand Up @@ -84,6 +83,7 @@ import Development.IDE.Core.Debouncer
import Development.IDE.GHC.Compat (NameCacheUpdater(..), upNameCache )
import Development.IDE.GHC.Orphans ()
import Development.IDE.Core.PositionMapping
import Development.IDE.Core.RuleTypes
import Development.IDE.Types.Action
import Development.IDE.Types.Logger hiding (Priority)
import Development.IDE.Types.KnownTargets
Expand Down Expand Up @@ -124,7 +124,6 @@ import Data.IORef
import NameCache
import UniqSupply
import PrelInfo
import Data.Int (Int64)
import Language.Haskell.LSP.Types.Capabilities
import OpenTelemetry.Eventlog

Expand Down Expand Up @@ -502,7 +501,7 @@ shakeShut IdeState{..} = withMVar shakeSession $ \runner -> do
-- | This is a variant of withMVar where the first argument is run unmasked and if it throws
-- an exception, the previous value is restored while the second argument is executed masked.
withMVar' :: MVar a -> (a -> IO b) -> (b -> IO (a, c)) -> IO c
withMVar' var unmasked masked = mask $ \restore -> do
withMVar' var unmasked masked = uninterruptibleMask $ \restore -> do
a <- takeMVar var
b <- restore (unmasked a) `onException` putMVar var a
(a', c) <- masked b
Expand Down Expand Up @@ -652,11 +651,6 @@ getHiddenDiagnostics IdeState{shakeExtras = ShakeExtras{hiddenDiagnostics}} = do
val <- readVar hiddenDiagnostics
return $ getAllDiagnostics val

-- | FIXME: This function is temporary! Only required because the files of interest doesn't work
unsafeClearDiagnostics :: IdeState -> IO ()
unsafeClearDiagnostics IdeState{shakeExtras = ShakeExtras{diagnostics}} =
writeVar diagnostics mempty

-- | Clear the results for all files that do not match the given predicate.
garbageCollect :: (NormalizedFilePath -> Bool) -> Action ()
garbageCollect keep = do
Expand Down Expand Up @@ -998,25 +992,19 @@ updateFileDiagnostics :: MonadIO m
updateFileDiagnostics fp k ShakeExtras{diagnostics, hiddenDiagnostics, publishedDiagnostics, state, debouncer, eventer} current = liftIO $ do
modTime <- (currentValue =<<) <$> getValues state GetModificationTime fp
let (currentShown, currentHidden) = partition ((== ShowDiag) . fst) current
uri = filePathToUri' fp
ver = vfsVersion =<< modTime
updateDiagnosticsWithForcing new store = do
store' <- evaluate $ setStageDiagnostics uri ver (T.pack $ show k) new store
new' <- evaluate $ getUriDiagnostics uri store'
return (store', new')
mask_ $ do
-- Mask async exceptions to ensure that updated diagnostics are always
-- published. Otherwise, we might never publish certain diagnostics if
-- an exception strikes between modifyVar but before
-- publishDiagnosticsNotification.
newDiags <- modifyVar diagnostics $ \old -> do
let newDiagsStore = setStageDiagnostics fp (vfsVersion =<< modTime)
(T.pack $ show k) (map snd currentShown) old
let newDiags = getFileDiagnostics fp newDiagsStore
_ <- evaluate newDiagsStore
_ <- evaluate newDiags
pure (newDiagsStore, newDiags)
modifyVar_ hiddenDiagnostics $ \old -> do
let newDiagsStore = setStageDiagnostics fp (vfsVersion =<< modTime)
(T.pack $ show k) (map snd currentHidden) old
let newDiags = getFileDiagnostics fp newDiagsStore
_ <- evaluate newDiagsStore
_ <- evaluate newDiags
return newDiagsStore
newDiags <- modifyVar diagnostics $ updateDiagnosticsWithForcing $ map snd currentShown
_ <- modifyVar hiddenDiagnostics $ updateDiagnosticsWithForcing $ map snd currentHidden
let uri = filePathToUri' fp
let delay = if null newDiags then 0.1 else 0
registerEvent debouncer delay uri $ do
Expand Down Expand Up @@ -1051,77 +1039,45 @@ actionLogger = do
return logger


-- The Shake key type for getModificationTime queries
data GetModificationTime = GetModificationTime_
{ missingFileDiagnostics :: Bool
-- ^ If false, missing file diagnostics are not reported
}
deriving (Show, Generic)

instance Eq GetModificationTime where
-- Since the diagnostics are not part of the answer, the query identity is
-- independent from the 'missingFileDiagnostics' field
_ == _ = True

instance Hashable GetModificationTime where
-- Since the diagnostics are not part of the answer, the query identity is
-- independent from the 'missingFileDiagnostics' field
hashWithSalt salt _ = salt

instance NFData GetModificationTime
instance Binary GetModificationTime

pattern GetModificationTime :: GetModificationTime
pattern GetModificationTime = GetModificationTime_ {missingFileDiagnostics=True}

-- | Get the modification time of a file.
type instance RuleResult GetModificationTime = FileVersion

data FileVersion
= VFSVersion !Int
| ModificationTime
!Int64 -- ^ Large unit (platform dependent, do not make assumptions)
!Int64 -- ^ Small unit (platform dependent, do not make assumptions)
deriving (Show, Generic)

instance NFData FileVersion

vfsVersion :: FileVersion -> Maybe Int
vfsVersion (VFSVersion i) = Just i
vfsVersion ModificationTime{} = Nothing

getDiagnosticsFromStore :: StoreItem -> [Diagnostic]
getDiagnosticsFromStore (StoreItem _ diags) = concatMap SL.fromSortedList $ Map.elems diags


-- | Sets the diagnostics for a file and compilation step
-- if you want to clear the diagnostics call this with an empty list
setStageDiagnostics
:: NormalizedFilePath
:: NormalizedUri
-> TextDocumentVersion -- ^ the time that the file these diagnostics originate from was last edited
-> T.Text
-> [LSP.Diagnostic]
-> DiagnosticStore
-> DiagnosticStore
setStageDiagnostics fp timeM stage diags ds =
updateDiagnostics ds uri timeM diagsBySource
where
diagsBySource = Map.singleton (Just stage) (SL.toSortedList diags)
uri = filePathToUri' fp
setStageDiagnostics uri ver stage diags ds = newDiagsStore where
-- When 'ver' is a new version, updateDiagnostics throws away diagnostics from all stages
-- This interacts bady with early cutoff, so we make sure to preserve diagnostics
-- from other stages when calling updateDiagnostics
-- But this means that updateDiagnostics cannot be called concurrently
-- for different stages anymore
updatedDiags = Map.insert (Just stage) (SL.toSortedList diags) oldDiags
oldDiags = case HMap.lookup uri ds of
Just (StoreItem _ byStage) -> byStage
_ -> Map.empty
newDiagsStore = updateDiagnostics ds uri ver updatedDiags


getAllDiagnostics ::
DiagnosticStore ->
[FileDiagnostic]
getAllDiagnostics =
concatMap (\(k,v) -> map (fromUri k,ShowDiag,) $ getDiagnosticsFromStore v) . HMap.toList

getFileDiagnostics ::
NormalizedFilePath ->
getUriDiagnostics ::
NormalizedUri ->
DiagnosticStore ->
[LSP.Diagnostic]
getFileDiagnostics fp ds =
getUriDiagnostics uri ds =
maybe [] getDiagnosticsFromStore $
HMap.lookup (filePathToUri' fp) ds
HMap.lookup uri ds

filterDiagnostics ::
(NormalizedFilePath -> Bool) ->
Expand Down
Loading