module CabalFmt.Error (Error (..), renderError) where
import Control.Exception (Exception)
import Data.List.NonEmpty (NonEmpty)
import System.FilePath (normalise)
import System.IO (hPutStr, hPutStrLn, stderr)
import Text.Parsec.Error (ParseError)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS8
import qualified Distribution.Parsec as C
import qualified Distribution.Simple.Utils as C (fromUTF8BS)
import qualified Distribution.Types.Version as C
data Error
= SomeError String
| CabalParseError FilePath BS.ByteString (NonEmpty C.PError) (Maybe C.Version) [C.PWarning]
| PanicCannotParseInput ParseError
| WarningError String
deriving (Show)
instance Exception Error
renderError :: Error -> IO ()
renderError (SomeError err) = hPutStrLn stderr $ "error: " ++ err
renderError (PanicCannotParseInput err) = hPutStrLn stderr $ "panic! " ++ show err
renderError (CabalParseError filepath contents errors _ warnings) =
hPutStr stderr $ renderParseError filepath contents errors warnings
renderError (WarningError w) = hPutStrLn stderr $ "error (-Werror): " ++ w
renderParseError
:: FilePath
-> BS.ByteString
-> NonEmpty C.PError
-> [C.PWarning]
-> String
renderParseError filepath contents errors warnings = unlines $
[ "Errors encountered when parsing cabal 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 = C.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'