| Safe Haskell | Safe-Inferred |
|---|---|
| Language | Haskell2010 |
Error.Diagnose
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, Positions 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
Thismarker, 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 firstThismarker, 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
Wheremarker contains additional information/provides context to the error/warning report. For example, it may underline where a given variablexis bound to emphasize it.This marker is output in blue.
A
Maybemarker may contain possible fixes (if the text is short, else hints are recommended for this use).This marker is output in magenta.
A
Blankmarker 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 Document --
or export it to a lazy JSON ByteString (e.g. in a LSP context).
Pretty-printing a diagnostic onto a file Handle
Diagnostics can be output to any Handle using the printDiagnostic function.
This function takes several parameters:
- The
Handleonto which to output theDiagnostic. It must be aHandlecapable of outputting data. A
Boolused to indicate whether you want to output theDiagnosticwith 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
Boolset toFalseif you don't want colors in the end result. - A
Intdescribing the number of spaces with which to output a TAB character. - The
Styledescribing colors of the report. See the module Error.Diagnose.Style for how to define new styles. - And finally the
Diagnosticto output.
Pretty-printing a diagnostic as a document
Diagnostics 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
Diagnostics 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 Diagnostics, 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 4 diag'
Right res -> print resThis 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 ParseErrors 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 4 diag'
Right res -> print resThis will output the following error 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
HasHintsdoes 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
msgcan be left abstract if the implements ofhintsishints _ = mempty.
Re-exports
module Error.Diagnose.Style
module Error.Diagnose.Report
module Error.Diagnose.Pretty
module Error.Diagnose.Position
module Error.Diagnose.Diagnostic