Skip to content

Commit b4589ae

Browse files
wz1000mpickeringpepeiborralukel97
authored
Typecheck entire project on Initial Load and typecheck reverse dependencies of a file on saving (#688)
* Add new command to GetModuleGraph for a session and propate changes to modules Only propagate changes to parent modules when saving Typecheck files when they are opened, don't TC FOI Add known files rule Don't save ifaces for files with defered errors Co-authored-by: Zubin Duggal <zubin@cmi.ac.in> * Add configuration for parent typechecking * hlint ignore * Use targets to filter located imports (#10) * Use targets to filter located imports * Remove import paths from the GHC session Otherwise GHC will prioritize source files found in the import path * Update session-loader/Development/IDE/Session.hs Co-authored-by: Pepe Iborra <pepeiborra@me.com> * Add session-loader to hie.yaml (#714) * move known files rule to RuleTypes * Disable checkParents on open and close document (#12) * Really disable expensive checkParents * Add an option to check parents on close Co-authored-by: Matthew Pickering <matthewtpickering@gmail.com> Co-authored-by: Pepe Iborra <pepeiborra@me.com> Co-authored-by: Luke Lau <luke_lau@icloud.com>
1 parent 6128c74 commit b4589ae

File tree

16 files changed

+280
-70
lines changed

16 files changed

+280
-70
lines changed

README.md

+18
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,24 @@ If you can't get `ghcide` working outside the editor, see [this setup troublesho
9595

9696
`ghcide` has been designed to handle projects with hundreds or thousands of modules. If `ghci` can handle it, then `ghcide` should be able to handle it. The only caveat is that this currently requires GHC >= 8.6, and that the first time a module is loaded in the editor will trigger generation of support files in the background if those do not already exist.
9797

98+
### Configuration
99+
100+
`ghcide` accepts the following lsp configuration options:
101+
102+
```typescript
103+
{
104+
// When to check the dependents of a module
105+
// AlwaysCheck means retypechecking them on every change
106+
// CheckOnSave means dependent/parent modules will only be checked when you save
107+
// "CheckOnSaveAndClose" by default
108+
checkParents : "NeverCheck" | "CheckOnClose" | "CheckOnSaveAndClose" | "AlwaysCheck" | ,
109+
// Whether to check the entire project on initial load
110+
// true by default
111+
checkProject : boolean
112+
113+
}
114+
```
115+
98116
### Using with VS Code
99117

100118
You can install the VSCode extension from the [VSCode

exe/Main.hs

+14-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module Main(main) where
88
import Arguments
99
import Control.Concurrent.Extra
1010
import Control.Monad.Extra
11+
import Control.Lens ( (^.) )
1112
import Data.Default
1213
import Data.List.Extra
1314
import Data.Maybe
@@ -33,6 +34,7 @@ import Development.IDE.Session
3334
import qualified Language.Haskell.LSP.Core as LSP
3435
import Language.Haskell.LSP.Messages
3536
import Language.Haskell.LSP.Types
37+
import Language.Haskell.LSP.Types.Lens (params, initializationOptions)
3638
import Development.IDE.LSP.LanguageServer
3739
import qualified System.Directory.Extra as IO
3840
import System.Environment
@@ -44,6 +46,7 @@ import System.Time.Extra
4446
import Paths_ghcide
4547
import Development.GitRev
4648
import qualified Data.HashSet as HashSet
49+
import qualified Data.Aeson as J
4750

4851
import HIE.Bios.Cradle
4952

@@ -78,8 +81,13 @@ main = do
7881
command <- makeLspCommandId "typesignature.add"
7982

8083
let plugins = Completions.plugin <> CodeAction.plugin
81-
onInitialConfiguration = const $ Right ()
82-
onConfigurationChange = const $ Right ()
84+
onInitialConfiguration :: InitializeRequest -> Either T.Text LspConfig
85+
onInitialConfiguration x = case x ^. params . initializationOptions of
86+
Nothing -> Right defaultLspConfig
87+
Just v -> case J.fromJSON v of
88+
J.Error err -> Left $ T.pack err
89+
J.Success a -> Right a
90+
onConfigurationChange = const $ Left "Updating Not supported"
8391
options = def { LSP.executeCommandCommands = Just [command]
8492
, LSP.completionTriggerCharacters = Just "."
8593
}
@@ -88,15 +96,18 @@ main = do
8896
t <- offsetTime
8997
hPutStrLn stderr "Starting LSP server..."
9098
hPutStrLn stderr "If you are seeing this in a terminal, you probably should have run ghcide WITHOUT the --lsp option!"
91-
runLanguageServer options (pluginHandler plugins) onInitialConfiguration onConfigurationChange $ \getLspId event vfs caps wProg wIndefProg -> do
99+
runLanguageServer options (pluginHandler plugins) onInitialConfiguration onConfigurationChange $ \getLspId event vfs caps wProg wIndefProg getConfig -> do
92100
t <- t
93101
hPutStrLn stderr $ "Started LSP server in " ++ showDuration t
94102
sessionLoader <- loadSession dir
103+
config <- fromMaybe defaultLspConfig <$> getConfig
95104
let options = (defaultIdeOptions sessionLoader)
96105
{ optReportProgress = clientSupportsProgress caps
97106
, optShakeProfiling = argsShakeProfiling
98107
, optTesting = IdeTesting argsTesting
99108
, optThreads = argsThreads
109+
, optCheckParents = checkParents config
110+
, optCheckProject = checkProject config
100111
}
101112
logLevel = if argsVerbose then minBound else Info
102113
debouncer <- newAsyncDebouncer

ghcide.cabal

+2
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ executable ghcide
263263
"-with-rtsopts=-I0 -qg -A128M"
264264
main-is: Main.hs
265265
build-depends:
266+
aeson,
266267
base == 4.*,
267268
data-default,
268269
directory,
@@ -274,6 +275,7 @@ executable ghcide
274275
haskell-lsp-types,
275276
hie-bios >= 0.6.0 && < 0.7,
276277
ghcide,
278+
lens,
277279
optparse-applicative,
278280
text,
279281
unordered-containers

session-loader/Development/IDE/Session.hs

+33-13
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,16 @@ import Data.Bifunctor
2525
import qualified Data.ByteString.Base16 as B16
2626
import Data.Either.Extra
2727
import Data.Function
28+
import qualified Data.HashSet as HashSet
29+
import Data.Hashable
2830
import Data.List
2931
import Data.IORef
3032
import Data.Maybe
3133
import Data.Time.Clock
3234
import Data.Version
3335
import Development.IDE.Core.OfInterest
3436
import Development.IDE.Core.Shake
37+
import Development.IDE.Core.RuleTypes
3538
import Development.IDE.GHC.Util
3639
import Development.IDE.Session.VersionCheck
3740
import Development.IDE.Types.Diagnostics
@@ -47,6 +50,7 @@ import Language.Haskell.LSP.Core
4750
import Language.Haskell.LSP.Messages
4851
import Language.Haskell.LSP.Types
4952
import System.Directory
53+
import qualified System.Directory.Extra as IO
5054
import System.FilePath
5155
import System.Info
5256
import System.IO
@@ -96,8 +100,10 @@ loadSession dir = do
96100
runningCradle <- newVar dummyAs :: IO (Var (Async (IdeResult HscEnvEq,[FilePath])))
97101

98102
return $ do
99-
ShakeExtras{logger, eventer, restartShakeSession, withIndefiniteProgress, ideNc} <- getShakeExtras
100-
IdeOptions{optTesting = IdeTesting optTesting} <- getIdeOptions
103+
ShakeExtras{logger, eventer, restartShakeSession, withIndefiniteProgress
104+
,ideNc, knownFilesVar, session=ideSession} <- getShakeExtras
105+
106+
IdeOptions{optTesting = IdeTesting optTesting, optCheckProject = CheckProject checkProject } <- getIdeOptions
101107

102108
-- Create a new HscEnv from a hieYaml root and a set of options
103109
-- If the hieYaml file already has an HscEnv, the new component is
@@ -170,7 +176,7 @@ loadSession dir = do
170176

171177

172178
let session :: (Maybe FilePath, NormalizedFilePath, ComponentOptions, FilePath)
173-
-> IO (IdeResult HscEnvEq,[FilePath])
179+
-> IO ([NormalizedFilePath],(IdeResult HscEnvEq,[FilePath]))
174180
session args@(hieYaml, _cfp, _opts, _libDir) = do
175181
(hscEnv, new, old_deps) <- packageSetup args
176182
-- Make a map from unit-id to DynFlags, this is used when trying to
@@ -194,9 +200,9 @@ loadSession dir = do
194200
invalidateShakeCache
195201
restartShakeSession [kick]
196202

197-
return (second Map.keys res)
203+
return (map fst cs ++ map fst cached_targets, second Map.keys res)
198204

199-
let consultCradle :: Maybe FilePath -> FilePath -> IO (IdeResult HscEnvEq, [FilePath])
205+
let consultCradle :: Maybe FilePath -> FilePath -> IO ([NormalizedFilePath], (IdeResult HscEnvEq, [FilePath]))
200206
consultCradle hieYaml cfp = do
201207
when optTesting $ eventer $ notifyCradleLoaded cfp
202208
logInfo logger $ T.pack ("Consulting the cradle for " <> show cfp)
@@ -219,7 +225,7 @@ loadSession dir = do
219225
InstallationNotFound{..} ->
220226
error $ "GHC installation not found in libdir: " <> libdir
221227
InstallationMismatch{..} ->
222-
return (([renderPackageSetupException cfp GhcVersionMismatch{..}], Nothing),[])
228+
return ([],(([renderPackageSetupException cfp GhcVersionMismatch{..}], Nothing),[]))
223229
InstallationChecked _compileTime _ghcLibCheck ->
224230
session (hieYaml, toNormalizedFilePath' cfp, opts, libDir)
225231
-- Failure case, either a cradle error or the none cradle
@@ -229,11 +235,12 @@ loadSession dir = do
229235
let res = (map (renderCradleError ncfp) err, Nothing)
230236
modifyVar_ fileToFlags $ \var -> do
231237
pure $ Map.insertWith HM.union hieYaml (HM.singleton ncfp (res, dep_info)) var
232-
return (res,[])
238+
return ([ncfp],(res,[]))
233239

234240
-- This caches the mapping from hie.yaml + Mod.hs -> [String]
235241
-- Returns the Ghc session and the cradle dependencies
236-
let sessionOpts :: (Maybe FilePath, FilePath) -> IO (IdeResult HscEnvEq, [FilePath])
242+
let sessionOpts :: (Maybe FilePath, FilePath)
243+
-> IO ([NormalizedFilePath], (IdeResult HscEnvEq, [FilePath]))
237244
sessionOpts (hieYaml, file) = do
238245
v <- fromMaybe HM.empty . Map.lookup hieYaml <$> readVar fileToFlags
239246
cfp <- canonicalizePath file
@@ -248,25 +255,38 @@ loadSession dir = do
248255
-- Keep the same name cache
249256
modifyVar_ hscEnvs (return . Map.adjust (\(h, _) -> (h, [])) hieYaml )
250257
consultCradle hieYaml cfp
251-
else return (opts, Map.keys old_di)
258+
else return (HM.keys v, (opts, Map.keys old_di))
252259
Nothing -> consultCradle hieYaml cfp
253260

254261
-- The main function which gets options for a file. We only want one of these running
255262
-- at a time. Therefore the IORef contains the currently running cradle, if we try
256263
-- to get some more options then we wait for the currently running action to finish
257264
-- before attempting to do so.
258-
let getOptions :: FilePath -> IO (IdeResult HscEnvEq, [FilePath])
265+
let getOptions :: FilePath -> IO ([NormalizedFilePath],(IdeResult HscEnvEq, [FilePath]))
259266
getOptions file = do
260267
hieYaml <- cradleLoc file
261268
sessionOpts (hieYaml, file) `catch` \e ->
262-
return (([renderPackageSetupException file e], Nothing),[])
269+
return ([],(([renderPackageSetupException file e], Nothing),[]))
263270

264271
returnWithVersion $ \file -> do
265-
liftIO $ join $ mask_ $ modifyVar runningCradle $ \as -> do
272+
(cs, opts) <- liftIO $ join $ mask_ $ modifyVar runningCradle $ \as -> do
266273
-- If the cradle is not finished, then wait for it to finish.
267274
void $ wait as
268275
as <- async $ getOptions file
269-
return (as, wait as)
276+
return (fmap snd as, wait as)
277+
unless (null cs) $
278+
-- Typecheck all files in the project on startup
279+
void $ shakeEnqueueSession ideSession $ mkDelayedAction "InitialLoad" Debug $ void $ do
280+
cfps' <- liftIO $ filterM (IO.doesFileExist . fromNormalizedFilePath) cs
281+
-- populate the knownFilesVar with all the
282+
-- files in the project so that `knownFiles` can learn about them and
283+
-- we can generate a complete module graph
284+
liftIO $ modifyVar_ knownFilesVar $ traverseHashed $ pure . HashSet.union (HashSet.fromList cfps')
285+
mmt <- uses GetModificationTime cfps'
286+
let cs_exist = catMaybes (zipWith (<$) cfps' mmt)
287+
when checkProject $
288+
void $ uses GetModIface cs_exist
289+
pure opts
270290

271291
-- | Run the specific cradle on a specific FilePath via hie-bios.
272292
-- This then builds dependencies or whatever based on the cradle, gets the

src/Development/IDE/Core/Compile.hs

+14-12
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ module Development.IDE.Core.Compile
1919
, mkTcModuleResult
2020
, generateByteCode
2121
, generateAndWriteHieFile
22-
, generateAndWriteHiFile
22+
, writeHiFile
2323
, getModSummaryFromImports
2424
, loadHieFile
2525
, loadInterface
@@ -133,9 +133,10 @@ typecheckModule (IdeDefer defer) hsc pm = do
133133
(warnings, tcm) <- withWarnings "typecheck" $ \tweak ->
134134
GHC.typecheckModule $ enableTopLevelWarnings
135135
$ demoteIfDefer pm{pm_mod_summary = tweak modSummary'}
136-
tcm2 <- mkTcModuleResult tcm
137136
let errorPipeline = unDefer . hideDiag dflags
138-
return (map errorPipeline warnings, tcm2)
137+
diags = map errorPipeline warnings
138+
tcm2 <- mkTcModuleResult tcm (any fst diags)
139+
return (map snd diags, tcm2)
139140
where
140141
demoteIfDefer = if defer then demoteTypeErrorsToWarnings else id
141142

@@ -233,11 +234,11 @@ update_pm_mod_summary :: (ModSummary -> ModSummary) -> ParsedModule -> ParsedMod
233234
update_pm_mod_summary up pm =
234235
pm{pm_mod_summary = up $ pm_mod_summary pm}
235236

236-
unDefer :: (WarnReason, FileDiagnostic) -> FileDiagnostic
237-
unDefer (Reason Opt_WarnDeferredTypeErrors , fd) = upgradeWarningToError fd
238-
unDefer (Reason Opt_WarnTypedHoles , fd) = upgradeWarningToError fd
239-
unDefer (Reason Opt_WarnDeferredOutOfScopeVariables, fd) = upgradeWarningToError fd
240-
unDefer ( _ , fd) = fd
237+
unDefer :: (WarnReason, FileDiagnostic) -> (Bool, FileDiagnostic)
238+
unDefer (Reason Opt_WarnDeferredTypeErrors , fd) = (True, upgradeWarningToError fd)
239+
unDefer (Reason Opt_WarnTypedHoles , fd) = (True, upgradeWarningToError fd)
240+
unDefer (Reason Opt_WarnDeferredOutOfScopeVariables, fd) = (True, upgradeWarningToError fd)
241+
unDefer ( _ , fd) = (False, fd)
241242

242243
upgradeWarningToError :: FileDiagnostic -> FileDiagnostic
243244
upgradeWarningToError (nfp, sh, fd) =
@@ -257,8 +258,9 @@ addRelativeImport fp modu dflags = dflags
257258
mkTcModuleResult
258259
:: GhcMonad m
259260
=> TypecheckedModule
261+
-> Bool
260262
-> m TcModuleResult
261-
mkTcModuleResult tcm = do
263+
mkTcModuleResult tcm upgradedError = do
262264
session <- getSession
263265
let sf = modInfoSafe (tm_checked_module_info tcm)
264266
#if MIN_GHC_API_VERSION(8,10,0)
@@ -267,7 +269,7 @@ mkTcModuleResult tcm = do
267269
(iface, _) <- liftIO $ mkIfaceTc session Nothing sf details tcGblEnv
268270
#endif
269271
let mod_info = HomeModInfo iface details Nothing
270-
return $ TcModuleResult tcm mod_info
272+
return $ TcModuleResult tcm mod_info upgradedError
271273
where
272274
(tcGblEnv, details) = tm_internals_ tcm
273275

@@ -294,8 +296,8 @@ generateAndWriteHieFile hscEnv tcm =
294296
mod_location = ms_location mod_summary
295297
targetPath = Compat.ml_hie_file mod_location
296298

297-
generateAndWriteHiFile :: HscEnv -> TcModuleResult -> IO [FileDiagnostic]
298-
generateAndWriteHiFile hscEnv tc =
299+
writeHiFile :: HscEnv -> TcModuleResult -> IO [FileDiagnostic]
300+
writeHiFile hscEnv tc =
299301
handleGenerationErrors dflags "interface generation" $ do
300302
atomicFileWrite targetPath $ \fp ->
301303
writeIfaceFile dflags fp modIface

src/Development/IDE/Core/FileStore.hs

+26-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module Development.IDE.Core.FileStore(
1111
setSomethingModified,
1212
fileStoreRules,
1313
modificationTime,
14+
typecheckParents,
1415
VFSHandle,
1516
makeVFSHandle,
1617
makeLSPVFSHandle
@@ -37,6 +38,7 @@ import Development.IDE.Types.Location
3738
import Development.IDE.Core.OfInterest (kick)
3839
import Development.IDE.Core.RuleTypes
3940
import qualified Data.Rope.UTF16 as Rope
41+
import Development.IDE.Import.DependencyInformation
4042

4143
#ifdef mingw32_HOST_OS
4244
import qualified System.Directory as Dir
@@ -202,8 +204,14 @@ setBufferModified state absFile contents = do
202204

203205
-- | Note that some buffer for a specific file has been modified but not
204206
-- with what changes.
205-
setFileModified :: IdeState -> NormalizedFilePath -> IO ()
206-
setFileModified state nfp = do
207+
setFileModified :: IdeState
208+
-> Bool -- ^ True indicates that we should also attempt to recompile
209+
-- modules which depended on this file. Currently
210+
-- it is true when saving but not on normal
211+
-- document modification events
212+
-> NormalizedFilePath
213+
-> IO ()
214+
setFileModified state prop nfp = do
207215
VFSHandle{..} <- getIdeGlobalState state
208216
when (isJust setVirtualFileContents) $
209217
fail "setSomethingModified can't be called on this type of VFSHandle"
@@ -213,6 +221,22 @@ setFileModified state nfp = do
213221
void $ use GetSpanInfo nfp
214222
liftIO $ progressUpdate KickCompleted
215223
shakeRestart state [da]
224+
when prop $
225+
typecheckParents state nfp
226+
227+
typecheckParents :: IdeState -> NormalizedFilePath -> IO ()
228+
typecheckParents state nfp = void $ shakeEnqueue state parents
229+
where parents = mkDelayedAction "ParentTC" L.Debug (typecheckParentsAction nfp)
230+
231+
typecheckParentsAction :: NormalizedFilePath -> Action ()
232+
typecheckParentsAction nfp = do
233+
revs <- reverseDependencies nfp <$> useNoFile_ GetModuleGraph
234+
logger <- logger <$> getShakeExtras
235+
let log = L.logInfo logger . T.pack
236+
liftIO $ do
237+
(log $ "Typechecking reverse dependencies for" ++ show nfp ++ ": " ++ show revs)
238+
`catch` \(e :: SomeException) -> log (show e)
239+
() <$ uses GetModIface revs
216240

217241
-- | Note that some buffer somewhere has been modified, but don't say what.
218242
-- Only valid if the virtual file system was initialised by LSP, as that

src/Development/IDE/Core/OfInterest.hs

-2
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,6 @@ modifyFilesOfInterest state f = do
8080
OfInterestVar var <- getIdeGlobalState state
8181
files <- modifyVar var $ pure . dupe . f
8282
logDebug (ideLogger state) $ "Set files of interest to: " <> T.pack (show $ HashSet.toList files)
83-
let das = map (\nfp -> mkDelayedAction "OfInterest" Debug (use GetSpanInfo nfp)) (HashSet.toList files)
84-
shakeRestart state das
8583

8684
-- | Typecheck all the files of interest.
8785
-- Could be improved

0 commit comments

Comments
 (0)