packages feed

policeman-0.0.0.0: src/Policeman/Cabal.hs

{- | Functions to work with Cabal file and its content.
-}

module Policeman.Cabal
    ( CabalError (..)
    , parseCabalFile
    , findCabalDescription
    , extractExposedModules
    , extractPackageName
    , extractPackageVersion
    ) where

import Control.Monad.Except (throwError)
import Distribution.ModuleName (ModuleName, components)
import Distribution.PackageDescription (CondTree (..), GenericPackageDescription (..), Library (..),
                                        PackageDescription (..))
import Distribution.PackageDescription.Parsec (parseGenericPackageDescriptionMaybe)
import Distribution.Types.PackageId (PackageIdentifier (..))
import Distribution.Types.Version (versionNumbers)
import System.Directory (doesDirectoryExist, listDirectory)
import System.FilePath (takeExtension, (</>))

import Policeman.Core.Package (Module (..), PackageName (..))
import Policeman.Core.Version (Version, versionFromIntList)

import qualified Distribution.Types.PackageName as Cabal


{- | Errors about dealing with @.cabal@ files.
-}
data CabalError
    -- | Directory where we're looking for cabal file, not found.
    = NoSuchDirectory FilePath
    -- | No @.cabal@ file under given path.
    | NoCabalFile FilePath
    -- | Multiple cabal files found.
    | MultipleCabalFiles (NonEmpty FilePath)
    -- | Error parsing cabal file.
    | CabalParseError
    deriving stock (Show, Eq)

{- | This function takes a path to a directory, finds a cabal file
under this directory, parses it and returns parsed content.
-}
findCabalDescription :: FilePath -> ExceptT CabalError IO GenericPackageDescription
findCabalDescription dirPath = do
    unlessM (liftIO $ doesDirectoryExist dirPath) $ throwError $ NoSuchDirectory dirPath
    dirContent <- liftIO $ listDirectory dirPath
    let cabalFiles = filter (\p -> takeExtension p == ".cabal") dirContent
    case cabalFiles of
        [] -> throwError $ NoCabalFile dirPath
        [cabalFile] -> do
            let cabalPath = dirPath </> cabalFile
            cabalContent <- readFileBS cabalPath
            parseCabalFile cabalContent
        x:xs -> throwError $ MultipleCabalFiles (x :| xs)

-- | Parses the given cabal file source and returns 'GenericPackageDescription'
parseCabalFile :: ByteString -> ExceptT CabalError IO GenericPackageDescription
parseCabalFile cabalContent = whenNothing (parseGenericPackageDescriptionMaybe cabalContent) $
    throwError CabalParseError  -- TODO: better error

extractPackageName :: GenericPackageDescription -> PackageName
extractPackageName =
    PackageName
    . toText
    . Cabal.unPackageName
    . pkgName
    . package
    . packageDescription

extractPackageVersion :: GenericPackageDescription -> Maybe Version
extractPackageVersion =
    versionFromIntList
    . versionNumbers
    . pkgVersion
    . package
    . packageDescription

extractExposedModules :: GenericPackageDescription -> [Module]
extractExposedModules =
    map toModule
    . concatMap (exposedModules . condTreeData)
    . maybeToList
    . condLibrary
  where
    toModule :: ModuleName -> Module
    toModule = Module . toText . intercalate "." . components