module Development.IDE.Import.DependencyInformation
( DependencyInformation(..)
, ModuleImports(..)
, RawDependencyInformation(..)
, NodeError(..)
, ModuleParseError(..)
, TransitiveDependencies(..)
, FilePathId(..)
, NamedModuleDep(..)
, PathIdMap
, emptyPathIdMap
, getPathId
, lookupPathToId
, insertImport
, pathToId
, idToPath
, reachableModules
, processDependencyInformation
, transitiveDeps
, BootIdMap
, insertBootId
) where
import Control.DeepSeq
import Data.Bifunctor
import Data.Coerce
import Data.List
import Data.Tuple.Extra hiding (first, second)
import Development.IDE.GHC.Orphans()
import Data.Either
import Data.Graph
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HMS
import Data.List.NonEmpty (NonEmpty(..), nonEmpty)
import qualified Data.List.NonEmpty as NonEmpty
import Data.IntMap (IntMap)
import qualified Data.IntMap.Strict as IntMap
import qualified Data.IntMap.Lazy as IntMapLazy
import Data.IntSet (IntSet)
import qualified Data.IntSet as IntSet
import Data.Maybe
import Data.Set (Set)
import qualified Data.Set as Set
import GHC.Generics (Generic)
import Development.IDE.Types.Diagnostics
import Development.IDE.Types.Location
import Development.IDE.Import.FindImports (ArtifactsLocation(..))
import GHC
import Module
data ModuleImports = ModuleImports
{ moduleImports :: ![(Located ModuleName, Maybe FilePathId)]
, packageImports :: !(Set InstalledUnitId)
} deriving Show
newtype FilePathId = FilePathId { getFilePathId :: Int }
deriving (Show, NFData, Eq, Ord)
type FilePathIdMap = IntMap
type FilePathIdSet = IntSet
data PathIdMap = PathIdMap
{ idToPathMap :: !(FilePathIdMap ArtifactsLocation)
, pathToIdMap :: !(HashMap NormalizedFilePath FilePathId)
}
deriving (Show, Generic)
instance NFData PathIdMap
emptyPathIdMap :: PathIdMap
emptyPathIdMap = PathIdMap IntMap.empty HMS.empty
getPathId :: ArtifactsLocation -> PathIdMap -> (FilePathId, PathIdMap)
getPathId path m@PathIdMap{..} =
case HMS.lookup (artifactFilePath path) pathToIdMap of
Nothing ->
let !newId = FilePathId $ HMS.size pathToIdMap
in (newId, insertPathId path newId m)
Just id -> (id, m)
insertPathId :: ArtifactsLocation -> FilePathId -> PathIdMap -> PathIdMap
insertPathId path id PathIdMap{..} =
PathIdMap (IntMap.insert (getFilePathId id) path idToPathMap) (HMS.insert (artifactFilePath path) id pathToIdMap)
insertImport :: FilePathId -> Either ModuleParseError ModuleImports -> RawDependencyInformation -> RawDependencyInformation
insertImport (FilePathId k) v rawDepInfo = rawDepInfo { rawImports = IntMap.insert k v (rawImports rawDepInfo) }
pathToId :: PathIdMap -> NormalizedFilePath -> FilePathId
pathToId PathIdMap{pathToIdMap} path = pathToIdMap HMS.! path
lookupPathToId :: PathIdMap -> NormalizedFilePath -> Maybe FilePathId
lookupPathToId PathIdMap{pathToIdMap} path = HMS.lookup path pathToIdMap
idToPath :: PathIdMap -> FilePathId -> NormalizedFilePath
idToPath pathIdMap filePathId = artifactFilePath $ idToModLocation pathIdMap filePathId
idToModLocation :: PathIdMap -> FilePathId -> ArtifactsLocation
idToModLocation PathIdMap{idToPathMap} (FilePathId id) = idToPathMap IntMap.! id
type BootIdMap = FilePathIdMap FilePathId
insertBootId :: FilePathId -> FilePathId -> BootIdMap -> BootIdMap
insertBootId k = IntMap.insert (getFilePathId k)
data RawDependencyInformation = RawDependencyInformation
{ rawImports :: !(FilePathIdMap (Either ModuleParseError ModuleImports))
, rawPathIdMap :: !PathIdMap
, rawBootMap :: !BootIdMap
} deriving Show
pkgDependencies :: RawDependencyInformation -> FilePathIdMap (Set InstalledUnitId)
pkgDependencies RawDependencyInformation{..} =
IntMap.map (either (const Set.empty) packageImports) rawImports
data DependencyInformation =
DependencyInformation
{ depErrorNodes :: !(FilePathIdMap (NonEmpty NodeError))
, depModuleNames :: !(FilePathIdMap ShowableModuleName)
, depModuleDeps :: !(FilePathIdMap FilePathIdSet)
, depPkgDeps :: !(FilePathIdMap (Set InstalledUnitId))
, depPathIdMap :: !PathIdMap
, depBootMap :: !BootIdMap
} deriving (Show, Generic)
newtype ShowableModuleName =
ShowableModuleName {showableModuleName :: ModuleName}
deriving NFData
instance Show ShowableModuleName where show = moduleNameString . showableModuleName
reachableModules :: DependencyInformation -> [NormalizedFilePath]
reachableModules DependencyInformation{..} =
map (idToPath depPathIdMap . FilePathId) $ IntMap.keys depErrorNodes <> IntMap.keys depModuleDeps
instance NFData DependencyInformation
data ModuleParseError = ModuleParseError
deriving (Show, Generic)
instance NFData ModuleParseError
data LocateError = LocateError [Diagnostic]
deriving (Eq, Show, Generic)
instance NFData LocateError
data NodeError
= PartOfCycle (Located ModuleName) [FilePathId]
| FailedToLocateImport (Located ModuleName)
| ParseError ModuleParseError
| ParentOfErrorNode (Located ModuleName)
deriving (Show, Generic)
instance NFData NodeError where
rnf (PartOfCycle m fs) = m `seq` rnf fs
rnf (FailedToLocateImport m) = m `seq` ()
rnf (ParseError e) = rnf e
rnf (ParentOfErrorNode m) = m `seq` ()
data NodeResult
= ErrorNode (NonEmpty NodeError)
| SuccessNode [(Located ModuleName, FilePathId)]
deriving Show
partitionNodeResults
:: [(a, NodeResult)]
-> ([(a, NonEmpty NodeError)], [(a, [(Located ModuleName, FilePathId)])])
partitionNodeResults = partitionEithers . map f
where f (a, ErrorNode errs) = Left (a, errs)
f (a, SuccessNode imps) = Right (a, imps)
instance Semigroup NodeResult where
ErrorNode errs <> ErrorNode errs' = ErrorNode (errs <> errs')
ErrorNode errs <> SuccessNode _ = ErrorNode errs
SuccessNode _ <> ErrorNode errs = ErrorNode errs
SuccessNode a <> SuccessNode _ = SuccessNode a
processDependencyInformation :: RawDependencyInformation -> DependencyInformation
processDependencyInformation rawDepInfo@RawDependencyInformation{..} =
DependencyInformation
{ depErrorNodes = IntMap.fromList errorNodes
, depModuleDeps = moduleDeps
, depModuleNames = IntMap.fromList $ coerce moduleNames
, depPkgDeps = pkgDependencies rawDepInfo
, depPathIdMap = rawPathIdMap
, depBootMap = rawBootMap
}
where resultGraph = buildResultGraph rawImports
(errorNodes, successNodes) = partitionNodeResults $ IntMap.toList resultGraph
moduleNames :: [(FilePathId, ModuleName)]
moduleNames =
[ (fId, modName) | (_, imports) <- successNodes, (L _ modName, fId) <- imports]
successEdges :: [(FilePathId, FilePathId, [FilePathId])]
successEdges =
map
(\(file, imports) -> (FilePathId file, FilePathId file, map snd imports))
successNodes
moduleDeps =
IntMap.fromList $
map (\(_, FilePathId v, vs) -> (v, IntSet.fromList $ coerce vs))
successEdges
buildResultGraph :: FilePathIdMap (Either ModuleParseError ModuleImports) -> FilePathIdMap NodeResult
buildResultGraph g = propagatedErrors
where
sccs = stronglyConnComp (graphEdges g)
(_, cycles) = partitionSCC sccs
cycleErrors :: IntMap NodeResult
cycleErrors = IntMap.unionsWith (<>) $ map errorsForCycle cycles
errorsForCycle :: [FilePathId] -> IntMap NodeResult
errorsForCycle files =
IntMap.fromListWith (<>) $ coerce $ concatMap (cycleErrorsForFile files) files
cycleErrorsForFile :: [FilePathId] -> FilePathId -> [(FilePathId,NodeResult)]
cycleErrorsForFile cycle f =
let entryPoints = mapMaybe (findImport f) cycle
in map (\imp -> (f, ErrorNode (PartOfCycle imp cycle :| []))) entryPoints
otherErrors = IntMap.map otherErrorsForFile g
otherErrorsForFile :: Either ModuleParseError ModuleImports -> NodeResult
otherErrorsForFile (Left err) = ErrorNode (ParseError err :| [])
otherErrorsForFile (Right ModuleImports{moduleImports}) =
let toEither (imp, Nothing) = Left imp
toEither (imp, Just path) = Right (imp, path)
(errs, imports') = partitionEithers (map toEither moduleImports)
in case nonEmpty errs of
Nothing -> SuccessNode imports'
Just errs' -> ErrorNode (NonEmpty.map FailedToLocateImport errs')
unpropagatedErrors = IntMap.unionWith (<>) cycleErrors otherErrors
propagatedErrors =
IntMapLazy.map propagate unpropagatedErrors
propagate :: NodeResult -> NodeResult
propagate n@(ErrorNode _) = n
propagate n@(SuccessNode imps) =
let results = map (\(imp, FilePathId dep) -> (imp, propagatedErrors IntMap.! dep)) imps
(errs, _) = partitionNodeResults results
in case nonEmpty errs of
Nothing -> n
Just errs' -> ErrorNode (NonEmpty.map (ParentOfErrorNode . fst) errs')
findImport :: FilePathId -> FilePathId -> Maybe (Located ModuleName)
findImport (FilePathId file) importedFile =
case g IntMap.! file of
Left _ -> error "Tried to call findImport on a module with a parse error"
Right ModuleImports{moduleImports} ->
fmap fst $ find (\(_, resolvedImp) -> resolvedImp == Just importedFile) moduleImports
graphEdges :: FilePathIdMap (Either ModuleParseError ModuleImports) -> [(FilePathId, FilePathId, [FilePathId])]
graphEdges g =
map (\(k, v) -> (FilePathId k, FilePathId k, deps v)) $ IntMap.toList g
where deps :: Either e ModuleImports -> [FilePathId]
deps (Left _) = []
deps (Right ModuleImports{moduleImports}) = mapMaybe snd moduleImports
partitionSCC :: [SCC a] -> ([a], [[a]])
partitionSCC (CyclicSCC xs:rest) = second (xs:) $ partitionSCC rest
partitionSCC (AcyclicSCC x:rest) = first (x:) $ partitionSCC rest
partitionSCC [] = ([], [])
transitiveDeps :: DependencyInformation -> NormalizedFilePath -> Maybe TransitiveDependencies
transitiveDeps DependencyInformation{..} file = do
let !fileId = pathToId depPathIdMap file
reachableVs <-
IntSet.delete (getFilePathId fileId) .
IntSet.fromList . map (fst3 . fromVertex) .
reachable g <$> toVertex (getFilePathId fileId)
let transitiveModuleDepIds =
filter (\v -> v `IntSet.member` reachableVs) $ map (fst3 . fromVertex) vs
let transitivePkgDeps =
Set.toList $ Set.unions $
map (\f -> IntMap.findWithDefault Set.empty f depPkgDeps) $
getFilePathId fileId : transitiveModuleDepIds
let transitiveModuleDeps =
map (idToPath depPathIdMap . FilePathId) transitiveModuleDepIds
let transitiveNamedModuleDeps =
[ NamedModuleDep (idToPath depPathIdMap (FilePathId fid)) mn artifactModLocation
| (fid, ShowableModuleName mn) <- IntMap.toList depModuleNames
, let ArtifactsLocation{artifactModLocation} = idToPathMap depPathIdMap IntMap.! fid
]
pure TransitiveDependencies {..}
where
(g, fromVertex, toVertex) = graphFromEdges edges
edges = map (\(f, fs) -> (f, f, IntSet.toList fs ++ boot_edge f)) $ IntMap.toList depModuleDeps
boot_edge f = [getFilePathId f' | Just f' <- [IntMap.lookup f depBootMap]]
vs = topSort g
data TransitiveDependencies = TransitiveDependencies
{ transitiveModuleDeps :: [NormalizedFilePath]
, transitiveNamedModuleDeps :: [NamedModuleDep]
, transitivePkgDeps :: [InstalledUnitId]
} deriving (Eq, Show, Generic)
instance NFData TransitiveDependencies
data NamedModuleDep = NamedModuleDep {
nmdFilePath :: !NormalizedFilePath,
nmdModuleName :: !ModuleName,
nmdModLocation :: !ModLocation
}
deriving Generic
instance Eq NamedModuleDep where
a == b = nmdFilePath a == nmdFilePath b
instance NFData NamedModuleDep where
rnf NamedModuleDep{..} =
rnf nmdFilePath `seq`
rnf nmdModuleName `seq`
rwhnf nmdModLocation
instance Show NamedModuleDep where
show NamedModuleDep{..} = show nmdFilePath