wireform-proto-0.2.0.0: src/Proto/IDL/Parser/Error.hs
{- | Rust-quality error rendering for protobuf parser diagnostics.
Produces output like:
@
error: unexpected end of input
--> example.proto:5:25
|
5 | string name = 1
| ^ expected ';' after field definition
|
@
-}
module Proto.IDL.Parser.Error (
renderParseErrors,
renderParseError,
) where
import Data.List (intercalate)
import Data.List.NonEmpty qualified as NE
import Data.Maybe (catMaybes, isNothing)
import Data.Maybe qualified
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import Data.Void (Void)
import Text.Megaparsec
-- | Render all errors in a ParseErrorBundle to a single Rust-style diagnostic string.
renderParseErrors :: ParseErrorBundle Text Void -> String
renderParseErrors bundle =
let errs = NE.toList (bundleErrors bundle)
src = pstateInput (bundlePosState bundle)
sourceLines = T.lines src
renderedErrors = fmap (renderOneError sourceLines bundle) errs
in intercalate "\n" renderedErrors
-- | Render a single ParseErrorBundle (convenience for the common single-error case).
renderParseError :: ParseErrorBundle Text Void -> String
renderParseError = renderParseErrors
renderOneError :: [Text] -> ParseErrorBundle Text Void -> ParseError Text Void -> String
renderOneError sourceLines bundle err =
let offset = errorOffset err
pst = bundlePosState bundle
(_, pst') = reachOffset offset pst
sp = pstateSourcePos pst'
filePath = sourceName sp
line = unPos (sourceLine sp)
col = unPos (sourceColumn sp)
(summary, details) = describeError err
lineNumWidth = max 1 (length (show line))
pad = replicate lineNumWidth ' '
mainLine = showSourceLine sourceLines line lineNumWidth
-- When error is past EOF (line doesn't exist), show the last available line
-- and point to the end of it
useFallback = isNothing mainLine && line >= 2
fallbackLine =
if useFallback
then showSourceLine sourceLines (line - 1) lineNumWidth
else Nothing
effectiveLine = if Data.Maybe.isJust mainLine then mainLine else fallbackLine
effectiveCol =
if Data.Maybe.isJust mainLine
then col
else maybe col (\l -> T.length l + 1) (getSourceLine sourceLines (line - 1))
-- Show context line before the effective error line, avoiding duplicates
contextLineNum = if useFallback then line - 2 else line - 1
contextBefore =
if contextLineNum >= 1
then showSourceLine sourceLines contextLineNum lineNumWidth
else Nothing
pointer = makePointer lineNumWidth effectiveCol (pointerWidth err)
in unlines $
catMaybes
[ Just $ "error: " <> summary
, Just $ pad <> " --> " <> filePath <> ":" <> show line <> ":" <> show col
, Just $ pad <> " |"
, contextBefore
, effectiveLine
, Just $ pointer <> " " <> details
, Just $ pad <> " |"
]
getSourceLine :: [Text] -> Int -> Maybe Text
getSourceLine sourceLines lineNum
| lineNum < 1 || lineNum > length sourceLines = Nothing
| otherwise = Just (sourceLines !! (lineNum - 1))
showSourceLine :: [Text] -> Int -> Int -> Maybe String
showSourceLine sourceLines lineNum lineNumWidth = do
content <- getSourceLine sourceLines lineNum
let num = show lineNum
padding = replicate (lineNumWidth - length num) ' '
pure $ padding <> num <> " | " <> T.unpack content
makePointer :: Int -> Int -> Int -> String
makePointer lineNumWidth col width =
let pad = replicate lineNumWidth ' '
caretPad = replicate (max 0 (col - 1)) ' '
carets =
if width <= 1
then "^"
else replicate width '^'
in pad <> " | " <> caretPad <> carets
pointerWidth :: ParseError Text Void -> Int
pointerWidth = \case
TrivialError _ (Just (Tokens ts)) _ -> max 1 (NE.length ts)
_ -> 1
describeError :: ParseError Text Void -> (String, String)
describeError (TrivialError _ unexpected' expected') =
(describeUnexpected unexpected', describeExpected expected')
describeError (FancyError _ fancyErrors) =
case Set.toList fancyErrors of
[] -> ("syntax error", "")
msgs -> (describeFancySet msgs, "")
describeUnexpected :: Maybe (ErrorItem Char) -> String
describeUnexpected Nothing = "syntax error"
describeUnexpected (Just item) = "unexpected " <> describeItem item
describeItem :: ErrorItem Char -> String
describeItem (Tokens ts) =
let s = NE.toList ts
in case s of
[c]
| c == '\n' -> "newline"
| c == '\t' -> "tab"
| c == ' ' -> "space"
| otherwise -> "'" <> [c] <> "'"
_ -> "\"" <> escapeString s <> "\""
describeItem (Label cs) = NE.toList cs
describeItem EndOfInput = "end of input"
escapeString :: String -> String
escapeString = concatMap go
where
go '\n' = "\\n"
go '\t' = "\\t"
go '\r' = "\\r"
go c = [c]
describeExpected :: Set (ErrorItem Char) -> String
describeExpected items
| Set.null items = ""
| otherwise =
let groups = categorizeExpected (Set.toList items)
in "expected " <> formatExpectedGroups groups
data ExpectedGroup = ExpectedGroup
{ egLabels :: [String]
, egTokens :: [String]
, egEOI :: Bool
}
categorizeExpected :: [ErrorItem Char] -> ExpectedGroup
categorizeExpected = foldl go (ExpectedGroup [] [] False)
where
go acc (Label cs) = acc {egLabels = NE.toList cs : egLabels acc}
go acc (Tokens ts) = acc {egTokens = showToken (NE.toList ts) : egTokens acc}
go acc EndOfInput = acc {egEOI = True}
showToken :: String -> String
showToken [c]
| c == ';' = "';'"
| c == '{' = "'{'"
| c == '}' = "'}'"
| c == '=' = "'='"
| c == '(' = "'('"
| c == ')' = "')'"
| c == '<' = "'<'"
| c == '>' = "'>'"
| c == '[' = "'['"
| c == ']' = "']'"
| c == ',' = "','"
| c == '.' = "'.'"
| c == '"' = "'\"'"
| c == '\'' = "\"'\""
| c == '\n' = "newline"
| c == ' ' = "space"
showToken s = "\"" <> s <> "\""
formatExpectedGroups :: ExpectedGroup -> String
formatExpectedGroups (ExpectedGroup labels toks eoi) =
let allParts = labels <> toks <> (if eoi then ["end of input"] else [])
in case allParts of
[] -> "something"
[x] -> x
_ -> commaOr allParts
commaOr :: [String] -> String
commaOr [] = ""
commaOr [x] = x
commaOr [x, y] = x <> " or " <> y
commaOr xs =
let front = init xs
end = last xs
in intercalate ", " front <> ", or " <> end
describeFancySet :: [ErrorFancy Void] -> String
describeFancySet = intercalate "; " . fmap describeFancy
describeFancy :: ErrorFancy Void -> String
describeFancy (ErrorFail msg) = msg
describeFancy (ErrorIndentation ord ref actual) =
"incorrect indentation (got "
<> show (unPos actual)
<> ", should be "
<> showOrd ord
<> show (unPos ref)
<> ")"
describeFancy (ErrorCustom v) = absurd v
where
absurd :: Void -> a
absurd x = case x of {}
showOrd :: Ordering -> String
showOrd LT = "less than "
showOrd EQ = "equal to "
showOrd GT = "greater than "