{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE UndecidableInstances #-}
module Cabal.Parse (
parseWith,
ParseError (..),
renderParseError,
) where
import Control.DeepSeq (NFData (..))
import Control.Exception (Exception (..))
import Data.ByteString (ByteString)
import Data.Foldable (for_)
import Data.List.NonEmpty (NonEmpty)
import Data.Typeable (Typeable)
import Distribution.Simple.Utils (fromUTF8BS)
import GHC.Generics (Generic)
import System.FilePath (normalise)
import qualified Data.ByteString.Char8 as BS8
import qualified Distribution.Fields as C
import qualified Distribution.Fields.LexerMonad as C
import qualified Distribution.Parsec as C
import qualified Distribution.Utils.Generic as C
import qualified Text.Parsec as P
parseWith
:: ([C.Field C.Position] -> C.ParseResult a)
-> FilePath
-> ByteString
-> Either (ParseError NonEmpty) a
parseWith parser fp bs = case C.runParseResult result of
(_, Right x) -> return x
(ws, Left (_, es)) -> Left $ ParseError fp bs es ws
where
result = case C.readFields' bs of
Left perr -> C.parseFatalFailure pos (show perr) where
ppos = P.errorPos perr
pos = C.Position (P.sourceLine ppos) (P.sourceColumn ppos)
Right (fields, lexWarnings) -> do
C.parseWarnings (C.toPWarnings lexWarnings)
for_ (C.validateUTF8 bs) $ \pos ->
C.parseWarning C.zeroPos C.PWTUTF $ "UTF8 encoding problem at byte offset " ++ show pos
parser fields
data ParseError f = ParseError
{ peFilename :: FilePath
, peContents :: ByteString
, peErrors :: f C.PError
, peWarnings :: [C.PWarning]
}
deriving (Generic)
deriving instance (Show (f C.PError)) => Show (ParseError f)
instance (Foldable f, Show (f C.PError), Typeable f) => Exception (ParseError f) where
displayException = renderParseError
instance (NFData (f C.PError)) => NFData (ParseError f)
renderParseError :: Foldable f => ParseError f -> String
renderParseError (ParseError filepath contents errors warnings)
| null errors && null warnings = ""
| null errors = unlines $
("Warnings encountered when parsing file " ++ filepath ++ ":")
: renderedWarnings
| otherwise = unlines $
[ "Errors encountered when parsing file " ++ filepath ++ ":"
]
++ renderedErrors
++ renderedWarnings
where
filepath' = normalise filepath
rows :: [(String, Int, Bool)]
rows = zipWith f (BS8.lines contents) [1..] where
f bs i = let s = fromUTF8BS bs in (s, i, isEmptyOrComment s)
rowsZipper = listToZipper rows
isEmptyOrComment :: String -> Bool
isEmptyOrComment s = case dropWhile (== ' ') s of
"" -> True
('-':'-':_) -> True
_ -> False
renderedErrors = concatMap renderError errors
renderedWarnings = concatMap renderWarning warnings
renderError :: C.PError -> [String]
renderError (C.PError pos@(C.Position row col) msg)
| pos == C.zeroPos = msgs
| otherwise = msgs ++ formatInput row col
where
msgs = [ "", filepath' ++ ":" ++ C.showPos pos ++ ": error:", trimLF msg, "" ]
renderWarning :: C.PWarning -> [String]
renderWarning (C.PWarning _ pos@(C.Position row col) msg)
| pos == C.zeroPos = msgs
| otherwise = msgs ++ formatInput row col
where
msgs = [ "", filepath' ++ ":" ++ C.showPos pos ++ ": warning:", trimLF msg, "" ]
trimLF :: String -> String
trimLF = dropWhile (== '\n') . reverse . dropWhile (== '\n') . reverse
formatInput :: Int -> Int -> [String]
formatInput row col = case advance (row - 1) rowsZipper of
Zipper xs ys -> before ++ after where
before = case span (\(_, _, b) -> b) xs of
(_, []) -> []
(zs, z : _) -> map formatInputLine $ z : reverse zs
after = case ys of
[] -> []
(z : _zs) ->
[ formatInputLine z
, " | " ++ replicate (col - 1) ' ' ++ "^"
]
formatInputLine :: (String, Int, Bool) -> String
formatInputLine (str, row, _) = leftPadShow row ++ " | " ++ str
leftPadShow :: Int -> String
leftPadShow n = let s = show n in replicate (5 - length s) ' ' ++ s
data Zipper a = Zipper [a] [a]
listToZipper :: [a] -> Zipper a
listToZipper = Zipper []
advance :: Int -> Zipper a -> Zipper a
advance n z@(Zipper xs ys)
| n <= 0 = z
| otherwise = case ys of
[] -> z
(y:ys') -> advance (n - 1) $ Zipper (y:xs) ys'