packages feed

graphmod-1.4.5.1: src/Graphmod/Utils.hs

module Graphmod.Utils
  ( parseFile
  , parseString
  , Qualifier
  , qualifierNodes
  , fromHierarchy
  , Import(..)
  , ImpType(..)
  , splitQualifier
  , ModName
  , splitModName
  , joinModName
  , relPaths
  , modToFile
  , suffixes
  ) where

import Language.Haskell.Lexer(lexerPass0,Token(..),PosToken,line)

import Control.Monad(mplus, filterM)
import Control.Exception(evaluate)
import Data.List(intercalate,isPrefixOf,nub)
import System.Directory(doesFileExist)
import qualified System.IO as IO
import System.FilePath

data Import = Import { impMod :: ModName, impType :: ImpType }
                deriving (Show, Eq)

data ImpType = NormalImp | SourceImp
                deriving (Show,Eq,Ord)

-- | Get the imports of a file.
parseFile          :: FilePath -> IO (ModName,[Import])
parseFile f =
  do h <- IO.openFile f IO.ReadMode
     IO.hSetEncoding h IO.utf8
     (modName, imps) <- (parseString . get_text) `fmap` IO.hGetContents h
     _ <- evaluate (length imps) -- this is here so that the file gets closed
     IO.hClose h
     if ext == ".imports"
       then return (splitModName (takeBaseName f), imps)
       else case modName of
            -- disambiguate Main modules with no qualifiers
            (Hierarchy [],"Main") -> return (splitFilePath f,imps)
            _ -> return (modName, imps)


  where get_text txt = if ext == ".lhs" then delit txt else txt
        ext          = takeExtension f

-- | Get the imports from a string that represents a program.
parseString        :: String -> (ModName,[Import])
parseString         = parse . dropApproxCPP . dropComments . lexerPass0


-- | Drop comments, but keep {-# SOURCE #-} pragmas.
dropComments :: [PosToken] -> [PosToken]
dropComments = filter (not . skip)
  where
  skip (t, (_,txt))
    |  t == Whitespace
    || t == Commentstart
    || t == Comment
    || t == LiterateComment = True
    |  t == NestedComment   = not (isSourcePragma txt)
    | otherwise             = False


isSourcePragma :: String -> Bool
isSourcePragma txt = case words txt of
                       ["{-#", "SOURCE", "#-}"] -> True
                       _                        -> False


dropApproxCPP :: [PosToken] -> [PosToken]

 -- this is some artifact of the lexer
dropApproxCPP ((_, (_,"")) : more) = dropApproxCPP more

dropApproxCPP ((Varsym, (_,"#")) : (_, (pos,tok)) : more)
  | tok `elem` [ "if", "ifdef", "ifndef" ] = dropToEndif more
  | tok `elem` [ "include", "define", "undef" ] = dropToEOL more
  where
  dropToEndif ((Varsym, (_,"#")) : (_, (_,"endif")) : rest)
                         = dropApproxCPP rest
  dropToEndif (_ : rest) = dropToEndif rest
  dropToEndif []         = []

  dropToEOL ((_, (pos1,_)) : rest)
    | line pos == line pos1 = dropToEOL rest
  dropToEOL xs  = dropApproxCPP xs

dropApproxCPP (x : xs) = x : dropApproxCPP xs
dropApproxCPP []       = []


-- 'import' maybe_src maybe_safe optqualified maybe_pkg modid
--                                                        maybeas maybeimpspec
isImp :: [PosToken] -> Maybe (Import, [PosToken])
isImp ts = attempt (1::Int) (drop 1 ts)
  where
  attempt n toks
    -- import safe qualified "package" ModId
    | n > 4     = Nothing
    | otherwise = mplus (isMod toks) (attempt (n+1) (drop 1 toks))

  isMod ((ty, (_,x)) : xs) = case ty of
                               Conid  -> Just (toImp x,xs)
                               Qconid -> Just (toImp x,xs)
                               _      -> Nothing
  isMod _                   = Nothing

  toImp x = Import { impMod = splitModName x, impType = isSrc }
  isSrc   = case ts of
              _ : (_,(_,x)) : _ | isSourcePragma x -> SourceImp
              _                                    -> NormalImp



parse              :: [PosToken] -> (ModName,[Import])
parse ((Reservedid,(_,"module")) : (_,(_,m)) : is) =
                                                  (splitModName m,imports is)
parse is            = ((Hierarchy [],"Main"),imports is)
-- TODO: special handling for Main modules, 
-- to disambiguate multiple main Modules in a single project

imports            :: [PosToken] -> [Import]
imports ts          = case isImp $ dropWhile (not . (("import" ==) . snd . snd)) ts of
                        Just (x,xs) -> x : imports xs
                        _           -> []

-- | A hierarchical module name.
-- We make this an opaque type with accessors 'qualifierNodes' and 'fromHierarchy' 
-- so that we can transparently add new structure to this type.
data Qualifier = Hierarchy [String]
    | FromFile [String] deriving (Show, Eq)
qualifierNodes :: Qualifier -> [String]
qualifierNodes (Hierarchy qs) = qs
qualifierNodes (FromFile qs) = qs
fromHierarchy :: [String] -> Qualifier
fromHierarchy = Hierarchy

type ModName        = (Qualifier,String)

-- | Convert a string name into a hierarchical name qualifier.
splitQualifier     :: String -> Qualifier
splitQualifier cs = case break ('.'==) cs of
                        (xs,_:ys)  -> let Hierarchy qs = splitQualifier ys
                            in Hierarchy (xs:qs)
                        _          -> Hierarchy [cs]

-- | The 'Qualifier' for a Main module is the path leading to it, 
-- the module name is the file's basename, which is Main in typical cases.  
splitFilePath :: FilePath -> ModName
splitFilePath path = let (d,f) = splitFileName path
    in (FromFile . splitDirectories . takeDirectory $ d, dropExtensions f)

-- | Convert a string name into a hierarchical name.
-- It is important that 
-- 
-- @
-- f `elem` (('relPaths' . 'splitFilePath') f)
-- @
splitModName       :: String -> ModName
splitModName cs     = case break ('.'==) cs of
                        (xs,_:ys)  -> let (Hierarchy as,bs) = splitModName ys
                                   in (Hierarchy (xs:as),bs)
                        _ -> (Hierarchy [],cs)

joinModName        :: ModName -> String
joinModName (q,y)  = intercalate "." (qualifierNodes q ++ [y])

-- | The files in which a module might reside.
relPaths           :: ModName -> [FilePath]
relPaths (q,y)     = [ prefix ++ suffix | suffix <- suffixes ]
  where prefix      = foldr (</>) y (qualifierNodes q)

suffixes           :: [String]
suffixes            = [".hs",".lhs", ".imports"]

-- | The files in which a module might reside.
-- We report only files that exist.
modToFile          :: [FilePath] -> ModName -> IO [FilePath]
modToFile dirs m    = nub `fmap` filterM doesFileExist paths
  where
  paths             = [ d </> r | d <- dirs, r <- relPaths m ]


delit :: String -> String
delit txt = unlines $ bird $ lines txt
  where
  bird (('>' : cs) : ls)  = (' ' : cs) : bird ls
  bird (l : ls)
    | "\\begin{code}" `isPrefixOf` l  = in_code ls
    | otherwise                       = bird ls
  bird []                             = []

  in_code (l : ls)
    | "\\end{code}" `isPrefixOf` l    = bird ls
    | otherwise                       = l : in_code ls
  in_code []                          = []    -- unterminated code...