hsinspect-0.1.0: library/HsInspect/Index.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
-- | Dumps an index of all terms and their types
module HsInspect.Index
( index,
PackageEntries,
)
where
#if MIN_VERSION_GLASGOW_HASKELL(9,3,0,0)
import qualified GHC.Driver.Session as GHC
#endif
#if MIN_VERSION_GLASGOW_HASKELL(9,1,0,0)
import qualified GHC.Data.ShortText as GHC
import qualified GHC.Driver.Env.Types as GHC
import qualified GHC.Driver.Ppr as GHC
import qualified GHC.Unit.Env as GHC
#endif
#if MIN_VERSION_GLASGOW_HASKELL(9,0,0,0)
import qualified GHC.Core.ConLike as GHC
import qualified GHC.Core.PatSyn as GHC
import qualified GHC.Core.TyCon as GHC
import qualified GHC.Data.FastString as GHC
import qualified GHC.Iface.Binary as GHC
import qualified GHC.Tc.Types as GHC
import qualified GHC.Tc.Utils.Env as GHC
import qualified GHC.Tc.Utils.Monad as GHC
import qualified GHC.Types.Avail as GHC
import qualified GHC.Types.Id as GHC
import qualified GHC.Types.Name as GHC
import qualified GHC.Unit.Database as GHC
import qualified GHC.Unit.State as GHC
import qualified GHC.Unit.Types as GHC
import qualified GHC.Utils.Outputable as GHC
#else
import qualified Avail as GHC
import qualified BinIface as GHC
import qualified ConLike as GHC
import qualified DynFlags as GHC
import qualified FastString as GHC
import qualified GHC.PackageDb as GHC
import qualified Id as GHC
import qualified Module as GHC
import qualified Name as GHC
import qualified Outputable as GHC
import qualified PackageConfig as GHC
import qualified Packages as GHC
import qualified PatSyn as GHC
import qualified TcEnv as GHC
import qualified TcRnMonad as GHC
import qualified TyCon as GHC
#endif
import qualified GHC
import Control.Monad
import Control.Monad.IO.Class
import Data.List (isInfixOf, sort)
import Data.Maybe (catMaybes, mapMaybe, maybeToList)
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Text (Text)
import qualified Data.Text as T
import HsInspect.Sexp
import HsInspect.Util
-- TODO export unexposed modules too, since they could be exposed by an export elsewhere
--
-- TODO modules that export other modules seem to be skipped, e.g. Language.Haskell.LSP.Types
index :: GHC.GhcMonad m => m [PackageEntries]
index = do
dflags <- GHC.getSessionDynFlags
#if MIN_VERSION_GLASGOW_HASKELL(9,3,0,0)
let unarg as = fst <$> as
#elif MIN_VERSION_GLASGOW_HASKELL(9,1,0,0)
let unarg = id
#endif
#if MIN_VERSION_GLASGOW_HASKELL(9,1,0,0)
sess <- GHC.getSession
let unit_state = GHC.ue_units $ GHC.hsc_unit_env sess
explicit = GHC.explicitUnits unit_state
pkgcfgs = maybeToList . GHC.lookupUnit unit_state =<< unarg explicit
#elif MIN_VERSION_GLASGOW_HASKELL(9,0,0,0)
let unit_state = GHC.unitState dflags
explicit = GHC.explicitUnits unit_state
pkgcfgs = maybeToList . GHC.lookupUnit unit_state =<< explicit
#else
let explicit = GHC.explicitPackages $ GHC.pkgState dflags
pkgcfgs = maybeToList . GHC.lookupPackage dflags =<< explicit
#endif
deps <- traverse getPkgSymbols pkgcfgs
loadCompiledModules
#if MIN_VERSION_GLASGOW_HASKELL(9,1,0,0)
let unitid = GHC.homeUnitId_ dflags
#elif MIN_VERSION_GLASGOW_HASKELL(9,0,0,0)
let unitid = GHC.homeUnitId dflags
#else
let unitid = GHC.thisPackage dflags
#endif
dirs = maybeToList $ GHC.hiDir dflags
home_mods <- getTargetModules
home_entries <- getSymbols unitid True [] home_mods dirs
pure $ home_entries : deps
-- finds the module names of all the .hi files in the output directory and then
-- tells ghc to load them as the only targets. Compared to loading all the home
-- modules provided by the ghcflags, this means that ghc can only see the
-- contents of compiled files and will not attempt to compile any source code.
-- Obviously comes with caveats but will be much faster if the preferred
-- behaviour is to fail fast with partial data instead of trying (futilely) to
-- compile all home modules with the interactive compiler.
loadCompiledModules :: GHC.GhcMonad m => m ()
loadCompiledModules = do
dflags <- GHC.getSessionDynFlags
case GHC.hiDir dflags of
Nothing -> pure ()
Just dir -> do
compiled <- getCompiledTargets dir
GHC.setTargets compiled
void . GHC.load $ GHC.LoadAllTargets
getCompiledTargets :: GHC.GhcMonad m => FilePath -> m [GHC.Target]
getCompiledTargets dir = do
provided <- getTargetModules
his <- liftIO $ walkSuffix ".hi" dir
modules <- catMaybes <$> traverse (flip withHi (pure . GHC.mi_module)) his
#if MIN_VERSION_GLASGOW_HASKELL(9,3,0,0)
sess <- GHC.getSession
let unitid = GHC.ue_current_unit $ GHC.hsc_unit_env sess
mkTarget m = GHC.Target (GHC.TargetModule m) True unitid Nothing
#else
let mkTarget m = GHC.Target (GHC.TargetModule m) True Nothing
#endif
toTarget m =
if Set.member m provided
then Just $ mkTarget m
else Nothing
pure $ mapMaybe (toTarget . GHC.moduleName) modules
-- Perform an operation given the parsed .hi file. tcLookup will only succeed if
-- the module is on the packagedb or is a home module that has been loaded.
withHi :: GHC.GhcMonad m => FilePath -> (GHC.ModIface -> (GHC.TcRnIf GHC.TcGblEnv GHC.TcLclEnv) a) -> m (Maybe a)
withHi hi f = do
env <- GHC.getSession
#if MIN_VERSION_GLASGOW_HASKELL(9,3,0,0)
dflags <- GHC.getSessionDynFlags
let profile = GHC.targetProfile dflags
name_cache = GHC.hsc_NC env
(_, res) <- liftIO $ do
iface <- GHC.readBinIface profile name_cache GHC.IgnoreHiWay GHC.QuietBinIFace hi
GHC.initTcInteractive env $ f iface
#elif MIN_VERSION_GLASGOW_HASKELL(9,1,0,0)
(_, res) <- liftIO . GHC.initTcInteractive env $ do
iface <- GHC.readBinIface GHC.IgnoreHiWay GHC.QuietBinIFace hi
f iface
#else
(_, res) <- liftIO . GHC.initTcInteractive env $ do
iface <- GHC.readBinIface GHC.IgnoreHiWay GHC.QuietBinIFaceReading hi
f iface
#endif
pure res
#if MIN_VERSION_GLASGOW_HASKELL(9,0,0,0)
getPkgSymbols :: GHC.GhcMonad m => GHC.UnitInfo -> m PackageEntries
#else
getPkgSymbols :: GHC.GhcMonad m => GHC.PackageConfig -> m PackageEntries
#endif
getPkgSymbols pkg =
#if MIN_VERSION_GLASGOW_HASKELL(9,1,0,0)
let exposed = Set.fromList $ fst <$> GHC.unitExposedModules pkg
GHC.GenericUnitInfo {GHC.unitId = unitid} = pkg
dirs = GHC.unpack <$> (GHC.unitImportDirs pkg)
haddocks = GHC.unpack <$> GHC.unitHaddockHTMLs pkg
#elif MIN_VERSION_GLASGOW_HASKELL(9,0,0,0)
let exposed = Set.fromList $ fst <$> GHC.unitExposedModules pkg
GHC.GenericUnitInfo {GHC.unitId = unitid} = pkg
dirs = GHC.unitImportDirs pkg
haddocks = GHC.unitHaddockHTMLs pkg
#else
let exposed = Set.fromList $ fst <$> GHC.exposedModules pkg
unitid = GHC.packageConfigId pkg
dirs = (GHC.importDirs pkg)
haddocks = GHC.haddockHTMLs pkg
#endif
unit_string = GHC.unitIdString unitid
inplace = "-inplace" `isInfixOf` unit_string
in getSymbols unitid inplace haddocks exposed dirs
getSymbols :: GHC.GhcMonad m => GHC.UnitId -> Bool -> [FilePath] -> Set GHC.ModuleName -> [FilePath] -> m PackageEntries
getSymbols unitid inplace haddocks exposed dirs = do
let findHis dir = liftIO $ walkSuffix ".hi" dir
his <- join <$> traverse findHis dirs
dflags <- GHC.getSessionDynFlags
#if MIN_VERSION_GLASGOW_HASKELL(9,1,0,0)
sess <- GHC.getSession
let unit_state = GHC.ue_units . GHC.hsc_unit_env $ sess
findPid unitid' = GHC.unitPackageId <$> GHC.lookupUnitId unit_state unitid'
findUnitId = GHC.toUnitId . GHC.moduleUnit
mkPackageId (GHC.PackageId fs) = PackageId . T.pack $ GHC.unpackFS fs
#elif MIN_VERSION_GLASGOW_HASKELL(9,0,0,0)
let unit_state = GHC.unitState dflags
findPid unitid' = GHC.unitPackageId <$> GHC.lookupUnitId unit_state unitid'
findUnitId = GHC.toUnitId . GHC.moduleUnit
mkPackageId (GHC.PackageId fs) = PackageId . T.pack $ GHC.unpackFS fs
#else
let findPid unitid' = GHC.sourcePackageId <$> GHC.lookupPackage dflags unitid'
findUnitId = GHC.moduleUnitId
mkPackageId (GHC.SourcePackageId fs) = PackageId . T.pack $ GHC.unpackFS fs
#endif
srcid = findPid unitid
symbols <- catMaybes <$> traverse (hiToSymbols exposed) his
let entries = sort $ uncurry mkEntries <$> symbols
mkModuleName :: GHC.Module -> ModuleName
mkModuleName = ModuleName . T.pack . GHC.moduleNameString . GHC.moduleName
mkEntries m things = ModuleEntries (mkModuleName m) (sort $ renderThings things)
-- for the given module, only including the packageid if it is different
-- than the package of the module under inspection.
mkExported m =
let unitid' = findUnitId m
pid = if unitid == unitid'
then Nothing
else findPid unitid'
in Exported (mkPackageId <$> pid) (mkModuleName m)
renderThings things = catMaybes $ (\(mm, thing) -> tyrender dflags (mkExported <$> mm) thing) <$> things
pure $ PackageEntries (mkPackageId <$> srcid) inplace entries (T.pack <$> haddocks)
-- for a .hi file returns the module and a list of all things (with types
-- resolved) in that module and their original module if they are re-exported.
hiToSymbols
:: GHC.GhcMonad m
=> Set GHC.ModuleName
-> FilePath
-> m (Maybe (GHC.Module, [(Maybe GHC.Module, GHC.TcTyThing)]))
hiToSymbols exposed hi = (join <$>) <$> withHi hi $ \iface -> do
let m = GHC.mi_module iface
-- TODO we should include all modules from inplace packages, otherwise the
-- user is unable to jump-to-definition within the same multi-package project.
if not $ Set.member (GHC.moduleName m) exposed
then pure Nothing
else do
let thing (GHC.Avail name) = traverse tcLookup' [name]
-- TODO the fields in AvailTC
#if MIN_VERSION_GLASGOW_HASKELL(9,1,0,0)
thing (GHC.AvailTC _ members) = traverse tcLookup' members
#else
thing (GHC.AvailTC _ members _) = traverse tcLookup' members
#endif
reexport name = do
modl <- GHC.nameModule_maybe name
if m == modl then Nothing else Just modl
tcLookup' name =
#if MIN_VERSION_GLASGOW_HASKELL(9,1,0,0)
let name' = GHC.greNameMangledName name
#else
let name' = name
#endif
in (reexport name',) <$> GHC.tcLookup name'
things <- join <$> traverse thing (GHC.mi_exports iface)
pure . Just $ (m, things)
-- TODO should we lose the dflags and use the unsafe variant?
tyrender :: GHC.DynFlags -> Maybe Exported -> GHC.TcTyThing -> Maybe Entry
tyrender dflags m (GHC.AGlobal thing) =
let
shw :: GHC.Outputable o => o -> Text
shw = T.pack . GHC.showPpr dflags
in case thing of
(GHC.AnId var) -> Just $ IdEntry m
(shw $ GHC.idName var)
(shw $ GHC.idType var)
(GHC.AConLike (GHC.RealDataCon dc)) -> Just $ ConEntry m
(shw $ GHC.getName dc)
#if MIN_VERSION_GLASGOW_HASKELL(9,0,0,0)
(shw $ GHC.dataConWrapperType dc)
#else
(shw $ GHC.dataConUserType dc)
#endif
(GHC.AConLike (GHC.PatSynCon ps)) -> Just $ PatSynEntry m
(shw $ GHC.getName ps)
(T.pack . GHC.showSDoc dflags $ GHC.pprPatSynType ps )
(GHC.ATyCon tc) -> Just $ TyConEntry m
(shw $ GHC.tyConName tc)
(shw $ GHC.tyConFlavour tc)
_ -> Nothing
tyrender _ _ _ = Nothing
data Entry = IdEntry (Maybe Exported) Text Text -- ^ name type
| ConEntry (Maybe Exported) Text Text -- ^ name type
| PatSynEntry (Maybe Exported) Text Text -- ^ name orig
| TyConEntry (Maybe Exported) Text Text -- ^ type flavour
deriving (Eq, Ord)
{- BOILERPLATE Entry ToSexp
field={IdEntry:[export,name,type],
ConEntry:[export,name,type],
PatSynEntry:[export,name,type],
TyConEntry:[export,type,flavour]}
class={IdEntry:id,
ConEntry:con,
PatSynEntry:pat,
TyConEntry:tycon}
-}
{- BOILERPLATE START -}
instance ToSexp Entry where
toSexp (IdEntry p_1_1 p_1_2 p_1_3) = alist $ ("class", "id") : [("export", toSexp p_1_1), ("name", toSexp p_1_2), ("type", toSexp p_1_3)]
toSexp (ConEntry p_1_1 p_1_2 p_1_3) = alist $ ("class", "con") : [("export", toSexp p_1_1), ("name", toSexp p_1_2), ("type", toSexp p_1_3)]
toSexp (PatSynEntry p_1_1 p_1_2 p_1_3) = alist $ ("class", "pat") : [("export", toSexp p_1_1), ("name", toSexp p_1_2), ("type", toSexp p_1_3)]
toSexp (TyConEntry p_1_1 p_1_2 p_1_3) = alist $ ("class", "tycon") : [("export", toSexp p_1_1), ("type", toSexp p_1_2), ("flavour", toSexp p_1_3)]
{- BOILERPLATE END -}
data ModuleEntries = ModuleEntries ModuleName [Entry]
deriving (Eq, Ord)
{- BOILERPLATE ModuleEntries ToSexp field=[module,ids] -}
{- BOILERPLATE START -}
instance ToSexp ModuleEntries where
toSexp (ModuleEntries p_1_1 p_1_2) = alist [("module", toSexp p_1_1), ("ids", toSexp p_1_2)]
{- BOILERPLATE END -}
-- The haddocks serve a dual purpose: not only do they point to where haddocks
-- might be, they give a hint to the text editor where the sources for this
-- package are (e.g. with the ghc distribution, build tool store or local).
--
-- Users should type `cabal haddock --enable-documentation` to populate the docs
-- of their dependencies and local projects.
type Haddocks = [Text]
-- Bool indicates if this is an -inplace package
data PackageEntries = PackageEntries (Maybe PackageId) Bool [ModuleEntries] Haddocks
{- BOILERPLATE PackageEntries ToSexp field=[srcid,inplace,modules,haddocks] -}
{- BOILERPLATE START -}
instance ToSexp PackageEntries where
toSexp (PackageEntries p_1_1 p_1_2 p_1_3 p_1_4) = alist [("srcid", toSexp p_1_1), ("inplace", toSexp p_1_2), ("modules", toSexp p_1_3), ("haddocks", toSexp p_1_4)]
{- BOILERPLATE END -}
-- srcid is Nothing if it matches the re-export location
data Exported = Exported (Maybe PackageId) ModuleName
deriving (Eq, Ord)
{- BOILERPLATE Exported ToSexp field=[srcid, module] -}
{- BOILERPLATE START -}
instance ToSexp Exported where
toSexp (Exported p_1_1 p_1_2) = alist [("srcid", toSexp p_1_1), ("module", toSexp p_1_2)]
{- BOILERPLATE END -}
-- local variants of things that exist in GHC
newtype ModuleName = ModuleName Text deriving (Eq, Ord, ToSexp)
newtype PackageId = PackageId Text deriving (Eq, Ord, ToSexp)