fix-imports-2.4.0: src/FixImports/Index.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
-- | Maintain the index from Qualification to the full module from the package
-- db that this Qualification probably intends.
module FixImports.Index (
Index, Package, empty, load, showIndex, makeIndex, parseSections
) where
import Prelude hiding (mod)
import Control.Monad
import Data.Bifunctor (second)
import Data.Maybe (mapMaybe)
import Data.Text (Text)
import System.FilePath ((</>))
import qualified Data.Either as Either
import qualified Data.List as List
import qualified Data.Map as Map
import qualified Data.Maybe as Maybe
import qualified Data.Set as Set
import qualified Data.Text as Text
import qualified Data.Text.IO as Text.IO
import qualified GHC.Paths
import qualified System.Directory as Directory
import qualified System.IO as IO
import qualified FixImports.PkgCache as PkgCache
import qualified FixImports.Types as Types
import qualified FixImports.Util as Util
-- | Map from tails of the each module in the package db to its module name.
-- So @List@ and @Data.List@ will map to @Data.List@. Modules from a set
-- of core packages, like base and containers, will take priority, so even if
-- there's a package with @Some.Obscure.List@, @List@ will still map to
-- @Data.List@.
type Index = Map.Map Types.Qualification [(Package, Types.ModuleName)]
-- | Package name without the version.
type Package = String
empty :: Index
empty = Map.empty
load :: IO (Index, Text)
load = fromGhcEnvironment >>= \case
Just index -> return (index, ".ghc.environment")
Nothing -> (, "global ghc-pkg") <$> fromGhcPkg
-- TODO ghc pkg could also use PkgCache to load the global db
showIndex :: Index -> Text
showIndex index = Text.unlines
[ Text.pack k <> ": " <> Text.pack (show v)
| (Types.Qualification k, v) <- Map.toAscList index
]
-- | I think the global package db is always under the libdir?
bootPkgDb :: FilePath
bootPkgDb = GHC.Paths.libdir </> "package.conf.d"
fromGhcEnvironment :: IO (Maybe Index)
fromGhcEnvironment = parseGhcEnvironment >>= \case
Nothing -> return Nothing
Just (pkgDbs, unitIds) -> do
nameModules <- PkgCache.load (Set.fromList unitIds)
(bootPkgDb : pkgDbs)
return $ Just $ makeIndex $
map (fmap (map Types.ModuleName)) nameModules
-- | The code to write .ghc.environment is in Cabal
-- Distribution.Simple.GHC.Internal, the code to read it is copy pasted over
-- into cabal-install Distribution.Client.CmdInstall. So they're not even
-- thinking of being consistent with themselves, let alone anyone else.
-- Too much bother.
parseGhcEnvironment :: IO (Maybe ([FilePath], [PkgCache.UnitId]))
parseGhcEnvironment = do
envFiles <- filter (".ghc.environment." `List.isPrefixOf`) <$>
Directory.listDirectory "."
case envFiles of
[] -> return Nothing
[envFile] -> Just . parseEnvFile <$> Text.IO.readFile envFile
_ -> error $ "multiple ghc env files: " <> unwords envFiles
parseEnvFile :: Text -> ([FilePath], [PkgCache.UnitId])
parseEnvFile = Either.partitionEithers . mapMaybe parse . Text.lines
where
parse line = case Text.words line of
["package-db", path] -> Just $ Left $ Text.unpack path
["package-id", unit] -> Just $ Right unit
_ -> Nothing
-- clear-package-db
-- global-package-db
-- package-db /Users/elaforge/.cabal/store/ghc-9.2.5/package.db
-- package-db dist-newstyle/packagedb/ghc-9.2.5
-- package-id hlibgit2-0.18.0.16-inplace
-- package-id base-4.16.4.0
-- package-id bndngs-DSL-1.0.25-d82df022
fromGhcPkg :: IO Index
fromGhcPkg = do
(_, out, err) <- Util.readProcessWithExitCode "ghc-pkg"
["field", "*", "name,exposed,exposed-modules"]
unless (Text.null err) $
IO.hPutStrLn IO.stderr $ "stderr from ghc-pkg: " ++ Text.unpack err
let (errors, index) = parseDump out
unless (null errors) $
IO.hPutStrLn IO.stderr $ "errors parsing ghc-pkg output: "
++ List.intercalate ", " errors
return index
makeIndex :: [(Text, [Types.ModuleName])] -- ^ [(package, modules)]
-> Index
makeIndex packages = Map.fromListWith (++)
[ (qual, [(Text.unpack package, mod)])
| (package, modules) <- packages
, mod <- modules
, qual <- moduleQualifications mod
]
parseDump :: Text -> ([String], Index)
parseDump text = (errors, makeIndex packages)
where
(errors, packages) = Either.partitionEithers $
extractSections (parseGhcPkg text)
extractSections :: [(Text, [Text])]
-> [Either String (Text, [Types.ModuleName])]
extractSections = Maybe.mapMaybe extract . Util.splitWith ((=="name") . fst)
where
extract [ ("name", [name])
, ("exposed", [exposed])
, ("exposed-modules", modules)
]
| exposed /= "True" = Nothing
| otherwise = Just $
Right (name, map (Types.ModuleName . Text.unpack) modules)
-- It may be missing exposed-modules, but that means I don't need it.
extract _ = Nothing
-- | Take a module name to all its possible qualifications, i.e. its list
-- of suffixes.
moduleQualifications :: Types.ModuleName -> [Types.Qualification]
moduleQualifications = map (Types.Qualification . Util.join ".")
. filter (not . null) . List.tails . Util.split "." . Types.moduleName
parseGhcPkg :: Text -> [(Text, [Text])]
parseGhcPkg = map (second (map uncomma . concatMap Text.words)) . parseSections
where
-- Somewhere in 9.2, ghc-pkg switched from space separated to comma
-- separated.
uncomma t = Maybe.fromMaybe t (Text.stripSuffix "," t)
parseSections :: Text -> [(Text, [Text])] -- ^ [(section_name, lines)]
parseSections = List.unfoldr parseSection . stripComments . Text.lines
stripComments :: [Text] -> [Text]
stripComments =
filter (not . Text.null) . map (Text.stripEnd . fst . Text.breakOn "--")
-- | Consume a "tag: xyz" plus indents until the next dedented section.
parseSection :: [Text] -> Maybe ((Text, [Text]), [Text])
parseSection [] = Nothing
parseSection (x:xs) = Just
( (tag, map Text.strip (Text.drop 1 rest : pre))
, post
)
where
(tag, rest) = Text.break (==':') x
(pre, post) = span (" " `Text.isPrefixOf`) xs
-- * read cache