ghcide-2.15.0.0: src/Development/IDE/Import/FindImports.hs
-- Copyright (c) 2019 The DAML Authors. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE NamedFieldPuns #-}
module Development.IDE.Import.FindImports
( locateModule
, Import(..)
, ArtifactsLocation(..)
, modSummaryToArtifactsLocation
, isBootLocation
, ModuleToFilenames(..)
, mkModuleToFilenames
, mkUnitVisibility
) where
import Control.DeepSeq
import Control.Monad.IO.Class
import qualified Data.HashSet as HS
import Data.List (intercalate, isSuffixOf,
sort, sortOn)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NE
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import qualified Data.Set as S
import Development.IDE.GHC.Compat as Compat
import Development.IDE.GHC.Error as ErrUtils
import Development.IDE.GHC.Orphans ()
import Development.IDE.Types.Diagnostics
import Development.IDE.Types.Location
import GHC.Fingerprint
import GHC.Generics
import GHC.Types.PkgQual
import GHC.Unit
#if MIN_VERSION_ghc(9,11,0)
import GHC.Driver.DynFlags (ReexportedModule (..),
hiddenModules)
#else
import GHC.Driver.Session (hiddenModules)
#endif
data Import
= FileImport !ArtifactsLocation
| PackageImport
deriving (Show)
data ArtifactsLocation = ArtifactsLocation
{ artifactFilePath :: !NormalizedFilePath
, artifactModLocation :: !(Maybe ModLocation)
, artifactIsSource :: !Bool -- ^ 'True' for a real Haskell source file ('HsSrcFile');
-- 'False' for a boot ('HsBootFile') or signature ('HsigFile') file.
, artifactModule :: !(Maybe Module)
} deriving Show
instance NFData ArtifactsLocation where
rnf ArtifactsLocation{..} = rnf artifactFilePath `seq` rwhnf artifactModLocation `seq` rnf artifactIsSource `seq` rnf artifactModule
isBootLocation :: ArtifactsLocation -> Bool
isBootLocation = not . artifactIsSource
instance NFData Import where
rnf (FileImport x) = rnf x
rnf PackageImport = ()
modSummaryToArtifactsLocation :: NormalizedFilePath -> Maybe ModSummary -> ArtifactsLocation
modSummaryToArtifactsLocation nfp ms = ArtifactsLocation nfp (ms_location <$> ms) source mbMod
where
isSource HsSrcFile = True
isSource _ = False
source = case ms of
Nothing -> "-boot" `isSuffixOf` fromNormalizedFilePath nfp
Just modSum -> isSource (ms_hsc_src modSum)
mbMod = ms_mod <$> ms
-- | For each module name, the units that provide it and the exact location
-- where the unit has it.
-- see 'locateModuleFile' for how we decide which unit an import actually
-- resolves to.
data ModuleToFilenames = ModuleToFilenames {
-- | Modules and the unit, source pairs they correspond to
moduleMap :: UniqMap ModuleName (NonEmpty (UnitId, NormalizedFilePath)),
-- | Boot files we know exist. If you want to check if a boot file exists,
-- check this field for precisely the -boot file corresponding to the non-boot
-- file you have already resolved.
bootFiles :: HS.HashSet NormalizedFilePath,
-- | Fingerprint of the two, for early cutoff
mtfFingerprint :: !Fingerprint
}
deriving Generic
-- | The fingerprint is strict and computing it forces the map and the set, so
-- there is nothing left to force. Deep forcing here would traverse them again
-- for every file of the session.
instance NFData ModuleToFilenames where
rnf = rwhnf
instance Show ModuleToFilenames where
show mtf = "ModuleToFilenames " ++ show (mtfFingerprint mtf)
mkModuleToFilenames
:: UniqMap ModuleName (NonEmpty (UnitId, NormalizedFilePath))
-> HS.HashSet NormalizedFilePath
-> ModuleToFilenames
mkModuleToFilenames normal boots =
ModuleToFilenames normal boots (fingerprintFingerprints [fpMap normal, fpSet boots])
where
fpMap m = fingerprintFingerprints
[ fingerprintString $ intercalate "\0" $
moduleNameString mn :
concat [ [unitIdString u, fromNormalizedFilePath p] | (u, p) <- NE.toList provs ]
| (mn, provs) <- sortOn (moduleNameString . fst) (nonDetEltsUFM (getUniqMap m))
]
fpSet s = fingerprintString $ intercalate "\0" $
sort $ map fromNormalizedFilePath $ HS.toList s
data LocateResult
= LocateNotFound
| LocateFoundReexport UnitId ModuleName
-- ^ The unit reexporting the module, and the name it has there
| LocateFoundFile UnitId NormalizedFilePath
-- | What a home unit exposes to the units depending on it.
data UnitVisibility = UnitVisibility
{ uvReexports :: Map ModuleName ModuleName
-- ^ The name we import it under, and the name it has in the unit it is
-- reexported from
, uvHidden :: S.Set ModuleName
}
-- | What a home unit exposes, from its flags.
mkUnitVisibility :: (UnitId, DynFlags) -> (UnitId, UnitVisibility)
mkUnitVisibility (i, flags) = (i, UnitVisibility reexports (hiddenModules flags))
where
#if MIN_VERSION_ghc(9,11,0)
-- Earlier entries win, as in 'GHC.Driver.Config.Finder.initFinderOpts'
reexports = Map.fromList
[ (reexportTo r, reexportFrom r) | r <- reverse (reexportedModules flags) ]
#else
reexports = Map.fromSet id (reexportedModules flags)
#endif
-- | Locate a module in the file system.
--
-- We go through the units in the given order and do exactly what GHC's finder
-- does: if the unit reexports the module we start again from that unit, if it
-- hides the module we skip it, and otherwise it provides the module if it has
-- a file for it. A unit that is not in the list is not visible to the importer.
locateModuleFile
:: ModuleToFilenames
-> ModuleName
-> [(UnitId, Maybe UnitVisibility)]
-- ^ Units to search, in priority order. 'Nothing' for the importing unit,
-- whose own reexports and hidden modules do not apply to it.
-> LocateResult
locateModuleFile ModuleToFilenames{moduleMap} modName = go
where
providers = maybe [] NE.toList $ lookupUniqMap moduleMap modName
go [] = LocateNotFound
go ((uid, mbVisibility) : units)
| Just vis <- mbVisibility
, Just realName <- Map.lookup modName (uvReexports vis)
= LocateFoundReexport uid realName
| Just vis <- mbVisibility
, modName `S.member` uvHidden vis
= go units
| Just file <- lookup uid providers
= LocateFoundFile uid file
| otherwise
= go units
-- | locate a module in either the file system or the package database. Where we go from *daml to
-- Haskell
locateModule
:: MonadIO m
=> ModuleToFilenames
-> HscEnv
-> Map UnitId UnitVisibility -- ^ What each home unit exposes
-> Located ModuleName -- ^ Module name
-> PkgQual -- ^ Package name
-> Bool -- ^ Is boot module
-> m (Either [FileDiagnostic] Import)
locateModule moduleMaps env unit_visibility modName mbPkgName isSource = do
case mbPkgName of
-- 'ThisPkg' just means some home module, not the current unit
-- A home unit qualifier is not a package, so the package database is not
-- consulted when the module is not in that unit
ThisPkg uid
| uid == homeUnitId_ dflags -> lookupIn moduleNotFound [(uid, Nothing)]
| Just vis <- Map.lookup uid unit_visibility -> lookupIn moduleNotFound [(uid, Just vis)]
| otherwise -> moduleNotFound
-- if a package name is given we only go look for a package
OtherPkg uid
| Just vis <- Map.lookup uid unit_visibility -> lookupIn lookupInPackageDB [(uid, Just vis)]
| otherwise -> lookupInPackageDB
NoPkgQual -> lookupIn lookupInPackageDB searchUnits
where
dflags = hsc_dflags env
moduleNotFound = return $ Left $ notFoundErr env modName $ LookupNotFound []
lookupIn onNotFound units =
case locateModuleFile moduleMaps (unLoc modName) units of
LocateNotFound -> onNotFound
-- Look again from the perspective of the unit reexporting the module,
-- under the name it has there
LocateFoundReexport uid realName ->
locateModule moduleMaps (hscSetActiveUnitId uid env) unit_visibility
(const realName <$> modName) noPkgQual isSource
LocateFoundFile uid file
-- The search only ever finds source files. A SOURCE import takes the
-- boot file next to the source file we found, and fails if there is
-- none, we do not go looking anywhere else.
| isSource -> maybe moduleNotFound (toModLocation uid) (bootFile file)
| otherwise -> toModLocation uid file
bootFile file
| boot `HS.member` bootFiles moduleMaps = Just boot
| otherwise = Nothing
where boot = toNormalizedFilePath' $ fromNormalizedFilePath file <> "-boot"
-- The units an unqualified import may come from: the current unit first,
-- then its dependencies, in the given order, which decides who wins when
-- several provide the module.
-- The current unit's own reexports and hidden modules do not apply to it,
-- which also stops the reexport search from looping.
searchUnits = (homeUnitId_ dflags, Nothing) :
[ (uid, Map.lookup uid unit_visibility) | uid <- hpt_deps ]
ue = hsc_unit_env env
units = homeUnitEnv_units $ ue_findHomeUnitEnv (homeUnitId_ dflags) ue
hpt_deps :: [UnitId]
hpt_deps = homeUnitDepends units
toModLocation uid file = liftIO $ do
loc <- mkHomeModLocation dflags (unLoc modName) (fromNormalizedFilePath file)
let genMod = mkModule (RealUnit $ Definite uid) (unLoc modName) -- TODO support backpack holes
loc' = if isSource then addBootSuffixLocnOut loc else loc
return $ Right $ FileImport $ ArtifactsLocation file (Just loc') (not isSource) (Just genMod)
lookupInPackageDB = do
case Compat.lookupModuleWithSuggestions env (unLoc modName) mbPkgName of
LookupFound _m _pkgConfig -> return $ Right PackageImport
reason -> return $ Left $ notFoundErr env modName reason
-- | Don't call this on a found module.
notFoundErr :: HscEnv -> Located ModuleName -> LookupResult -> [FileDiagnostic]
notFoundErr env modName reason =
mkError' $ ppr' $ cannotFindModule env modName0 $ lookupToFindResult reason
where
dfs = hsc_dflags env
mkError' doc = diagFromString "not found" DiagnosticSeverity_Error (Compat.getLoc modName) doc Nothing
modName0 = unLoc modName
ppr' = showSDoc dfs
-- We convert the lookup result to a find result to reuse GHC's cannotFindModule pretty printer.
lookupToFindResult =
\case
LookupFound _m _pkgConfig ->
pprPanic "Impossible: called lookupToFind on found module." (ppr modName0)
LookupMultiple rs -> FoundMultiple rs
LookupHidden pkg_hiddens mod_hiddens ->
notFound
{ fr_pkgs_hidden = map (moduleUnit . fst) pkg_hiddens
, fr_mods_hidden = map (moduleUnit . fst) mod_hiddens
}
LookupUnusable unusable ->
let unusables' = map get_unusable unusable
#if MIN_VERSION_ghc(9,6,4) && (!MIN_VERSION_ghc(9,8,1) || MIN_VERSION_ghc(9,8,2))
get_unusable (_m, ModUnusable r) = r
#else
get_unusable (m, ModUnusable r) = (moduleUnit m, r)
#endif
get_unusable (_, r) =
pprPanic "findLookupResult: unexpected origin" (ppr r)
in notFound {fr_unusables = unusables'}
LookupNotFound suggest ->
notFound {fr_suggestions = suggest}
notFound :: FindResult
notFound = NotFound
{ fr_paths = []
, fr_pkg = Nothing
, fr_pkgs_hidden = []
, fr_mods_hidden = []
, fr_unusables = []
, fr_suggestions = []
}
noPkgQual :: PkgQual
noPkgQual = NoPkgQual