packages feed

nirum-0.2.0: src/Nirum/Package.hs

module Nirum.Package ( BoundModule(boundPackage, modulePath)
                     , ImportError ( CircularImportError
                                   , MissingImportError
                                   , MissingModulePathError
                                   )
                     , Package(modules)
                     , PackageError(ImportError, ParseError, ScanError)
                     , TypeLookup(Imported, Local, Missing)
                     , docs
                     , findInBoundModule
                     , lookupType
                     , makePackage
                     , resolveBoundModule
                     , resolveModule
                     , scanModules
                     , scanPackage
                     , types
                     ) where

import qualified Data.Map.Strict as M
import qualified Data.Set as S
import System.Directory (doesDirectoryExist, listDirectory)
import System.FilePath ((</>))

import Nirum.Constructs.Docs (Docs)
import qualified Nirum.Constructs.DeclarationSet as DS
import Nirum.Constructs.Identifier (Identifier)
import qualified Nirum.Constructs.Module as Mod
import Nirum.Constructs.ModulePath (ModulePath, fromFilePath)
import Nirum.Constructs.TypeDeclaration ( Type
                                        , TypeDeclaration ( Import
                                                          , ServiceDeclaration
                                                          , TypeDeclaration
                                                          , type'
                                                          )
                                        )
import Nirum.Parser (ParseError, parseFile)

-- | Represents a package which consists of modules.
data Package = Package { modules :: M.Map ModulePath Mod.Module
                       } deriving (Eq, Ord, Show)
-- TODO: uri, version, dependencies

data ImportError = CircularImportError [ModulePath]
                 | MissingModulePathError ModulePath ModulePath
                 | MissingImportError ModulePath ModulePath Identifier
                 deriving (Eq, Ord, Show)

makePackage :: M.Map ModulePath Mod.Module -> Either (S.Set ImportError) Package
makePackage modules'
    | S.null importErrors = Right package
    | otherwise = Left importErrors
  where
    package :: Package
    package = Package modules'
    importErrors :: S.Set ImportError
    importErrors = detectImportErrors package

resolveModule :: ModulePath -> Package -> Maybe Mod.Module
resolveModule path Package { modules = ms } = M.lookup path ms

resolveBoundModule :: ModulePath -> Package -> Maybe BoundModule
resolveBoundModule path package =
    case resolveModule path package of
        Just _ -> Just $ BoundModule package path
        Nothing -> Nothing

detectImportErrors :: Package -> S.Set ImportError
detectImportErrors package = detectMissingImports package `S.union`
                             detectCircularImports package

detectMissingImports :: Package -> S.Set ImportError
detectMissingImports package@Package { modules = ms } =
    S.fromList [e | (path, module') <- M.toList ms, e <- detect path module']
  where
    detect :: ModulePath -> Mod.Module -> [ImportError]
    detect path module' =
        [ e
        | (path', idents) <- M.toList (Mod.imports module')
        , e <- case resolveModule path' package of
                Nothing -> [MissingModulePathError path path']
                Just (Mod.Module decls _) ->
                    [ e
                    | i <- S.toList idents
                    , e <- case DS.lookup i decls of
                        Just TypeDeclaration {} -> []
                        Just ServiceDeclaration {} -> []
                        Just Import {} -> [MissingImportError path path' i]
                        Nothing -> [MissingImportError path path' i]
                    ]
        ]

detectCircularImports :: Package -> S.Set ImportError
detectCircularImports Package { modules = ms } =
    S.fromList [e | path <- M.keys ms, e <- detect path []]
  where
    moduleImports :: M.Map ModulePath (S.Set ModulePath)
    moduleImports =
        M.fromList [ (path, M.keysSet $ Mod.imports module')
                   | (path, module') <- M.toList ms
                   ]
    detect :: ModulePath -> [ModulePath] -> [ImportError]
    detect path reversedCycle
        | path `elem` reversedCycle =
            [CircularImportError $ reverse reversedCycle']
        | otherwise =
            case M.lookup path moduleImports of
                Just paths -> [ e
                              | path' <- S.toList paths
                              , e <- detect path' reversedCycle'
                              ]
                Nothing -> []
      where
        reversedCycle' :: [ModulePath]
        reversedCycle' = path : reversedCycle

data PackageError = ScanError FilePath IOError
                  | ParseError ModulePath ParseError
                  | ImportError (S.Set ImportError)
                  deriving (Eq, Show)

-- | Scan the given package path, and then return the read package.
scanPackage :: FilePath -> IO (Either PackageError Package)
scanPackage packagePath = do
    modulePaths <- scanModules packagePath
    -- FIXME: catch IO errors
    modules' <- mapM parseFile modulePaths
    return $ case M.foldrWithKey excludeFailedParse (Right M.empty) modules' of
        Right parsedModules -> case makePackage parsedModules of
            Right p -> Right p
            Left errors -> Left $ ImportError errors
        Left error' -> Left error'
  where
    excludeFailedParse :: ModulePath
                       -> Either ParseError Mod.Module
                       -> Either PackageError (M.Map ModulePath Mod.Module)
                       -> Either PackageError (M.Map ModulePath Mod.Module)
    excludeFailedParse _ _ (Left error') = Left error'
    excludeFailedParse path (Left error') _ = Left $ ParseError path error'
    excludeFailedParse path (Right module') (Right map') =
        Right (M.insert path module' map')

-- | Scan the given path recursively, and then return the map of
-- detected module paths.
scanModules :: FilePath -> IO (M.Map ModulePath FilePath)
scanModules packagePath = do
    files <- scanFiles ""
    return $ M.fromList files
  where
    isNotHidden :: FilePath -> Bool
    isNotHidden ('.':_) = False
    isNotHidden _ = True
    scanFiles :: FilePath -> IO [(ModulePath, FilePath)]
    scanFiles path = do
        dir <- doesDirectoryExist realPath
        if dir
        then do
            subpaths <- listDirectory realPath
            scans <- mapM (scanFiles . (path </>)) $ filter isNotHidden subpaths
            return $ concat scans
        else return $ case fromFilePath path of
                          Just modulePath' -> [(modulePath', realPath)]
                          Nothing -> []
      where
        realPath :: FilePath
        realPath = case path of
                       [] -> packagePath
                       p -> packagePath </> p

data BoundModule = BoundModule { boundPackage :: Package
                               , modulePath :: ModulePath
                               } deriving (Eq, Ord, Show)

findInBoundModule :: (Mod.Module -> a) -> a -> BoundModule -> a
findInBoundModule valueWhenExist valueWhenNotExist
                  BoundModule { boundPackage = Package { modules = ms }
                              , modulePath = path
                              } =
    case M.lookup path ms of
        Nothing -> valueWhenNotExist
        Just mod' -> valueWhenExist mod'

types :: BoundModule -> DS.DeclarationSet TypeDeclaration
types = findInBoundModule Mod.types DS.empty

docs :: BoundModule -> Maybe Docs
docs = findInBoundModule Mod.docs Nothing

data TypeLookup = Missing
                | Local Type
                | Imported ModulePath Type
                deriving (Eq, Ord, Show)

lookupType :: Identifier -> BoundModule -> TypeLookup
lookupType identifier boundModule =
    case DS.lookup identifier (types boundModule) of
        Nothing -> toType Mod.coreModulePath
            (DS.lookup identifier $ Mod.types Mod.coreModule)
        Just TypeDeclaration { type' = t } -> Local t
        Just (Import path _ _) ->
            case resolveModule path (boundPackage boundModule) of
                Nothing -> Missing
                Just (Mod.Module decls _) ->
                    toType path (DS.lookup identifier decls)
        Just ServiceDeclaration {} -> Missing
  where
    toType :: ModulePath -> Maybe TypeDeclaration -> TypeLookup
    toType mp (Just TypeDeclaration { type' = t }) = Imported mp t
    toType _ _ = Missing