packages feed

stack-3.9.1: src/Stack/Dot.hs

{-# LANGUAGE NoImplicitPrelude   #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings   #-}

{-|
Module      : Stack.Dot
Description : Functions related to Stack's @dot@ command.
License     : BSD-3-Clause

Functions related to Stack's @dot@ command.
-}

module Stack.Dot
  ( dotCmd
  , printGraph
  ) where

import qualified Data.Foldable as F
import qualified Data.Set as Set
import qualified Data.Map as Map
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import           Stack.Constants ( wiredInPackages )
import           Stack.DependencyGraph ( createPrunedDependencyGraph )
import           Stack.Prelude
import           Stack.Types.Compiler ( ActualCompiler )
import           Stack.Types.DependencyTree ( DependencyGraph )
import           Stack.Types.DotOpts ( DotOpts (..) )
import           Stack.Types.Runner ( Runner )

-- | Visualize the project's dependencies as a graphviz graph
dotCmd :: DotOpts -> RIO Runner ()
dotCmd dotOpts = do
  (compiler, localNames, prunedGraph) <- createPrunedDependencyGraph dotOpts
  printGraph dotOpts compiler localNames prunedGraph

-- | Print a graphviz graph of the edges in the Map and highlight the given
-- project packages
printGraph ::
     (Applicative m, MonadIO m)
  => DotOpts
  -> ActualCompiler
  -> Set PackageName -- ^ All project packages.
  -> DependencyGraph
  -> m ()
printGraph dotOpts compiler locals graph = do
  liftIO $ Text.putStrLn "strict digraph deps {"
  printLocalNodes dotOpts filteredLocals
  printLeaves compiler graph
  let allNodes = Map.keysSet graph
  void (Map.traverseWithKey (printEdges allNodes) (fst <$> graph))
  liftIO $ Text.putStrLn "}"
 where
  filteredLocals =
    Set.filter (\local' -> local' `Set.notMember` dotOpts.prune) locals

-- | Print the project packages nodes with a different style, depending on
-- options
printLocalNodes ::
     (F.Foldable t, MonadIO m)
  => DotOpts
  -> t PackageName
  -> m ()
printLocalNodes dotOpts locals =
  liftIO $ Text.putStrLn (Text.intercalate "\n" lpNodes)
 where
  applyStyle :: Text -> Text
  applyStyle n = if dotOpts.includeExternal
                   then n <> " [style=dashed];"
                   else n <> " [style=solid];"
  lpNodes :: [Text]
  lpNodes = map (applyStyle . nodeName) (F.toList locals)

-- | Print relevant nodes, based on their relevant attributes.
printLeaves :: MonadIO m => ActualCompiler -> DependencyGraph -> m ()
printLeaves compiler graph =
  F.mapM_ printLeaf (Map.mapWithKey nodeAttributes graph)
 where
  allNodes = Map.keysSet graph
  hasNoNodes = F.all (`Set.notMember` allNodes)
  nodeAttributes package (deps, _) =
    let isWiredInPackage = isWiredIn compiler package
        isBottomRow = hasNoNodes deps
    in  (package, isWiredInPackage, isBottomRow)

-- | @printDedges ps p ps'@ prints an edge from @p@ to every @ps'@, if it is a
-- member of @ps@.
printEdges ::
     MonadIO m
  => Set PackageName
     -- ^ The nodes in the graph.
  -> PackageName
     -- ^ The node in question.
  -> Set PackageName
     -- ^ The dependencies of the node in question.
  -> m ()
printEdges nodes package deps = F.forM_ deps $ \dep ->
  when (dep `elem` nodes) $ printEdge package dep

-- | Print an edge between the two package names
printEdge :: MonadIO m => PackageName -> PackageName -> m ()
printEdge from to' =
  liftIO $ Text.putStrLn (Text.concat [ nodeName from
                                      , " -> "
                                      , nodeName to'
                                      , ";" ])

-- | Convert a package name to a graph node name.
nodeName :: PackageName -> Text
nodeName name = "\"" <> Text.pack (packageNameString name) <> "\""

-- | Print a node if it (a) is a GHC wired-in package or (b) has no dependencies
-- that are also nodes.
printLeaf ::
     MonadIO m
  =>  ( PackageName
      , Bool
        -- Is package a GHC wired-in package?
      , Bool
        -- Does package have no dependencies that are are also nodes in the
        -- graph?
      )
  -> m ()
printLeaf (package, isWiredInPackage, isBottomRow) =
  when (isWiredInPackage || isBottomRow) $
    liftIO . Text.putStrLn . Text.concat $
         [ "{"]
      <> [ "rank=max; " | isBottomRow ]
      <> [ nodeName package ]
      <> [ " [shape=box]" | isWiredInPackage ]
      <> [ "; };" ]

-- | Check if the package is a GHC wired-in package
isWiredIn :: ActualCompiler -> PackageName -> Bool
isWiredIn compiler package =
  package `Set.member` wiredInPackages compiler