ghcide-0.0.1: src/Development/IDE/Core/Rules.hs
-- Copyright (c) 2019 The DAML Authors. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE DuplicateRecordFields #-}
-- | A Shake implementation of the compiler service, built
-- using the "Shaker" abstraction layer for in-memory use.
--
module Development.IDE.Core.Rules(
IdeState, GetDependencies(..), GetParsedModule(..), TransitiveDependencies(..),
Priority(..),
priorityTypeCheck,
priorityGenerateCore,
priorityFilesOfInterest,
runAction, useE, useNoFileE, usesE,
toIdeResult, defineNoFile,
mainRule,
getGhcCore,
getAtPoint,
getDefinition,
getDependencies,
getParsedModule,
fileFromParsedModule,
writeIfacesAndHie,
) where
import Control.Monad.Except
import Control.Monad.Trans.Maybe
import Development.IDE.Core.Compile
import Development.IDE.Types.Options
import Development.IDE.Spans.Calculate
import Development.IDE.Import.DependencyInformation
import Development.IDE.Import.FindImports
import Development.IDE.Core.FileStore
import Development.IDE.Types.Diagnostics
import Development.IDE.Types.Location
import Development.IDE.GHC.Util
import Data.Coerce
import Data.Either.Extra
import Data.Maybe
import Data.Foldable
import qualified Data.IntMap.Strict as IntMap
import qualified Data.IntSet as IntSet
import Data.List
import qualified Data.Set as Set
import qualified Data.Text as T
import Development.IDE.GHC.Error
import Development.Shake hiding (Diagnostic)
import Development.IDE.Core.RuleTypes
import GHC hiding (parseModule, typecheckModule)
import Development.IDE.GHC.Compat
import UniqSupply
import NameCache
import HscTypes
import GHC.Generics(Generic)
import qualified Development.IDE.Spans.AtPoint as AtPoint
import Development.IDE.Core.Service
import Development.IDE.Core.Shake
import Development.Shake.Classes
import System.Directory
import System.FilePath
import MkIface
-- | This is useful for rules to convert rules that can only produce errors or
-- a result into the more general IdeResult type that supports producing
-- warnings while also producing a result.
toIdeResult :: Either [FileDiagnostic] v -> IdeResult v
toIdeResult = either (, Nothing) (([],) . Just)
-- | useE is useful to implement functions that aren’t rules but need shortcircuiting
-- e.g. getDefinition.
useE :: IdeRule k v => k -> NormalizedFilePath -> MaybeT Action v
useE k = MaybeT . use k
useNoFileE :: IdeRule k v => k -> MaybeT Action v
useNoFileE k = useE k ""
usesE :: IdeRule k v => k -> [NormalizedFilePath] -> MaybeT Action [v]
usesE k = MaybeT . fmap sequence . uses k
defineNoFile :: IdeRule k v => (k -> Action v) -> Rules ()
defineNoFile f = define $ \k file -> do
if file == "" then do res <- f k; return ([], Just res) else
fail $ "Rule " ++ show k ++ " should always be called with the empty string for a file"
------------------------------------------------------------
-- Exposed API
-- | Generate the GHC Core for the supplied file and its dependencies.
getGhcCore :: NormalizedFilePath -> Action (Maybe [CoreModule])
getGhcCore file = runMaybeT $ do
files <- transitiveModuleDeps <$> useE GetDependencies file
pms <- usesE GetParsedModule $ files ++ [file]
usesE GenerateCore $ map fileFromParsedModule pms
-- | Get all transitive file dependencies of a given module.
-- Does not include the file itself.
getDependencies :: NormalizedFilePath -> Action (Maybe [NormalizedFilePath])
getDependencies file = fmap transitiveModuleDeps <$> use GetDependencies file
-- | Try to get hover text for the name under point.
getAtPoint :: NormalizedFilePath -> Position -> Action (Maybe (Maybe Range, [T.Text]))
getAtPoint file pos = fmap join $ runMaybeT $ do
opts <- lift getIdeOptions
files <- transitiveModuleDeps <$> useE GetDependencies file
tms <- usesE TypeCheck (file : files)
spans <- useE GetSpanInfo file
return $ AtPoint.atPoint opts (map tmrModule tms) spans pos
-- | Goto Definition.
getDefinition :: NormalizedFilePath -> Position -> Action (Maybe Location)
getDefinition file pos = fmap join $ runMaybeT $ do
spans <- useE GetSpanInfo file
pkgState <- hscEnv <$> useE GhcSession file
opts <- lift getIdeOptions
let getHieFile x = useNoFile (GetHieFile x)
lift $ AtPoint.gotoDefinition getHieFile opts pkgState spans pos
-- | Parse the contents of a daml file.
getParsedModule :: NormalizedFilePath -> Action (Maybe ParsedModule)
getParsedModule file = use GetParsedModule file
-- | Write interface files and hie files to the location specified by the given options.
writeIfacesAndHie ::
NormalizedFilePath -> [NormalizedFilePath] -> Action (Maybe [NormalizedFilePath])
writeIfacesAndHie ifDir files =
runMaybeT $ do
tcms <- usesE TypeCheck files
fmap concat $ forM (zip files tcms) $ \(file, tcm) -> do
session <- lift $ hscEnv <$> use_ GhcSession file
liftIO $ writeTcm session tcm
where
writeTcm session tcm =
do
let fp =
fromNormalizedFilePath ifDir </>
(ms_hspp_file $
pm_mod_summary $ tm_parsed_module $ tmrModule tcm)
createDirectoryIfMissing True (takeDirectory fp)
let ifaceFp = replaceExtension fp ".hi"
let hieFp = replaceExtension fp ".hie"
writeIfaceFile
(hsc_dflags session)
ifaceFp
(hm_iface $ tmrModInfo tcm)
hieFile <-
liftIO $
runHsc session $
mkHieFile
(pm_mod_summary $ tm_parsed_module $ tmrModule tcm)
(fst $ tm_internals_ $ tmrModule tcm)
(fromJust $ tm_renamed_source $ tmrModule tcm)
writeHieFile hieFp hieFile
pure [toNormalizedFilePath ifaceFp, toNormalizedFilePath hieFp]
------------------------------------------------------------
-- Rules
-- These typically go from key to value and are oracles.
priorityTypeCheck :: Priority
priorityTypeCheck = Priority 0
priorityGenerateCore :: Priority
priorityGenerateCore = Priority (-1)
priorityFilesOfInterest :: Priority
priorityFilesOfInterest = Priority (-2)
getParsedModuleRule :: Rules ()
getParsedModuleRule =
define $ \GetParsedModule file -> do
(_, contents) <- getFileContents file
packageState <- hscEnv <$> use_ GhcSession file
opt <- getIdeOptions
liftIO $ parseModule opt packageState (fromNormalizedFilePath file) contents
getLocatedImportsRule :: Rules ()
getLocatedImportsRule =
define $ \GetLocatedImports file -> do
pm <- use_ GetParsedModule file
let ms = pm_mod_summary pm
let imports = [(False, imp) | imp <- ms_textual_imps ms] ++ [(True, imp) | imp <- ms_srcimps ms]
env <- hscEnv <$> useNoFile_ GhcSession
let dflags = addRelativeImport pm $ hsc_dflags env
opt <- getIdeOptions
(diags, imports') <- fmap unzip $ forM imports $ \(isSource, (mbPkgName, modName)) -> do
diagOrImp <- locateModule dflags (optExtensions opt) getFileExists modName mbPkgName isSource
case diagOrImp of
Left diags -> pure (diags, Left (modName, Nothing))
Right (FileImport path) -> pure ([], Left (modName, Just path))
Right (PackageImport pkgId) -> liftIO $ do
diagsOrPkgDeps <- computePackageDeps env pkgId
case diagsOrPkgDeps of
Left diags -> pure (diags, Right Nothing)
Right pkgIds -> pure ([], Right $ Just $ pkgId : pkgIds)
let (moduleImports, pkgImports) = partitionEithers imports'
case sequence pkgImports of
Nothing -> pure (concat diags, Nothing)
Just pkgImports -> pure (concat diags, Just (moduleImports, Set.fromList $ concat pkgImports))
-- | Given a target file path, construct the raw dependency results by following
-- imports recursively.
rawDependencyInformation :: NormalizedFilePath -> Action RawDependencyInformation
rawDependencyInformation f = do
let (initialId, initialMap) = getPathId f emptyPathIdMap
go (IntSet.singleton $ getFilePathId initialId)
(RawDependencyInformation IntMap.empty initialMap)
where
go fs rawDepInfo =
case IntSet.minView fs of
-- Queue is empty
Nothing -> pure rawDepInfo
-- Pop f from the queue and process it
Just (f, fs) -> do
let fId = FilePathId f
importsOrErr <- use GetLocatedImports $ idToPath (rawPathIdMap rawDepInfo) fId
case importsOrErr of
Nothing ->
-- File doesn’t parse
let rawDepInfo' = insertImport fId (Left ModuleParseError) rawDepInfo
in go fs rawDepInfo'
Just (modImports, pkgImports) -> do
let f :: PathIdMap -> (a, Maybe NormalizedFilePath) -> (PathIdMap, (a, Maybe FilePathId))
f pathMap (imp, mbPath) = case mbPath of
Nothing -> (pathMap, (imp, Nothing))
Just path ->
let (pathId, pathMap') = getPathId path pathMap
in (pathMap', (imp, Just pathId))
-- Convert paths in imports to ids and update the path map
let (pathIdMap, modImports') = mapAccumL f (rawPathIdMap rawDepInfo) modImports
-- Files that we haven’t seen before are added to the queue.
let newFiles =
IntSet.fromList (coerce $ mapMaybe snd modImports')
IntSet.\\ IntMap.keysSet (rawImports rawDepInfo)
let rawDepInfo' = insertImport fId (Right $ ModuleImports modImports' pkgImports) rawDepInfo
go (newFiles `IntSet.union` fs) (rawDepInfo' { rawPathIdMap = pathIdMap })
getDependencyInformationRule :: Rules ()
getDependencyInformationRule =
define $ \GetDependencyInformation file -> do
rawDepInfo <- rawDependencyInformation file
pure ([], Just $ processDependencyInformation rawDepInfo)
reportImportCyclesRule :: Rules ()
reportImportCyclesRule =
define $ \ReportImportCycles file -> fmap (\errs -> if null errs then ([], Just ()) else (errs, Nothing)) $ do
DependencyInformation{..} <- use_ GetDependencyInformation file
let fileId = pathToId depPathIdMap file
case IntMap.lookup (getFilePathId fileId) depErrorNodes of
Nothing -> pure []
Just errs -> do
let cycles = mapMaybe (cycleErrorInFile fileId) (toList errs)
-- Convert cycles of files into cycles of module names
forM cycles $ \(imp, files) -> do
modNames <- forM files $ \fileId -> do
let file = idToPath depPathIdMap fileId
getModuleName file
pure $ toDiag imp $ sort modNames
where cycleErrorInFile f (PartOfCycle imp fs)
| f `elem` fs = Just (imp, fs)
cycleErrorInFile _ _ = Nothing
toDiag imp mods = (fp ,) $ Diagnostic
{ _range = (_range :: Location -> Range) loc
, _severity = Just DsError
, _source = Just "Import cycle detection"
, _message = "Cyclic module dependency between " <> showCycle mods
, _code = Nothing
, _relatedInformation = Nothing
}
where loc = srcSpanToLocation (getLoc imp)
fp = toNormalizedFilePath $ srcSpanToFilename (getLoc imp)
getModuleName file = do
pm <- use_ GetParsedModule file
pure (moduleNameString . moduleName . ms_mod $ pm_mod_summary pm)
showCycle mods = T.intercalate ", " (map T.pack mods)
-- returns all transitive dependencies in topological order.
-- NOTE: result does not include the argument file.
getDependenciesRule :: Rules ()
getDependenciesRule =
define $ \GetDependencies file -> do
depInfo@DependencyInformation{..} <- use_ GetDependencyInformation file
let allFiles = reachableModules depInfo
_ <- uses_ ReportImportCycles allFiles
return ([], transitiveDeps depInfo file)
-- Source SpanInfo is used by AtPoint and Goto Definition.
getSpanInfoRule :: Rules ()
getSpanInfoRule =
define $ \GetSpanInfo file -> do
tc <- use_ TypeCheck file
(fileImports, _) <- use_ GetLocatedImports file
packageState <- hscEnv <$> use_ GhcSession file
x <- liftIO $ getSrcSpanInfos packageState fileImports tc
return ([], Just x)
-- Typechecks a module.
typeCheckRule :: Rules ()
typeCheckRule =
define $ \TypeCheck file -> do
pm <- use_ GetParsedModule file
deps <- use_ GetDependencies file
tms <- uses_ TypeCheck (transitiveModuleDeps deps)
setPriority priorityTypeCheck
packageState <- hscEnv <$> use_ GhcSession file
liftIO $ typecheckModule packageState tms pm
generateCoreRule :: Rules ()
generateCoreRule =
define $ \GenerateCore file -> do
deps <- use_ GetDependencies file
(tm:tms) <- uses_ TypeCheck (file:transitiveModuleDeps deps)
setPriority priorityGenerateCore
packageState <- hscEnv <$> use_ GhcSession file
liftIO $ compileModule packageState tms tm
-- A local rule type to get caching. We want to use newCache, but it has
-- thread killed exception issues, so we lift it to a full rule.
-- https://github.com/digital-asset/daml/pull/2808#issuecomment-529639547
type instance RuleResult GhcSessionIO = GhcSessionFun
data GhcSessionIO = GhcSessionIO deriving (Eq, Show, Typeable, Generic)
instance Hashable GhcSessionIO
instance NFData GhcSessionIO
newtype GhcSessionFun = GhcSessionFun (FilePath -> Action HscEnvEq)
instance Show GhcSessionFun where show _ = "GhcSessionFun"
instance NFData GhcSessionFun where rnf !_ = ()
loadGhcSession :: Rules ()
loadGhcSession = do
defineNoFile $ \GhcSessionIO -> do
opts <- getIdeOptions
liftIO $ GhcSessionFun <$> optGhcSession opts
define $ \GhcSession file -> do
GhcSessionFun fun <- useNoFile_ GhcSessionIO
val <- fun $ fromNormalizedFilePath file
return ([], Just val)
getHieFileRule :: Rules ()
getHieFileRule =
defineNoFile $ \(GetHieFile f) -> do
u <- liftIO $ mkSplitUniqSupply 'a'
let nameCache = initNameCache u []
liftIO $ fmap (hie_file_result . fst) $ readHieFile nameCache f
-- | A rule that wires per-file rules together
mainRule :: Rules ()
mainRule = do
getParsedModuleRule
getLocatedImportsRule
getDependencyInformationRule
reportImportCyclesRule
getDependenciesRule
typeCheckRule
getSpanInfoRule
generateCoreRule
loadGhcSession
getHieFileRule
------------------------------------------------------------
fileFromParsedModule :: ParsedModule -> NormalizedFilePath
fileFromParsedModule = toNormalizedFilePath . ms_hspp_file . pm_mod_summary