packages feed

implicit-hie-0.1.3.0: src/Hie/Cabal/Parser.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards   #-}

module Hie.Cabal.Parser
( Package(..)
, Component(..)
, CompType(..)
, Name
, extractPkgs
, parsePackage'
) where

import           Control.Applicative
import           Control.Monad
import           Data.Attoparsec.Text
import           Data.Char
import           Data.Foldable                                 (asum)
import           Data.Maybe                                    (catMaybes,
                                                                maybeToList)
import           Data.Text                                     (Text)
import qualified Data.Text                                     as T
import           Data.Text.Encoding                            (encodeUtf8)
import           Distribution.ModuleName                       (ModuleName,
                                                                toFilePath)
import           Distribution.Package                          (pkgName,
                                                                unPackageName)
import           Distribution.PackageDescription               (Benchmark (benchmarkBuildInfo, benchmarkInterface, benchmarkName),
                                                                BenchmarkInterface (BenchmarkExeV10),
                                                                Executable (buildInfo, exeName, modulePath),
                                                                ForeignLib (foreignLibBuildInfo, foreignLibName),
                                                                Library (libBuildInfo, libName),
                                                                LibraryName (..),
                                                                TestSuiteInterface (TestSuiteExeV10),
                                                                benchmarkModules,
                                                                exeModules,
                                                                explicitLibModules,
                                                                foreignLibModules)
import           Distribution.PackageDescription.Configuration
import           Distribution.PackageDescription.Parsec
import           Distribution.Types.BuildInfo
import           Distribution.Types.PackageDescription
import           Distribution.Types.TestSuite
import           Distribution.Types.UnqualComponentName
import           Distribution.Utils.Path                       (getSymbolicPath)
import           GHC.IO                                        (unsafePerformIO)
import           System.Directory                              (doesFileExist)
import           System.FilePath                               ((<.>), (</>))


type Name = Text

type Path = Text

type Indent = Int

data Package = Package Name [Component]
  deriving (Show, Eq, Ord)

data CompType = Lib | Exe | Test | Bench
  deriving (Show, Eq, Ord)

data Component
  = Comp CompType Name Path
  deriving (Show, Eq, Ord)

parseQuoted :: Parser Text
parseQuoted = do
  q <- char '"' <|> char '\''
  s <- takeTill (== q)
  _ <- char q
  pure s

parseString :: Parser Name
parseString = parseQuoted <|> unqualName

unqualName :: Parser Text
unqualName = takeWhile1 (not . (\c -> isSpace c || c == ','))

-- | Comma or space separated list, with optional new lines.
parseList :: Indent -> Parser [Text]
parseList i = many (nl <|> sl)
  where
    sep = skipMany (char ',' <|> tabOrSpace)
    com = skipMany tabOrSpace >> "--" >> skipWhile (not . isEndOfLine)
    sl = do
      sep
      x <- parseString
      sep
      skipMany com
      pure x

    nl = do
      skipMany emptyOrComLine <|> endOfLine
      _ <- indent i
      sep
      skipMany com
      x <- parseString
      sep
      skipMany com
      pure x

skipToNextLine :: Parser ()
skipToNextLine = skipWhile (not . isEndOfLine) >> endOfLine

comment :: Parser ()
comment = skipMany tabOrSpace >> "--" >> skipToNextLine

emptyOrComLine :: Parser ()
emptyOrComLine = (skipMany tabOrSpace >> endOfLine) <|> comment

tabOrSpace :: Parser Char
tabOrSpace = char ' ' <|> char '\t'

-- field :: Indent -> Text -> Parser Text
field ::
  Indent ->
  [Text] ->
  (Indent -> Parser a) ->
  Parser a
field i f p =
  do
    i' <- indent i
    _ <- asum $ map asciiCI f
    skipMany tabOrSpace
    _ <- char ':'
    skipMany tabOrSpace
    p' <- p $ i' + 1
    skipToNextLine
    pure p'

-- | Skip at least n spaces
indent :: Indent -> Parser Int
indent i = do
  c <- length <$> many' tabOrSpace
  if c >= i then pure c else fail "insufficient indent"

extractPkgs :: Parser [T.Text]
extractPkgs = join . catMaybes <$> many' (Just <$> field 0 ["packages"] parseList <|> (skipToNextLine >> pure Nothing))

parsePackage' :: T.Text -> Either String Package
parsePackage' t = do
  let bytes = encodeUtf8 t
  case runParseResult (parseGenericPackageDescription bytes) of
    (_warnings, Left err) ->
        error $ "Cannot parse Cabal file: " <> show err
    (_warnings, Right res) -> do
      let pkg = flattenPackageDescription res
      Right $ extractPackage pkg

extractPackage :: PackageDescription -> Package
extractPackage PackageDescription{..} = Package n cc where
  n = T.pack . unPackageName $ pkgName package

  cc = concat $
    [mkComp Test (unqName $ testName t) (testBuildInfo t) (testExePath t) (testModules t) | t <- testSuites] ++
    [mkComp Bench (unqName $ benchmarkName b) (benchmarkBuildInfo b) (benchmarkExePath b) (benchmarkModules b) | b <- benchmarks] ++
    [mkComp Exe (unqName $ exeName e) (buildInfo e) [modulePath e] (exeModules e) | e <- executables] ++
    [mkComp Lib (libName' l) (libBuildInfo l) [] (explicitLibModules l) | l <- maybeToList library ++ subLibraries ] ++
    [mkComp Lib (unqName $ foreignLibName f) (foreignLibBuildInfo f) [] (foreignLibModules f) | f <- foreignLibs]

  mkComp :: CompType -> T.Text -> BuildInfo -> [FilePath] -> [ModuleName] -> [Component]
  mkComp typ name bi fps mods =
    [Comp typ name (T.pack fp)
    | fp0 <- fps <> concatMap toFilePath' mods
    , srcDir <- map getSymbolicPath $ hsSourceDirs bi
    , let fp = srcDir </> fp0
    , unsafePerformIO $ doesFileExist fp
    ]

  unqName = T.pack . unUnqualComponentName
  libName' x = case libName x of
    LMainLibName  -> ""
    LSubLibName u -> unqName u

benchmarkExePath :: Benchmark -> [FilePath]
benchmarkExePath b = case benchmarkInterface b of
  BenchmarkExeV10 _ f -> [f]
  _                   -> []

toFilePath' :: ModuleName -> [FilePath]
toFilePath' mod = [ toFilePath mod <.> ext | ext <- ["hs", "lhs"]]

testExePath :: TestSuite -> [FilePath]
testExePath t = case testInterface t of
    TestSuiteExeV10 _ fp -> [fp]
    _                    -> []