module Error.Diagnose
( -- $header
-- * How to use this module
-- $usage
-- ** Generating a report
-- $generate_report
-- ** Creating diagnostics from reports
-- $create_diagnostic
-- *** Pretty-printing a diagnostic onto a file 'System.IO.Handle'
-- $diagnostic_pretty
-- *** Pretty-printing a diagnostic as a document
-- $diagnostic_to_doc
-- *** Exporting a diagnostic to JSON
-- $diagnostic_json
-- ** Compatibility layers for popular parsing libraries
-- $compatibility_layers
-- *** megaparsec >= 9.0.0 ("Error.Diagnose.Compat.Megaparsec")
-- $compatibility_megaparsec
-- *** parsec >= 3.1.14.0 ("Error.Diagnose.Compat.Parsec")
-- $compatibility_parsec
-- *** Common errors
-- $compatibility_errors
-- * Re-exports
module Export,
)
where
import Error.Diagnose.Diagnostic as Export
import Error.Diagnose.Position as Export
import Error.Diagnose.Pretty as Export
import Error.Diagnose.Report as Export
import Error.Diagnose.Style as Export
-- $header
--
-- This module exports all the needed data types to use this library.
-- It should be sufficient to only @import "Error.Diagnose"@.
-- $usage
--
-- 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.
-- $generate_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, '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 'Error.Diagnose.Report.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/ 'Error.Diagnose.Report.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 'Error.Diagnose.Report.Where' marker contains additional information\/provides context to the error\/warning report.
-- For example, it may underline where a given variable @x@ is bound to emphasize it.
--
-- This marker is output in blue.
--
-- - A 'Error.Diagnose.Report.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 'Error.Diagnose.Report.Blank' marker is useful only to output additional lines of code in the report.
--
-- This marker is not output and has no color.
-- $create_diagnostic
--
-- To create a new diagnostic, you need to use its 'Data.Default.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 'Prettyprinter.Pretty')
-- -- directly onto a file handle or as a plain 'Prettyprinter.Doc'ument --
-- or export it to a lazy JSON 'Data.Bytestring.Lazy.ByteString' (e.g. in a LSP context).
-- $diagnostic_pretty
--
-- 'Diagnostic's can be output to any 'System.IO.Handle' using the 'printDiagnostic' function.
-- This function takes several parameters:
--
-- - The 'System.IO.Handle' onto which to output the 'Diagnostic'.
-- It __must__ be a 'System.IO.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(x : a) : a := x + 1
-- > • ┬────
-- > • ╰╸ Required here
-- > ─────╯
--
-- > [error]: Error with one marker in bounds
-- > +--> test.zc@1:25-1:30
-- > |
-- > 1 | let id(x : a) : a := x + 1
-- > : ^----
-- > : `- Required here
-- > -----+
--
-- - A 'Bool' set to 'False' 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.
-- $diagnostic_to_doc
--
-- 'Diagnostic's can be “output” (at least ready to be rendered) to a 'Prettyprinter.Doc' using 'prettyDiagnostic', which allows it to be easily added to other 'Prettyprinter.Doc' outputs.
-- This makes it easy to customize the error messages further (though not the internal parts, only adding to it).
-- As a 'Prettyprinter.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).
-- $diagnostic_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
--
-- 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:
-- $compatibility_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 'Text.Megaparsec.ParseErrorBundle' which is returned by running a parser into a 'Diagnostic' by using 'Error.Diagnose.Compat.Megaparsec.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 = ""
-- > 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 @'Error.Diagnose.Compat.Megaparsec.HasHints' 'Data.Void.Void' msg@):
--
-- > [error]: Parse error on input
-- > ╭──▶ @1:6-1:7
-- > │
-- > 1 │ 00000a2223266
-- > • ┬
-- > • ├╸ unexpected 'a'
-- > • ╰╸ expecting digit, end of input, or integer
-- > ─────╯
-- $compatibility_parsec
--
-- This needs the flag @diagnose:parsec-compat@ to be enabled.
--
-- This compatibility layer allows easily converting 'Text.Parsec.Error.ParseError's into a single-report diagnostic containing all available information such
-- as unexpected/expected tokens or error messages.
-- The function 'Error.Diagnose.Compat.Parsec.diagnosticFromParseError' is used to perform the conversion between a 'Text.Parsec.Error.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 = ""
-- > 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 error on @stderr@:
--
-- > [error]: Parse error on input
-- > ╭──▶ @1:6-1:7
-- > │
-- > 1 │ 00000a2223266
-- > • ┬
-- > • ├╸ unexpected 'a'
-- > • ╰╸ expecting any of digit, end of input
-- > ─────╯
-- $compatibility_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 'Error.Diagnose.Compat.Megaparsec.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 'Error.Diagnose.Compat.Hints.hints' is @hints _ = mempty@.