hs2dot-0.1.4: src/Hs2Dot/Internals.hs
module Hs2Dot.Internals where
import System.Environment (getArgs)
import System.IO (FilePath)
import Control.Monad (filterM)
import System.Directory (doesFileExist)
import Control.Exception (throw, ErrorCall (..))
import Language.Haskell.Exts (parseFileContentsWithMode)
import Language.Haskell.Exts.Parser
import Language.Haskell.Exts.Pretty (prettyPrint)
import Language.Haskell.Exts.Syntax as E
import Language.Haskell.Syntax
-- import Text.Regex.Posix
import Language.Haskell.Exts.Extension
import Data.List
import Data.Maybe
import Data.List.Split
files2dot :: [FilePath] -> IO String
files2dot files = do
files <- mapM readFile files
let modules = map file2module files
namesInModules = modules >>= moduleNames -- map moduleNames modules
names' = map (\n -> FullName (snd n) (getFullNameId n) (fst n) (fst n) False) namesInModules
-- let code = foldr (++) "" files
return $ header ++ (modules >>= module2dot names') ++ "}" -- foldr (++) [] $ map file2dot files
-- return $ error $ show modules --
-- return $ error $ show names'
file2module :: String -> E.Module
file2module file = res
where
parsedModule = parseFileContentsWithMode (defaultParseMode { extensions = knownExtensions } ) file -- fileCode
res =
case parsedModule of
(ParseFailed _ msg) -> throw $ ErrorCall msg
(ParseOk m) -> m
-- file2dot :: String -> String
-- file2dot file = res
-- where
-- parsedModule = parseFileContents file -- fileCode
-- res =
-- case parsedModule of
-- (ParseFailed _ msg) -> throw $ ErrorCall msg
-- (ParseOk m) -> module2dot m
module2dot :: [FullName] -> E.Module -> String
module2dot names (E.Module srcLoc moduleName opts mwarnings mExps imps decls) = result
where
result =
(subgraphHeader moduleName) ++
(decls >>= (\n -> decl2dot n (getModuleName moduleName) ) allNames) ++
(subgraphFooter)
-- filter away names in modules not imported into this module
allNames = importedNames ++ (mapMaybe (decl2fullname (getModuleName moduleName)) decls)
importedNames = catMaybes $ mapCross (transformNameOnImport $ getModuleName moduleName ) imps names
decl2fullname :: String -> E.Decl -> Maybe FullName
decl2fullname moduleName (E.DataDecl _ _ _ name tyVarBind qualConDecl derivings) =
Just $ FullName name' (dot2Dash moduleName ++ "_" ++ name') moduleName moduleName False
where name' = name2string name
decl2fullname moduleName (E.TypeDecl _ name _ _) = name2fullname moduleName name
decl2fullname moduleName (E.ClassDecl _ _ name _ _ _) = name2fullname moduleName name
decl2fullname _ _ = Nothing
name2fullname :: String -> E.Name -> Maybe FullName
name2fullname moduleName name = Just $ FullName name' (dot2Dash moduleName ++ "_" ++ name') moduleName moduleName False
where name' = name2string name
mapCross :: (a -> b -> c) -> [a] -> [b] -> [c]
mapCross fn xs ys =
concatMap (\x -> (map (fn x) ys)) xs
-- foldr (++) [] $ map (map fn as) bs
-- | Can remove and transform FullName depending on ImportDecl.
transformNameOnImport :: String -> ImportDecl -> FullName -> Maybe FullName
transformNameOnImport nameOfThisModule (ImportDecl iLoc (ModuleName iName) iQual _ _ iAlias iSpecs) fullname = result
where
result = if fullNameModule fullname == nameOfThisModule
then Just fullname
else
if iName /= fullNameModule fullname
then Nothing
else resultOfSpecs
resultOfSpecs =
case iSpecs of
Nothing -> Just $ resultOfQualified
Just specs -> filterImportedSpecs specs
filterImportedSpecs :: (Bool, [ImportSpec]) -> Maybe FullName
filterImportedSpecs (hide, specs) =
if any (\i -> compare (importSpecName i) (fullNameDecl fullname)) specs -- include?
then Just $ resultOfQualified
else Nothing
where compare = if hide then (/=) else (==)
-- filterImportedSpecs False specs = any (\i -> importSpecName i == fullNameDecl fullname) specs -- filter away if not in list
resultOfQualified :: FullName
resultOfQualified =
case iAlias of
Nothing -> fullname
Just (ModuleName n) -> fullname { fullNamePrefix = n, fullNameQualified = True }
importSpecName :: ImportSpec -> String
importSpecName (IVar n) = prettyPrint n
importSpecName (IAbs n) = prettyPrint n
importSpecName (IThingAll n) = prettyPrint n
importSpecName (IThingWith n _) = prettyPrint n
getModuleName :: E.ModuleName -> String
getModuleName (E.ModuleName n) = n
getFullNameId :: (String, String) -> String
getFullNameId (modName, decName) =
dot2Dash modName ++ "_" ++ decName
data FullName =
FullName {
fullNameDecl :: String, -- name used in analyzed module
fullNameId :: String, -- unique id in dot-file, original-module-name + original decl name
fullNameModule :: String, -- original name of module
fullNamePrefix :: String, -- prefix of module in analyzed module
fullNameQualified :: Bool -- if is qualified import
}
deriving (Show, Eq)
subgraphHeader :: ModuleName -> String
subgraphHeader (ModuleName n) = "subgraph cluster_" ++ n' ++ " {\ncolor=lightgrey;\nlabel = \"" ++ n ++ "\";\n"
where n' = dot2Dash n
dot2Dash :: String -> String
dot2Dash value = map (\v -> if v == '.' then '_' else v) value
escapeName :: String -> String
escapeName value = value >>= (\v -> if v == '>' then ['\\',v] else [v])
subgraphFooter :: String
subgraphFooter = "}\n"
moduleNames :: E.Module -> [(String,String)]
moduleNames (E.Module _ (ModuleName moduleName) _ _ _ _ decls) = zip (repeat moduleName) $ mapMaybe declName decls
declName :: E.Decl -> Maybe String
declName (E.DataDecl _ _ _ name _ _ _ ) = Just $ name2string name
declName (E.TypeDecl _ name _ _ ) = Just $ name2string name
declName (E.ClassDecl _ _ name _ _ _) = Just $ name2string name
declName _ = Nothing
decl2dot :: [FullName] -> String -> E.Decl -> String
decl2dot names moduleName (E.TypeDecl _ name tyVarBind t ) = result
where
result = name'
name' = showTypeDecl (name2string name) moduleName [] [("",t')] -- []
t' = escapeName $ prettyPrint t
decl2dot names moduleName (E.ClassDecl _ _ name _ _ _) = name'
where
name' = showClassDecl (name2string name) moduleName [] []
decl2dot names moduleName (E.InstDecl _ _ qname types _) = show'
where
show' = if 1 == length types && isNameInNames name' names && isNameInNames dataTypeName names
then showRef'
else ""
showRef' = showArrow "odiamond" "datadecl" moduleName (prettyPrint $ head types) "datadecl" name' (fullNameModule fullClassName)
name' = prettyPrint qname -- change this!!!
dataTypeName = prettyPrint $ head types
fullClassName = getFullNameForName name' names
decl2dot names moduleName (E.DataDecl _ _ context name tyVarBind qualConDecl derivings) = result
where
result = name' ++ qualConDecl' ++ asst' -- showNode name [] []
name' = showDataDecl dataTypeRefs (name2string name) moduleName [] []
asst' = context >>= (asst2dot names (prettyPrint name) moduleName)
qualConDecl' = qualConDecl >>= (\c -> qualConDecl2dot names c moduleName)
dataTypeRefs :: [String]
dataTypeRefs = map getDataTypeRefs qualConDecl
getDataTypeRefs (E.QualConDecl _ _ _ c) = getConDeclName c
getConDeclName (E.ConDecl name _) = name2string name
getConDeclName (E.InfixConDecl _ name _ ) = name2string name
getConDeclName (E.RecDecl name _ ) = name2string name
-- node for name
-- node per qualConDecl
-- -> (inheritance) per qualConDecl to name
decl2dot _ _ _ = ""
qualConDecl2dot :: [FullName] -> E.QualConDecl -> String -> String
-- qualConDecl2dot = show --(E.QualConDecl
qualConDecl2dot names (E.QualConDecl _ _ _ c) moduleName = conDecl2dot names c moduleName
conDecl2dot :: [FullName] -> E.ConDecl -> String -> String
conDecl2dot names (E.ConDecl name bangTypes) moduleName =
showConDecl names (name2string name) moduleName [] $ map (\b -> ("",bangType2String b)) bangTypes
conDecl2dot names (E.InfixConDecl bangTypeL name bangTypeR) moduleName =
showConDecl names (name2string name) moduleName [] [("",bangType2String bangTypeL),("", bangType2String bangTypeR) ]
conDecl2dot names (E.RecDecl name nameBangTypes) moduleName =
showConDecl names (name2string name) moduleName [] $ map nameBangType2String nameBangTypes
asst2dot :: [FullName] -> String -> String -> Asst -> String
asst2dot fullnames declname moduleName (ClassA qname types) =
if isNameInNames classname fullnames
then varnames >>= (asst2dot' (getFullNameForName classname fullnames))
else error $classname ++ show ( length fullnames) -- ""
where
classname = prettyPrint qname
varnames = map prettyPrint types
asst2dot' :: FullName -> String -> String
asst2dot' fullname varname =
showArrow "diamond" "datadecl" moduleName declname "datadecl" (classname) (fullNameModule fullname)
-- showArrow :: String -> String -> String -> String -> String -> String -> String -> String
-- showArrow arrowHead fromPrefix fromModuleName fromDataRelation toPrefix toName toModuleName =
asst2dot _ _ _ _ = ""
bangType2String :: E.BangType -> String
bangType2String = prettyPrint
nameBangType2String :: ([Name],E.BangType) -> (String, String)
nameBangType2String (names,bangType) = (maybe "" prettyPrint $ listToMaybe names, prettyPrint bangType)
name2string :: E.Name -> String
name2string (E.Symbol n) = n
name2string (E.Ident n) = n
arrow2dot :: E.Decl -> String
arrow2dot = error "Not implemented yet."
showConDecl = showNode "record" "condecl" []
showDataDecl refs = showNode "record" "datadecl" refs []
showClassDecl = showNode "record" "datadecl" [] []
showTypeDecl = showNode "record" "datadecl" [] []
showNode :: String -> String -> [String] -> [FullName] -> String -> String -> [String] -> [(String,String)] -> String
showNode shape prefix refs names name moduleName instances records =
"\"" ++ prefix ++ "_" ++ (dot2Dash moduleName) ++ "_" ++ name ++ "\" [\n" ++
"label = \"<f0> " ++ name ++ (foldl (++) "" $ map showLabel recs) ++ "\"" ++ "\n" ++
"shape = \"" ++ shape ++ "\"" ++ "\n" ++
"];\n" ++
(recsForLine >>= showRecord') ++
(refs >>= showRef')
where
recs = zip [1..] records
recsForLine = zip [1..] $ filter (isRecordInNames names) records
showRef' = showRef "condecl" moduleName name
showRecord' r = showRecord prefix name moduleName (getModuleNameForRecord r names ) r
isRecordInNames :: [FullName] -> (String,String) -> Bool
isRecordInNames names record = result
where
result = any (isRecordInName record) names -- (\n -> fullNameDecl n == snd record) names
isNameInNames :: String -> [FullName] -> Bool
isNameInNames name = any (isNameTheName name)
isNameTheName :: String -> FullName -> Bool
isNameTheName left fullname =
if fullNameQualified fullname || modulePart /= ""
then sameName && fullNamePrefix fullname == modulePart
else sameName
where
sameName = fullNameDecl fullname == dataPart
splitedName = splitOn "." left
modulePart = if 1 < length splitedName
then head $ init splitedName
else ""
dataPart = if 1 < length splitedName
then last splitedName
else head $ splitedName
isRecordInName record name =
if fullNameQualified name || modulePart /= ""
then sameName && fullNamePrefix name == modulePart -- fst record
else sameName
where
sameName = fullNameDecl name == dataPart -- splitedName -- snd record
splitedName = splitOn "." (snd record)
modulePart :: String
modulePart = if 1 < length splitedName
then head $ init splitedName
else ""
dataPart :: String
dataPart = if 1 < length splitedName
then last splitedName
else head $ splitedName
getFullNameForName :: String -> [FullName] -> FullName
getFullNameForName name fullnames = fullname
where
fullname = fromJust $ find (isNameTheName name) fullnames
getModuleNameForRecord :: (Integer, (String,String)) -> [FullName] -> String
getModuleNameForRecord (i, (key,value)) fullNames = result
where
result = fullNameId fullname
fullname = fromJust $ find (isRecordInName (key,value)) fullNames
showRef :: String -> String -> String -> String -> String
showRef prefix moduleName name dataRelation =
"\""++ prefix ++"_" ++ (dot2Dash moduleName) ++ "_" ++ dataRelation ++ "\":f"++ show 0 ++" -> \"datadecl_"++ (dot2Dash moduleName) ++ "_" ++ name ++"\":f0 ["++ arrowhead ++ "];\n"
where
arrowhead = "arrowhead=onormal"
showArrow :: String -> String -> String -> String -> String -> String -> String -> String
showArrow arrowHead fromPrefix fromModuleName fromDataRelation toPrefix toName toModuleName =
"\""++ fromPrefix ++"_" ++ (dot2Dash fromModuleName) ++ "_" ++ fromDataRelation ++ "\":f"++ show 0 ++" -> \""++ toPrefix ++"_"++ (dot2Dash toModuleName) ++ "_" ++ toName ++"\":f0 ["++ arrowhead ++ "];\n"
where
arrowhead = "arrowhead=" ++ arrowHead
showLabel :: (Integer, (String, String)) -> String
showLabel (i, (key,value)) =
" | <f" ++ show i ++ "> " ++ key ++ " :: " ++ dataPart
where
splitedValue = splitOn "." value
dataPart :: String
dataPart = last $ splitedValue
showRecord :: String -> String -> String -> String -> (Integer, (String,String)) -> String
showRecord prefix dataRelation moduleName valueModuleName (i, (key, value)) =
"\""++ prefix ++"_" ++ (dot2Dash moduleName) ++ "_" ++ dataRelation ++ "\":f"++ show i ++" -> \"datadecl_"++ valueModuleName ++"\":f0 [];\n"
header =
"digraph g {\n" ++
"graph [\n" ++
"rankdir = \"LR\"\n" ++
"];\n" ++
"node [\n" ++
"fontsize = \"16\"\n" ++
"shape = \"ellipse\"\n" ++
"];\n" ++
"edge [\n" ++
"];\n"