cabal-install-3.18.1.0: src/Distribution/Client/Errors/Parser.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Distribution.Client.Errors.Parser where
import Distribution.Client.Compat.Prelude
import System.FilePath (normalise)
import Prelude ()
import qualified Data.ByteString.Char8 as BS8
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as Map
import qualified Data.Map.Merge.Strict as Map
import Distribution.Parsec
import Distribution.Parsec.Source
import Distribution.Simple.Utils (fromUTF8BS)
import Distribution.Solver.Types.ProjectConfigPath
import Distribution.Version (Version)
import Text.PrettyPrint (render)
-- Error when parsing a .cabal file
-- | Errors reported upon failing to parse a @.cabal@ file.
data CabalFileParseError
= CabalFileParseError
FilePath
-- ^ @.cabal@ file path
BS8.ByteString
-- ^ @.cabal@ file contents
(NonEmpty (PErrorWithSource CabalFileSource))
-- ^ errors
(Maybe Version)
-- ^ We might discover the spec version the package needs
[PWarningWithSource CabalFileSource]
-- ^ warnings
-- | Manual instance which skips file contents
instance Show CabalFileParseError where
showsPrec d (CabalFileParseError fp _ es mv ws) =
showParen (d > 10) $
showString "CabalFileParseError"
. showChar ' '
. showsPrec 11 fp
. showChar ' '
. showsPrec 11 ("" :: String)
. showChar ' '
. showsPrec 11 es
. showChar ' '
. showsPrec 11 mv
. showChar ' '
. showsPrec 11 ws
instance Exception CabalFileParseError where
displayException = renderCabalFileParseError
renderCabalFileParseError :: CabalFileParseError -> String
renderCabalFileParseError (CabalFileParseError _filePath _contents errors _ warnings) =
renderParseErrorCabalFile errors warnings
-- Error when parsing a project file
-- | Errors reported upon failing to parse a @cabal.project@ file.
data ProjectConfigParseError
= ProjectConfigParseError
(NonEmpty (PErrorWithSource ProjectFileSource))
-- ^ errors
[PWarningWithSource ProjectFileSource]
-- ^ warnings
-- | Manual instance which skips file contents
instance Show ProjectConfigParseError where
showsPrec d (ProjectConfigParseError es ws) =
showParen (d > 10) $
showString "ProjectConfigParseError"
. showChar ' '
. showsPrec 11 es
. showChar ' '
. showsPrec 11 ws
instance Exception ProjectConfigParseError where
displayException = renderProjectConfigParseError
renderProjectConfigParseError :: ProjectConfigParseError -> String
renderProjectConfigParseError (ProjectConfigParseError errors warnings) =
renderParseError displayProjectFileSource errors warnings
where
displayProjectFileSource (ProjectFileSource (path, contents)) =
renderParseErrorFile "project" (currentProjectConfigPath path) (if isTopLevelConfigPath path then Nothing else Just $ render (docProjectImportedBy path)) contents
data ProjectFileSource = ProjectFileSource (ProjectConfigPath, BS8.ByteString) deriving (Show, Generic)
instance Eq ProjectFileSource where
(ProjectFileSource (path1, _)) == (ProjectFileSource (path2, _)) = path1 == path2
instance Ord ProjectFileSource where
(ProjectFileSource (path1, _)) `compare` (ProjectFileSource (path2, _)) = path1 `compare` path2
renderProjectFileSource :: ProjectFileSource -> String
renderProjectFileSource (ProjectFileSource (path, _contents)) =
currentProjectConfigPath path
renderParseErrorCabalFile :: NonEmpty (PErrorWithSource CabalFileSource) -> [PWarningWithSource CabalFileSource] -> String
renderParseErrorCabalFile errors warnings =
renderParseError renderCabalFileSourceMsgs errors warnings
-- | Render parse error highlighting the part of the input file.
renderParseError
:: forall src
. Ord src
=> (src -> ([PError], [PWarning]) -> String)
-> NonEmpty (PErrorWithSource src)
-> [PWarningWithSource src]
-> String
renderParseError display errors warnings =
unlines [renderParseErrorsWarnings (fmap display source) ws | (source, ws) <- joinedErrorsWarningsList]
where
mkErrorGroup :: NonEmpty (PErrorWithSource src) -> (PSource src, [PError])
mkErrorGroup (x :| xs) = (perrorSource x, perror x : map perror xs)
mkWarningGroup :: NonEmpty (PWarningWithSource src) -> (PSource src, [PWarning])
mkWarningGroup (x :| xs) = (pwarningSource x, pwarning x : map pwarning xs)
groupedErrors =
Map.fromListWith (++) $
map mkErrorGroup $
NE.groupBy
(\a b -> perrorSource a == perrorSource b)
errors
groupedWarnings =
Map.fromListWith (++) $
map mkWarningGroup $
NE.groupBy
(\a b -> pwarningSource a == pwarningSource b)
warnings
joinedErrorsWarnings :: Map.Map (PSource src) ([PError], [PWarning])
joinedErrorsWarnings = Map.merge (Map.mapMissing (\_ es -> (es, []))) (Map.mapMissing (\_ ps -> ([], ps))) (Map.zipWithMatched (\_ es ps -> (es, ps))) groupedErrors groupedWarnings
joinedErrorsWarningsList = Map.toList joinedErrorsWarnings
renderParseErrorsWarnings :: PSource (([PError], [PWarning]) -> String) -> ([PError], [PWarning]) -> String
renderParseErrorsWarnings source (errors, warnings) =
case source of
PKnownSource src -> src (errors, warnings)
PUnknownSource -> renderParseErrorNoFile "" errors warnings
renderCabalFileSourceMsgs :: CabalFileSource -> ([PError], [PWarning]) -> String
renderCabalFileSourceMsgs (PCabalFile (fpath, contents)) (errors, warnings) =
renderParseErrorFile "cabal" fpath Nothing contents (errors, warnings)
renderInstalledPackageInfoSourceMsgs :: InstalledPackageInfoSource -> ([PError], [PWarning]) -> String
renderInstalledPackageInfoSourceMsgs PInstalledPackageInfo (errors, warnings) =
renderParseErrorNoFile "installed package info" errors warnings
renderParseErrorNoFile :: String -> [PError] -> [PWarning] -> String
renderParseErrorNoFile herald errors warnings =
renderParseErrorGeneral herald Nothing Nothing (const []) errors warnings
-- | Render a parse error which resulted from a file on disk
renderParseErrorFile
:: String
-- ^ Human name for the kind of file (i.e. cabal, project "file")
-> FilePath
-- ^ Path to the file
-> Maybe String
-- ^ Provenance, any additional contextual info to print
-> BS8.ByteString
-- ^ Contents of the file
-> ([PError], [PWarning])
-> String
renderParseErrorFile herald filepath provenance contents (errors, warnings) =
renderParseErrorGeneral (herald <> " file " <> filepath) (Just (filepath' <> ":")) provenance formatInput errors warnings
where
filepath' = normalise filepath
-- lines of the input file. 'lines' is taken, so they are called rows
-- contents, line number, whether it's empty line
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 -- empty
('-' : '-' : _) -> True -- comment
_ -> False
-- format line: prepend the given line number
formatInput :: Position -> [String]
formatInput (Position 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 -- error line
, " | " ++ replicate (col - 1) ' ' ++ "^" -- pointer: ^
]
-- do we need rows after?
-- ++ map formatInputLine (take 1 zs) -- one row after
formatInputLine :: (String, Int, Bool) -> String
formatInputLine (str, row, _) = leftPadShow row ++ " | " ++ str
-- hopefully we don't need to work with over 99999 lines .cabal files
-- at that point small glitches in error messages are hopefully fine.
leftPadShow :: Int -> String
leftPadShow n = let s = show n in replicate (5 - length s) ' ' ++ s
-- | A generic rendering function which can render from many sources.
renderParseErrorGeneral
:: String
-- ^ What we were parsing when the error occurred.
-> Maybe String
-- ^ A simpler/shorter header to display when displaying each error (normally a filepath)
-> Maybe String
-- ^ Provenance, used to print additional context about what file failed (used to print the import path of a project
-- file which failed to parse)
-> (Position -> [String])
-- ^ Extra information to render based on the position
-> [PError]
-> [PWarning]
-> String
renderParseErrorGeneral header err_header provenance extra_info errors warnings =
unlines $
[ warningsOrErrors <> " parsing" <> header' <> ":"
]
++ catMaybes [provenance]
++ [""] -- Place a newline between the header and the errors/warnings
-- Place a newline between each error and warning
++ intersperse "" (renderedWarnings ++ renderedErrors)
where
warningsOrErrors = case errors of
[] -> case warnings of
[_] -> "Warning"
_ -> "Warnings"
[_] -> "Error"
_ -> "Errors"
header' = if null header then "" else " " <> header
renderedErrors = map renderError (sortBy (comparing perrorPosition) errors)
renderedWarnings = map renderWarning (sortBy (comparing pwarningPosition) warnings)
renderErrorOrWarning :: String -> Position -> String -> String
renderErrorOrWarning err_type pos msg
-- if position is 0:0, then it doesn't make sense to show input
-- looks like, Parsec errors have line-feed in them
| pos == zeroPos = unlines (herald : map indent user_msg)
| otherwise = unlines (herald : map indent (user_msg ++ extra_info pos))
where
herald = renderErrorHerald pos ++ err_type ++ ":"
user_msg = lines (trimLF msg)
indent :: String -> String
indent s = replicate 2 ' ' ++ s
-- Don't render the 0:0 position
renderErrorHerald :: Position -> String
renderErrorHerald pos =
case (err_header, pos == zeroPos) of
(Nothing, True) -> ""
(Nothing, False) -> showPos pos ++ ": "
(Just herald, True) -> herald ++ " "
(Just herald, False) -> herald ++ showPos pos ++ ": "
renderError :: PError -> String
renderError (PError pos msg) = renderErrorOrWarning "error" pos msg
renderWarning :: PWarning -> String
renderWarning (PWarning _ pos msg) = renderErrorOrWarning "warning" pos msg
-- sometimes there are (especially trailing) newlines.
trimLF :: String -> String
trimLF = dropWhile (== '\n') . reverse . dropWhile (== '\n') . reverse
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'