1-- Copyright (c) 2019 The DAML Authors. All rights reserved.
2-- SPDX-License-Identifier: Apache-2.0
4module Development.IDE.Import.DependencyInformation
5 ( DependencyInformation(..)
7 , RawDependencyInformation(..)
10 , TransitiveDependencies(..)
22 , processDependencyInformation
33import Data.Tuple.Extra hiding (first, second)
34import Development.IDE.GHC.Orphans()
37import Data.HashMap.Strict (HashMap)
38import qualified Data.HashMap.Strict as HMS
39import Data.List.NonEmpty (NonEmpty(..), nonEmpty)
40import qualified Data.List.NonEmpty as NonEmpty
41import Data.IntMap (IntMap)
42import qualified Data.IntMap.Strict as IntMap
43import qualified Data.IntMap.Lazy as IntMapLazy
44import Data.IntSet (IntSet)
45import qualified Data.IntSet as IntSet
48import qualified Data.Set as Set
49import GHC.Generics (Generic)
51import Development.IDE.Types.Diagnostics
52import Development.IDE.Types.Location
53import Development.IDE.Import.FindImports (ArtifactsLocation(..))
58-- | The imports for a given module.
59data ModuleImports = ModuleImports
60 { moduleImports :: ![(Located ModuleName, Maybe FilePathId)]
61 -- ^ Imports of a module in the current package and the file path of
62 -- that module on disk (if we found it)
63 , packageImports :: !(Set InstalledUnitId)
64 -- ^ Transitive package dependencies unioned for all imports.
67-- | For processing dependency information, we need lots of maps and sets
68-- of filepaths. Comparing Strings is really slow, so we work with IntMap/IntSet
69-- instead and only convert at the edges
71newtype FilePathId = FilePathId { getFilePathId :: Int }
72 deriving (Show, NFData, Eq, Ord)
74data PathIdMap = PathIdMap
75 { idToPathMap :: !(IntMap ArtifactsLocation)
76 , pathToIdMap :: !(HashMap NormalizedFilePath FilePathId)
78 deriving (Show, Generic)
80instance NFData PathIdMap
82emptyPathIdMap :: PathIdMap
83emptyPathIdMap = PathIdMap IntMap.empty HMS.empty
85getPathId :: ArtifactsLocation -> PathIdMap -> (FilePathId, PathIdMap)
86getPathId path m@PathIdMap{..} =
87 case HMS.lookup (artifactFilePath path) pathToIdMap of
89 let !newId = FilePathId $ HMS.size pathToIdMap
90 in (newId, insertPathId path newId m)
93insertPathId :: ArtifactsLocation -> FilePathId -> PathIdMap -> PathIdMap
94insertPathId path id PathIdMap{..} =
95 PathIdMap (IntMap.insert (getFilePathId id) path idToPathMap) (HMS.insert (artifactFilePath path) id pathToIdMap)
97insertImport :: FilePathId -> Either ModuleParseError ModuleImports -> RawDependencyInformation -> RawDependencyInformation
98insertImport (FilePathId k) v rawDepInfo = rawDepInfo { rawImports = IntMap.insert k v (rawImports rawDepInfo) }
100pathToId :: PathIdMap -> NormalizedFilePath -> FilePathId
101pathToId PathIdMap{pathToIdMap} path = pathToIdMap HMS.! path
103lookupPathToId :: PathIdMap -> NormalizedFilePath -> Maybe FilePathId
104lookupPathToId PathIdMap{pathToIdMap} path = HMS.lookup path pathToIdMap
106idToPath :: PathIdMap -> FilePathId -> NormalizedFilePath
107idToPath pathIdMap filePathId = artifactFilePath $ idToModLocation pathIdMap filePathId
109idToModLocation :: PathIdMap -> FilePathId -> ArtifactsLocation
110idToModLocation PathIdMap{idToPathMap} (FilePathId id) = idToPathMap IntMap.! id
113type BootIdMap = IntMap FilePathId
115insertBootId :: FilePathId -> FilePathId -> BootIdMap -> BootIdMap
116insertBootId k = IntMap.insert (getFilePathId k)
119-- | Unprocessed results that we find by following imports recursively.
120data RawDependencyInformation = RawDependencyInformation
121 { rawImports :: !(IntMap (Either ModuleParseError ModuleImports))
122 , rawPathIdMap :: !PathIdMap
123 -- The rawBootMap maps the FilePathId of a hs-boot file to its
124 -- corresponding hs file. It is used when topologically sorting as we
125 -- need to add edges between .hs-boot and .hs so that the .hs files
126 -- appear later in the sort.
127 , rawBootMap :: !BootIdMap
130pkgDependencies :: RawDependencyInformation -> IntMap (Set InstalledUnitId)
131pkgDependencies RawDependencyInformation{..} =
132 IntMap.map (either (const Set.empty) packageImports) rawImports
134data DependencyInformation =
135 DependencyInformation
136 { depErrorNodes :: !(IntMap (NonEmpty NodeError))
137 -- ^ Nodes that cannot be processed correctly.
138 , depModuleNames :: !(IntMap ShowableModuleName)
139 , depModuleDeps :: !(IntMap IntSet)
140 -- ^ For a non-error node, this contains the set of module immediate dependencies
141 -- in the same package.
142 , depPkgDeps :: !(IntMap (Set InstalledUnitId))
143 -- ^ For a non-error node, this contains the set of immediate pkg deps.
144 , depPathIdMap :: !PathIdMap
145 -- ^ Map from hs-boot file to the corresponding hs file
146 , depBootMap :: !BootIdMap
147 } deriving (Show, Generic)
149newtype ShowableModuleName =
150 ShowableModuleName {showableModuleName :: ModuleName}
153instance Show ShowableModuleName where show = moduleNameString . showableModuleName
155reachableModules :: DependencyInformation -> [NormalizedFilePath]
156reachableModules DependencyInformation{..} =
157 map (idToPath depPathIdMap . FilePathId) $ IntMap.keys depErrorNodes <> IntMap.keys depModuleDeps
159instance NFData DependencyInformation
161-- | This does not contain the actual parse error as that is already reported by GetParsedModule.
162data ModuleParseError = ModuleParseError
163 deriving (Show, Generic)
165instance NFData ModuleParseError
167-- | Error when trying to locate a module.
168data LocateError = LocateError [Diagnostic]
169 deriving (Eq, Show, Generic)
171instance NFData LocateError
173-- | An error attached to a node in the dependency graph.
175 = PartOfCycle (Located ModuleName) [FilePathId]
176 -- ^ This module is part of an import cycle. The module name corresponds
177 -- to the import that enters the cycle starting from this module.
178 -- The list of filepaths represents the elements
179 -- in the cycle in unspecified order.
180 | FailedToLocateImport (Located ModuleName)
181 -- ^ This module has an import that couldn’t be located.
182 | ParseError ModuleParseError
183 | ParentOfErrorNode (Located ModuleName)
184 -- ^ This module is the parent of a module that cannot be
185 -- processed (either it cannot be parsed, is part of a cycle
186 -- or the parent of another error node).
187 deriving (Show, Generic)
189instance NFData NodeError where
190 rnf (PartOfCycle m fs) = m `seq` rnf fs
191 rnf (FailedToLocateImport m) = m `seq` ()
192 rnf (ParseError e) = rnf e
193 rnf (ParentOfErrorNode m) = m `seq` ()
195-- | A processed node in the dependency graph. If there was any error
196-- during processing the node or any of its dependencies, this is an
197-- `ErrorNode`. Otherwise it is a `SuccessNode`.
199 = ErrorNode (NonEmpty NodeError)
200 | SuccessNode [(Located ModuleName, FilePathId)]
205 -> ([(a, NonEmpty NodeError)], [(a, [(Located ModuleName, FilePathId)])])
206partitionNodeResults = partitionEithers . map f
207 where f (a, ErrorNode errs) = Left (a, errs)
208 f (a, SuccessNode imps) = Right (a, imps)
210instance Semigroup NodeResult where
211 ErrorNode errs <> ErrorNode errs' = ErrorNode (errs <> errs')
212 ErrorNode errs <> SuccessNode _ = ErrorNode errs
213 SuccessNode _ <> ErrorNode errs = ErrorNode errs
214 SuccessNode a <> SuccessNode _ = SuccessNode a
216processDependencyInformation :: RawDependencyInformation -> DependencyInformation
217processDependencyInformation rawDepInfo@RawDependencyInformation{..} =
218 DependencyInformation
219 { depErrorNodes = IntMap.fromList errorNodes
220 , depModuleDeps = moduleDeps
221 , depModuleNames = IntMap.fromList $ coerce moduleNames
222 , depPkgDeps = pkgDependencies rawDepInfo
223 , depPathIdMap = rawPathIdMap
224 , depBootMap = rawBootMap
226 where resultGraph = buildResultGraph rawImports
227 (errorNodes, successNodes) = partitionNodeResults $ IntMap.toList resultGraph
228 moduleNames :: [(FilePathId, ModuleName)]
230 [ (fId, modName) | (_, imports) <- successNodes, (L _ modName, fId) <- imports]
231 successEdges :: [(FilePathId, FilePathId, [FilePathId])]
234 (\(file, imports) -> (FilePathId file, FilePathId file, map snd imports))
238 map (\(_, FilePathId v, vs) -> (v, IntSet.fromList $ coerce vs))
241-- | Given a dependency graph, buildResultGraph detects and propagates errors in that graph as follows:
242-- 1. Mark each node that is part of an import cycle as an error node.
243-- 2. Mark each node that has a parse error as an error node.
244-- 3. Mark each node whose immediate children could not be located as an error.
245-- 4. Recursively propagate errors to parents if they are not already error nodes.
246buildResultGraph :: IntMap (Either ModuleParseError ModuleImports) -> IntMap NodeResult
247buildResultGraph g = propagatedErrors
249 sccs = stronglyConnComp (graphEdges g)
250 (_, cycles) = partitionSCC sccs
251 cycleErrors :: IntMap NodeResult
252 cycleErrors = IntMap.unionsWith (<>) $ map errorsForCycle cycles
253 errorsForCycle :: [FilePathId] -> IntMap NodeResult
254 errorsForCycle files =
255 IntMap.fromListWith (<>) $ coerce $ concatMap (cycleErrorsForFile files) files
256 cycleErrorsForFile :: [FilePathId] -> FilePathId -> [(FilePathId,NodeResult)]
257 cycleErrorsForFile cycle f =
258 let entryPoints = mapMaybe (findImport f) cycle
259 in map (\imp -> (f, ErrorNode (PartOfCycle imp cycle :| []))) entryPoints
260 otherErrors = IntMap.map otherErrorsForFile g
261 otherErrorsForFile :: Either ModuleParseError ModuleImports -> NodeResult
262 otherErrorsForFile (Left err) = ErrorNode (ParseError err :| [])
263 otherErrorsForFile (Right ModuleImports{moduleImports}) =
264 let toEither (imp, Nothing) = Left imp
265 toEither (imp, Just path) = Right (imp, path)
266 (errs, imports') = partitionEithers (map toEither moduleImports)
267 in case nonEmpty errs of
268 Nothing -> SuccessNode imports'
269 Just errs' -> ErrorNode (NonEmpty.map FailedToLocateImport errs')
271 unpropagatedErrors = IntMap.unionWith (<>) cycleErrors otherErrors
272 -- The recursion here is fine since we use a lazy map and
273 -- we only recurse on SuccessNodes. In particular, we do not recurse
274 -- on nodes that are part of a cycle as they are already marked as
277 IntMapLazy.map propagate unpropagatedErrors
278 propagate :: NodeResult -> NodeResult
279 propagate n@(ErrorNode _) = n
280 propagate n@(SuccessNode imps) =
281 let results = map (\(imp, FilePathId dep) -> (imp, propagatedErrors IntMap.! dep)) imps
282 (errs, _) = partitionNodeResults results
283 in case nonEmpty errs of
285 Just errs' -> ErrorNode (NonEmpty.map (ParentOfErrorNode . fst) errs')
286 findImport :: FilePathId -> FilePathId -> Maybe (Located ModuleName)
287 findImport (FilePathId file) importedFile =
288 case g IntMap.! file of
289 Left _ -> error "Tried to call findImport on a module with a parse error"
290 Right ModuleImports{moduleImports} ->
291 fmap fst $ find (\(_, resolvedImp) -> resolvedImp == Just importedFile) moduleImports
293graphEdges :: IntMap (Either ModuleParseError ModuleImports) -> [(FilePathId, FilePathId, [FilePathId])]
295 map (\(k, v) -> (FilePathId k, FilePathId k, deps v)) $ IntMap.toList g
296 where deps :: Either e ModuleImports -> [FilePathId]
298 deps (Right ModuleImports{moduleImports}) = mapMaybe snd moduleImports
300partitionSCC :: [SCC a] -> ([a], [[a]])
301partitionSCC (CyclicSCC xs:rest) = second (xs:) $ partitionSCC rest
302partitionSCC (AcyclicSCC x:rest) = first (x:) $ partitionSCC rest
303partitionSCC [] = ([], [])
306transitiveDeps :: DependencyInformation -> NormalizedFilePath -> Maybe TransitiveDependencies
307transitiveDeps DependencyInformation{..} file = do
308 let !fileId = pathToId depPathIdMap file
310 -- Delete the starting node
311 IntSet.delete (getFilePathId fileId) .
312 IntSet.fromList . map (fst3 . fromVertex) .
313 reachable g <$> toVertex (getFilePathId fileId)
314 let transitiveModuleDepIds =
315 filter (\v -> v `IntSet.member` reachableVs) $ map (fst3 . fromVertex) vs
316 let transitivePkgDeps =
317 Set.toList $ Set.unions $
318 map (\f -> IntMap.findWithDefault Set.empty f depPkgDeps) $
319 getFilePathId fileId : transitiveModuleDepIds
320 let transitiveModuleDeps =
321 map (idToPath depPathIdMap . FilePathId) transitiveModuleDepIds
322 let transitiveNamedModuleDeps =
323 [ NamedModuleDep (idToPath depPathIdMap (FilePathId fid)) mn artifactModLocation
324 | (fid, ShowableModuleName mn) <- IntMap.toList depModuleNames
325 , let ArtifactsLocation{artifactModLocation} = idToPathMap depPathIdMap IntMap.! fid
327 pure TransitiveDependencies {..}
329 (g, fromVertex, toVertex) = graphFromEdges edges
330 edges = map (\(f, fs) -> (f, f, IntSet.toList fs ++ boot_edge f)) $ IntMap.toList depModuleDeps
332 -- Need to add an edge between the .hs and .hs-boot file if it exists
333 -- so the .hs file gets loaded after the .hs-boot file and the right
334 -- stuff ends up in the HPT. If you don't have this check then GHC will
335 -- fail to work with ghcide.
336 boot_edge f = [getFilePathId f' | Just f' <- [IntMap.lookup f depBootMap]]
340data TransitiveDependencies = TransitiveDependencies
341 { transitiveModuleDeps :: [NormalizedFilePath]
342 -- ^ Transitive module dependencies in topological order.
343 -- The module itself is not included.
344 , transitiveNamedModuleDeps :: [NamedModuleDep]
345 -- ^ Transitive module dependencies in topological order.
346 -- The module itself is not included.
347 , transitivePkgDeps :: [InstalledUnitId]
348 -- ^ Transitive pkg dependencies in unspecified order.
349 } deriving (Eq, Show, Generic)
351instance NFData TransitiveDependencies
353data NamedModuleDep = NamedModuleDep {
354 nmdFilePath :: !NormalizedFilePath,
355 nmdModuleName :: !ModuleName,
356 nmdModLocation :: !ModLocation
360instance Eq NamedModuleDep where
361 a == b = nmdFilePath a == nmdFilePath b
363instance NFData NamedModuleDep where
364 rnf NamedModuleDep{..} =
365 rnf nmdFilePath `seq`
366 rnf nmdModuleName `seq`
367 -- 'ModLocation' lacks an 'NFData' instance
370instance Show NamedModuleDep where
371 show NamedModuleDep{..} = show nmdFilePath
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list