packages feed

graphmod-1.1: src/Main.hs

import Utils

import Data.Dot

import Control.Monad(when,forM_)
import Control.Monad.Fix(mfix)
import Data.List(intersperse)
import Data.Maybe(mapMaybe,isJust)
import System.Environment(getArgs)
import System.IO(hPutStrLn,stderr)
import System.FilePath
import System.Console.GetOpt
import Numeric(showHex)

main = do xs <- getArgs
          let (fs, ms, errs) = getOpt Permute options xs
          case errs of
            [] -> do let opts = foldr ($) default_opts fs
                     g <- graph (add_current opts) (map to_input ms)
                     putStrLn (make_dot (use_clusters opts) g)
            _ -> hPutStrLn stderr $ usageInfo "mods" options


data Input  = File FilePath | Module ModName

-- | Guess if we have a file or a module name
to_input :: String -> Input
to_input m
  | takeExtension m `elem` suffixes = File m
  | otherwise                       = Module (splitModName m)




type Edges  = [(Int,[Int])]

graph :: Opts -> [Input] -> IO (Edges, Trie)
graph opts ms = mfix (\ ~(_,g) -> loop g empty [] 0 ms)
  where
  loop :: Trie -> Trie -> Edges -> Int -> [Input] -> IO (Edges, Trie)
  loop _ done es _ [] = return (es,done)
  loop mods done es size (Module m:ms)
    | isJust (lkp done m) = loop mods done es size ms
    | otherwise =
      do fs <- modToFile (inc_dirs opts) m
         let size1 = size + 1
         seq size1 $ case fs of
           []     -> do warn opts (notFoundMsg m)
                        if with_missing opts
                          then add mods done es size m [] ms
                          else loop mods done es size ms
           f : gs -> do when (not (null gs)) (warn opts (ambigMsg m fs))
                        (x,imps) <- parseFile f
                        add mods done es size x imps ms

  loop mods done es size (File f:ms) =
    do (m,is) <- parseFile f
       case lkp done m of
         Just {} -> loop mods done es size ms
         Nothing -> add mods done es size m is ms

  add mods done es size m imps ms =
    let deps = mapMaybe (lkp mods) imps
        size1 = size + 1
    in size1 `seq` loop mods (ins (m,size) done)
                                  ((size,deps) : es)
                                  size1
                                  (map Module imps ++ ms)



-- We use tries to group modules by directory.
--------------------------------------------------------------------------------

-- | The labels on the nodes correspond to directories,
-- the nodes on the leaves correspond to (unqualified) modules.
-- Eeach module has a unique number.
data Trie = Sub [(String, Trie)] [(String,Int)] deriving Show
empty = Sub [] []

lkp (Sub _ bs) ([],a)   = lookup a bs
lkp (Sub as _) (k:ks,a) = (`lkp` (ks,a)) =<< lookup k as


ins (([],x),n) (Sub as bs)    = Sub as ((x,n):bs)
ins ((a:as,x),n) (Sub ts bs)  = Sub (upd ts) bs
  where new = ((as,x),n)

        upd ((k,t):ts)
          | k < a     = (k,t) : upd ts
          | k == a    = (k, ins new t) : ts
        upd ts = (a, ins new empty) : ts


-- Render edges and a trie into the dot language
--------------------------------------------------------------------------------

make_dot cl (es,t) =
  showDot $
  do if cl then make_clustered_dot 0 t
           else make_unclustered_dot 0 "" t >> return ()
     forM_ es $ \(x,ys) -> forM_ ys $ \y -> userNodeId x .->. userNodeId y


make_clustered_dot c (Sub xs ys) =
  do forM_ ys $ \(xs,n) -> userNode (userNodeId n) [("label",xs)]
     forM_ xs $ \(name,sub) ->
       cluster $
       do attribute ("label", name)
          attribute ("color" , colors !! c)
          attribute ("style", "filled")
          let c1 = c + 1
          c1 `seq` make_clustered_dot c1 sub


make_unclustered_dot c pre (Sub xs ys) =
  do forM_ ys $ \(xs,n) -> userNode (userNodeId n) [ ("label", pre ++ xs)
                                                   , ("color", colors !! c)
                                                   , ("style", "filled")
                                                   ]
     let c1 = if null ys then c else c + 1
     c1 `seq` loop xs c1
  where
  loop ((name,sub):xs) c1 =
    do let pre1 = pre ++ name ++ "."
       c2 <- make_unclustered_dot c1 pre1 sub
       loop xs c2
  loop [] c2 = return c2



-- XXX: generate all?
colors = map col (ys1 ++ ys2) ++ repeat "#cccccc"
  where
  xs1   = [ (1,0,1), (2,0,2), (3,0,3), (2,1,2), (3,1,3), (3,2,3) ]
  xs2   = map rotR xs1
  xs3   = map rotR xs2
  ys1   = xs1 ++ xs2 ++ xs3
  ys2   = map compl (reverse ys1)

  col (x,y,z)   = '#' : (showHex (mk x) $ showHex (mk y) $ showHex (mk z) "")
  mk n          = 0xFF - n * 0x33
  rotR (x,y,z)  = (z,x,y)
  compl (x,y,z) = (3-x,3-y,3-z)

-- Warnings and error messages
--------------------------------------------------------------------------------
err msg             = error ("ERROR: " ++ msg)

warn o _ | quiet o  = return ()
warn _ msg          = hPutStrLn stderr ("WARNING: " ++ msg)

notFoundMsg        :: ModName -> String
notFoundMsg m       = "Cannot find a file for module "
                                      ++ joinModName m ++ " (ignoring)"

ambigMsg           :: ModName -> [FilePath] -> String
ambigMsg m xs       = "Multiple files for module " ++ joinModName m
                   ++ " (picking the first):\n"
                   ++ concat (intersperse "," xs)

-- Command line options
--------------------------------------------------------------------------------
data Opts = Opts
  { inc_dirs :: [FilePath]
  , quiet :: Bool
  , with_missing :: Bool
  , use_clusters :: Bool
  }

default_opts = Opts
  { inc_dirs      = []
  , quiet         = False
  , with_missing  = False
  , use_clusters  = True
  }

add_current o = case inc_dirs o of
                  [] -> o { inc_dirs = ["."] }
                  _  -> o
options =
  [ Option ['q'] ["quiet"] (NoArg set_quiet) "Do not show warnings"
  , Option ['i'] []        (ReqArg add_inc "DIR") "Add a search directory"
  , Option ['a'] ["all"]   (NoArg set_all)   "Add nodes for missing modules"
  , Option []    ["no-cluster"] (NoArg set_no_cluster)
                                             "Do not cluster directories"
  ]

set_quiet o = o { quiet = True }
set_all o   = o { with_missing = True }
add_inc d o = o { inc_dirs = d : inc_dirs o }
set_no_cluster o = o { use_clusters = False }