diagnose-1.7.1: Beautiful error reporting done easily
Safe HaskellNone
LanguageHaskell2010

Error.Diagnose

Synopsis

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:

  • Errors, using err
  • Warnings, using warn

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 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 first This 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 informationprovides context to the errorwarning report. For example, it may underline where a given variable x 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.

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 [String], and returns a 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) or export it to a lazy JSON ByteString (e.g. in a LSP context).

Pretty-printing a diagnostic

Diagnostics can be output to any Handle using the printDiagnostic function. This function takes several parameters:

  • The Handle onto which to output the Diagnostic. It must be a Handle capable of outputting data.
  • A Bool used to indicate whether you want to output the Diagnostic 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 to False if you don't want colors in the end result.
  • And finally the Diagnostic to output.

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: 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 True 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 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 True 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 of hints is hints _ = mempty.

Re-exports