Safe Haskell | None |
---|---|
Language | Haskell2010 |
Synopsis
- module Error.Diagnose.Style
- module Error.Diagnose.Report
- module Error.Diagnose.Pretty
- module Error.Diagnose.Position
- module Error.Diagnose.Diagnostic
Documentation
This module exports all the needed data types to use this library.
It should be sufficient to only import Error.Diagnose
.
How to use this module
This library is intended to provide a very simple way of creating beautiful errors, by exposing a small yet simple API to the user.
The basic idea is that a diagnostic is a collection of reports (which embody errors or warnings) along with the files which can be referenced in those reports.
Generating a report
A report contains:
- A message, to be shown at the top
- A list of located markers, used to underline parts of the source code and to emphasize it with a message
- A list of hints, shown at the very bottom
Note: The message type contained in a report is abstracted by a type variable. In order to render the report, the message must also be able to be rendered in some way (that we'll see later).
This library allows defining two kinds of reports:
Both take an optional error code, a message, a list of located markers and a list of hints.
A very simple example is:
exampleReport :: Report String exampleReport = err -- vv OPTIONAL ERROR CODE Nothing -- vv ERROR MESSAGE "This is my first error report" -- vv MARKERS [ (Position (1, 3) (1, 8) "some_test.txt", This "Some text under the marker") ] -- vv HINTS []
In general, Position
s are returned by either a lexer or a parser, so that you never have to construct them
directly in the code.
Note: If using any parser library, you will have to convert from the internal positioning system to a Position
to be able to use this library.
Markers put in the report can be one of (the colors specified are used only when pretty-printing):
A
This
marker, which is the primary marker of the report. While it is allowed to have multiple of these inside one report, it is encouraged not to, because the position at the top of the report will only be the one of the firstThis
marker, and because the resulting report may be harder to understand.This marker is output in red in an error report, and yellow in a warning report.
A
Where
marker contains additional information/provides context to the error/warning report. For example, it may underline where a given variablex
is bound to emphasize it.This marker is output in blue.
A
Maybe
marker may contain possible fixes (if the text is short, else hints are recommended for this use).This marker is output in magenta.
A
Blank
marker is useful only to output additional lines of code in the report.This marker is not output and has no color.
Creating diagnostics from reports
To create a new diagnostic, you need to use its Default
instance (which exposes a def
function, returning a new empty Diagnostic
).
Once the Diagnostic
is created, you can use either addReport
(which takes a Diagnostic
and a Report
, abstract by the same message type,
and returns a Diagnostic
) to insert a new report inside the diagnostic, or addFile
(which takes a Diagnostic
, a FilePath
and a [
,
and returns a String
]Diagnostic
) to insert a new file reference in the diagnostic.
You can then either pretty-print the diagnostic obtained (which requires all messages to be instances of the Pretty
)
-- directly onto a file handle or as a plain Doc
ument --
or export it to a lazy JSON ByteString
(e.g. in a LSP context).
Pretty-printing a diagnostic onto a file Handle
Diagnostic
s can be output to any Handle
using the printDiagnostic
function.
This function takes several parameters:
- The
Handle
onto which to output theDiagnostic
. It must be aHandle
capable of outputting data. A
Bool
used to indicate whether you want to output theDiagnostic
with unicode characters, or simple ASCII characters.Here are two examples of the same diagnostic, the first output with unicode characters, and the second output with ASCII characters:
[error]: Error with one marker in bounds ╭──▶ test.zc@1:25-1:30 │ 1 │ let id<a>(x : a) : a := x + 1 • ┬──── • ╰╸ Required here ─────╯
[error]: Error with one marker in bounds +--> test.zc@1:25-1:30 | 1 | let id<a>(x : a) : a := x + 1 : ^---- : `- Required here -----+
- A
Bool
set toFalse
if you don't want colors in the end result. - A
Int
describing the number of spaces with which to output a TAB character. - The
Style
describing colors of the report. See the module Error.Diagnose.Style for how to define new styles. - And finally the
Diagnostic
to output.
Pretty-printing a diagnostic as a document
Diagnostic
s can be “output” (at least ready to be rendered) to a Doc
using prettyDiagnostic
, which allows it to be easily added to other Doc
outputs.
This makes it easy to customize the error messages further (though not the internal parts, only adding to it).
As a Doc
, there is also the possibility of altering internal annotations (styles) much easier (although this is already possible when printing the diagnostic).
The arguments of the function mostly follow the ones from printDiagnostic
.
The style is not one, as it can be applied by simply applying the styling function to the resulting function (if wanted).
Exporting a diagnostic to JSON
Diagnostic
s can be exported to a JSON record of the following type, using the diagnosticToJson
function:
{ files: { name: string , content: string[] }[] , reports: { kind: 'error' | 'warning' , code: string? , message: string , markers: { kind: 'this' | 'where' | 'maybe' , position: { beginning: { line: int, column: int } , end: { line: int, column: int } , file: string } , message: string }[] , hints: ({ note: string } | { hint: string })[] }[] }
This is particularly useful in the context of a LSP server, where outputting or parsing a raw error yields strange results or is unnecessarily complicated.
Please note that this requires the flag diagnose:json
to be enabled (it is disabled by default in order not to include aeson
, which is a heavy library).
Compatibility layers for popular parsing libraries
There are many parsing libraries available in the Haskell ecosystem, each coming with its own way of handling errors.
Eventually, one needs to be able to map errors from these libraries to Diagnostic
s, without having to include additional code for doing so.
This is where compatibility layers come in handy.
As of now, there are compatibility layers for these libraries:
megaparsec >= 9.0.0 (Error.Diagnose.Compat.Megaparsec)
This needs the flag diagnose:megaparsec-compat
to be enabled.
Using the compatibility layer is very easy, as it is designed to be as simple as possible.
One simply needs to convert the ParseErrorBundle
which is returned by running a parser into a Diagnostic
by using diagnosticFromBundle
.
Several wrappers are included for easy creation of kinds (error, warning) of diagnostics.
Note: the returned diagnostic does not include file contents, which needs to be added manually afterwards.
As a quick example:
import qualified Text.Megaparsec as MP import qualified Text.Megaparsec.Char as MP import qualified Text.Megaparsec.Char.Lexer as MP let filename = "<interactive>" content = "00000a2223266" let myParser = MP.some MP.decimal <* MP.eof let res = MP.runParser myParser filename content case res of Left bundle -> let diag = errorDiagnosticFromBundle Nothing "Parse error on input" Nothing bundle -- Creates a new diagnostic with no default hints from the bundle returned by megaparsec diag' = addFile diag filename content -- Add the file used when parsing with the same filename given to 'MP.runParser' in printDiagnostic stderr True True 4 diag' Right res -> print res
This example will return the following error message (assuming default instances for
):HasHints
Void
msg
[error]: Parse error on input ╭──▶ <interactive>@1:6-1:7 │ 1 │ 00000a2223266 • ┬ • ├╸ unexpected 'a' • ╰╸ expecting digit, end of input, or integer ─────╯
parsec >= 3.1.14.0 (Error.Diagnose.Compat.Parsec)
This needs the flag diagnose:parsec-compat
to be enabled.
This compatibility layer allows easily converting ParseError
s into a single-report diagnostic containing all available information such
as unexpected/expected tokens or error messages.
The function diagnosticFromParseError
is used to perform the conversion between a ParseError
and a Diagnostic
.
Note: the returned diagnostic does not include file contents, which needs to be added manually afterwards.
Quick example:
import qualified Text.Parsec as P let filename = "<interactive>" content = "00000a2223266" let myParser = P.many1 P.digit <* P.eof let res = P.parse myParser filename content case res of Left error -> let diag = errorDiagnosticFromParseError Nothing "Parse error on input" Nothing error -- Creates a new diagnostic with no default hints from the bundle returned by megaparsec diag' = addFile diag filename content -- Add the file used when parsing with the same filename given to 'MP.runParser' in printDiagnostic stderr True True 4 diag' Right res -> print res
This will output the following errr on stderr
:
[error]: Parse error on input ╭──▶ <interactive>@1:6-1:7 │ 1 │ 00000a2223266 • ┬ • ├╸ unexpected 'a' • ╰╸ expecting any of digit, end of input ─────╯
Common errors
No instance for (HasHints ??? msg) arising from a use of ‘errorDiagnosticFromBundle’
(???
is any type, depending on your parser's custom error type):The typeclass
HasHints
does not have any default instances, because treatments of custom errors is highly dependent on who is using the library. As such, you will need to create orphan instances for your parser's error type.Note that the message type
msg
can be left abstract if the implements ofhints
ishints _ = mempty
.
Re-exports
module Error.Diagnose.Style
module Error.Diagnose.Report
module Error.Diagnose.Pretty
module Error.Diagnose.Position
module Error.Diagnose.Diagnostic