diagnose (empty) → 1.6.3
raw patch · 17 files changed
+2078/−0 lines, 17 filesdep +aesondep +basedep +bytestring
Dependencies added: aeson, base, bytestring, containers, data-default, diagnose, hashable, megaparsec, parsec, prettyprinter, prettyprinter-ansi-terminal, text, unordered-containers
Files
- LICENSE +30/−0
- README.md +105/−0
- diagnose.cabal +201/−0
- src/Data/List/Safe.hs +35/−0
- src/Error/Diagnose.hs +289/−0
- src/Error/Diagnose/Compat/Hints.hs +14/−0
- src/Error/Diagnose/Compat/Megaparsec.hs +111/−0
- src/Error/Diagnose/Compat/Parsec.hs +108/−0
- src/Error/Diagnose/Diagnostic.hs +26/−0
- src/Error/Diagnose/Diagnostic/Internal.hs +144/−0
- src/Error/Diagnose/Position.hs +65/−0
- src/Error/Diagnose/Pretty.hs +4/−0
- src/Error/Diagnose/Report.hs +14/−0
- src/Error/Diagnose/Report/Internal.hs +543/−0
- test/megaparsec/Spec.hs +39/−0
- test/parsec/Spec.hs +38/−0
- test/rendering/Spec.hs +312/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Ghilain Bergeron (Mesabloo) (c) 2019-2020++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Ghilain Bergeron (Mesabloo) nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,105 @@+# Error reporting made easy++Diagnose is a small library used to report compiler/interpreter errors in a beautiful yet readable way.+It was in the beginning heavily inspired by [ariadne](https://github.com/zesterer/ariadne), but ended up quickly becoming its own thing.++As a great example, here's the output of the last test:++++If you do not like unicode characters, or choose to target platforms which cannot output them natively;+you may alternatively print the whole diagnostic with ASCII characters, like this:++++Colors are also optional, and you may choose not to print them.++## Features++- Show diagnostics with/without 8-bit colors, with/without Unicode characters+- Inline and multiline markers are nicely displayed+- The order of markers matters!+ If there are multiple markers on the same line, they are ordered according to how they were put in each report+- Reports spanning across multiple files are handled as well+- Generic over the type of message which can be displayed, meaning that you can output custom data types as well as they can be pretty-printed+- Diagnostics can be exported to JSON, if you don't quite like the rendering as it is, or if you need to transmit them to e.g. a website+- Plug and play (mega)parsec integration and it magically works with your parsers!++## Usage++You only need to `import Error.Diagnose`, and everything should be ready to go.+You don't even need to `import Prettyprinter`, as it is already provided to you by `Error.Diagnose`!++--------++A diagnostic can be viewed as a collection of reports, spanning on files.+This is what the `Diagnostic` type embodies.++It has a `Default` instance, which can be used to construct an empty diagnostic (contains no reports, and has no files).++The second step is to add some reports.+There are two kinds of reports:+- Error reports, created through `err`+- Warning reports, created by using `warn`++Both of these fonctions have the following type:+```haskell+ -- | The main message, which is output at the top right after `[error]` or `[warning]`+ msg ->+ -- | A list of markers, along with the positions they span on+ [(Position, Marker msg)] ->+ -- | Some hints to be output at the bottom of the report+ [msg] ->+ -- | The created report+ Report msg+```++Each report contains markers, which are what underlines the code in the screenshots above.+They come in three flavors:+- A `This` marker indicates the main reason of the error.+ It is highlighted in red (for errors) or yellow (for warnings).+ Ideally, there is only one per report, but this isn't strictly required.+- A `Where` marker adds additional context to the error by adding highlighted code to the error.+ This can be used to remind used that a variable was found of a given type earlier, or even where a previous declaration was found in another file.+ This is output in blue by default.+- A `Maybe` marker is probably the rarest one.+ It is basically a way of suggesting fixes (as when GCC tells you that you probably mistyped a variable name).+ These markers are highlighted in green.++The `Position` datatype is however required to be used with this library.+If you use another way of keeping track of position information, you will need to convert them to the `Position` datatype.++Once your reports are created, you will need to add them inside the diagnostic using `addReport`.+You will also need to put your files into the diagnostic with `addFile`, else lines won't be printed and you will get `<no-line>` in your reports.++After all of this is done, you may choose to either print the diagnostic onto a handle using `printDiagnostic`+or export it to JSON with `diagnosticToJson` or the `ToJSON` class of Aeson (the output format is documented under the `diagnosticToJson` function).++## Example++Here is how the above screenshot was generated:+```haskell+let beautifulExample =+ err+ "Could not deduce constraint 'Num(a)' from the current context"+ [ (Position (1, 25) (2, 6) "somefile.zc", This "While applying function '+'"),+ (Position (1, 11) (1, 16) "somefile.zc", Where "'x' is supposed to have type 'a'"),+ (Position (1, 8) (1, 9) "somefile.zc", Where "type 'a' is bound here without constraints")+ ]+ ["Adding 'Num(a)' to the list of constraints may solve this problem."]++-- Create the diagnostic +let diagnostic = addFile def "somefile.zc" "let id<a>(x : a) : a := x\n + 1"+let diagnostic' = addReport diagnostic beautifulExample++-- Print with unicode characters and colors+printDiagnostic stdout True True diagnostic'+```++More examples are given in the [`test/rendering`](./test/rendering) folder.++## License++This work is licensed under the BSD-3 clause license.++Copyright (c) 2021- Mesabloo, all rights reserved.
+ diagnose.cabal view
@@ -0,0 +1,201 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.34.6.+--+-- see: https://github.com/sol/hpack++name: diagnose+version: 1.6.3+synopsis: Beautiful error reporting done easily+description: This package provides a simple way of getting beautiful compiler/interpreter errors+ using a very simple interface for the programmer.+ .+ A quick tutorial is available in the module "Error.Diagnose", which goes on most of the basis+ of how to use it.+category: Error Reporting+homepage: https://github.com/mesabloo/diagnose#readme+bug-reports: https://github.com/mesabloo/diagnose/issues+author: Ghilain Bergeron+maintainer: Ghilain Bergeron+copyright: 2021- Ghilain Bergeron+license: BSD3+license-file: LICENSE+build-type: Simple+extra-source-files:+ README.md++source-repository head+ type: git+ location: https://github.com/mesabloo/diagnose++flag json+ description: Allows exporting diagnostics as JSON. This is disabled by default as this relies on the very heavy dependency Aeson.+ manual: True+ default: False++flag megaparsec-compat+ description: Includes a small compatibility layer (in the module `Error.Diagnose.Compat.Megaparsec`) to transform megaparsec errors into reports for this library.+ manual: True+ default: False++flag parsec-compat+ description: Includes a small compatibility layer (in the module `Error.Diagnose.Compat.Parsec`) to transform parsec errors into reports for this library.+ manual: True+ default: False++library+ exposed-modules:+ Error.Diagnose+ Error.Diagnose.Diagnostic+ Error.Diagnose.Position+ Error.Diagnose.Pretty+ Error.Diagnose.Report+ other-modules:+ Data.List.Safe+ Error.Diagnose.Compat.Hints+ Error.Diagnose.Diagnostic.Internal+ Error.Diagnose.Report.Internal+ Paths_diagnose+ hs-source-dirs:+ src+ default-extensions:+ OverloadedStrings+ LambdaCase+ BlockArguments+ ghc-options: -Wall -Wextra+ build-depends:+ base >=4.7 && <5+ , bytestring ==0.10.*+ , data-default ==0.7.*+ , hashable ==1.3.*+ , prettyprinter ==1.7.*+ , prettyprinter-ansi-terminal ==1.1.*+ , text ==1.2.*+ , unordered-containers ==0.2.*+ if flag(json)+ cpp-options: -DUSE_AESON+ build-depends:+ aeson ==1.5.*+ if flag(megaparsec-compat)+ build-depends:+ containers ==0.6.*+ , megaparsec >=9.0.0+ if flag(parsec-compat)+ build-depends:+ parsec >=3.1.14+ if flag(megaparsec-compat)+ exposed-modules:+ Error.Diagnose.Compat.Megaparsec+ if flag(parsec-compat)+ exposed-modules:+ Error.Diagnose.Compat.Parsec+ default-language: Haskell2010++test-suite diagnose-megaparsec-tests+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Paths_diagnose+ hs-source-dirs:+ test/megaparsec+ default-extensions:+ OverloadedStrings+ LambdaCase+ BlockArguments+ ghc-options: -Wall -Wextra -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ base >=4.7 && <5+ , bytestring ==0.10.*+ , data-default ==0.7.*+ , diagnose+ , hashable ==1.3.*+ , prettyprinter ==1.7.*+ , prettyprinter-ansi-terminal ==1.1.*+ , text ==1.2.*+ , unordered-containers ==0.2.*+ if flag(json)+ cpp-options: -DUSE_AESON+ build-depends:+ aeson ==1.5.*+ if flag(megaparsec-compat)+ build-depends:+ containers ==0.6.*+ , megaparsec >=9.0.0+ if flag(parsec-compat)+ build-depends:+ parsec >=3.1.14+ if !(flag(megaparsec-compat))+ buildable: False+ default-language: Haskell2010++test-suite diagnose-parsec-tests+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Paths_diagnose+ hs-source-dirs:+ test/parsec+ default-extensions:+ OverloadedStrings+ LambdaCase+ BlockArguments+ ghc-options: -Wall -Wextra -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ base >=4.7 && <5+ , bytestring ==0.10.*+ , data-default ==0.7.*+ , diagnose+ , hashable ==1.3.*+ , prettyprinter ==1.7.*+ , prettyprinter-ansi-terminal ==1.1.*+ , text ==1.2.*+ , unordered-containers ==0.2.*+ if flag(json)+ cpp-options: -DUSE_AESON+ build-depends:+ aeson ==1.5.*+ if flag(megaparsec-compat)+ build-depends:+ containers ==0.6.*+ , megaparsec >=9.0.0+ if flag(parsec-compat)+ build-depends:+ parsec >=3.1.14+ if !(flag(parsec-compat))+ buildable: False+ default-language: Haskell2010++test-suite diagnose-rendering-tests+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Paths_diagnose+ hs-source-dirs:+ test/rendering+ default-extensions:+ OverloadedStrings+ LambdaCase+ BlockArguments+ ghc-options: -Wall -Wextra -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ base >=4.7 && <5+ , bytestring ==0.10.*+ , data-default ==0.7.*+ , diagnose+ , hashable ==1.3.*+ , prettyprinter ==1.7.*+ , prettyprinter-ansi-terminal ==1.1.*+ , text ==1.2.*+ , unordered-containers ==0.2.*+ if flag(json)+ cpp-options: -DUSE_AESON+ build-depends:+ aeson ==1.5.*+ if flag(megaparsec-compat)+ build-depends:+ containers ==0.6.*+ , megaparsec >=9.0.0+ if flag(parsec-compat)+ build-depends:+ parsec >=3.1.14+ default-language: Haskell2010
+ src/Data/List/Safe.hs view
@@ -0,0 +1,35 @@+module Data.List.Safe where++import Data.Bifunctor (first)+++-- | Analogous to 'Data.List.last', but returns 'Nothing' on an empty list, instead of throwing an error.+safeLast :: [a] -> Maybe a+safeLast [] = Nothing+safeLast l = Just $ last l++-- | Analogous to `Data.List.head`, but returns 'Nothing' in case of an empty list.+safeHead :: [a] -> Maybe a+safeHead [] = Nothing+safeHead (x : _) = Just x++-- | Analogous tu 'Data.List.!!', but does not throw an error on missing index.+safeIndex :: Int -> [a] -> Maybe a+safeIndex _ [] = Nothing+safeIndex 0 (x : _) = Just x+safeIndex n (_ : xs)+ | n < 0 = Nothing+ | otherwise = safeIndex (n - 1) xs++-- | Safely deconstructs a list from the end.+--+-- More efficient than @(init x, last x)@+safeUnsnoc :: [a] -> Maybe ([a], a)+safeUnsnoc [] = Nothing+safeUnsnoc [x] = Just ([], x)+safeUnsnoc (x : xs) = first (x :) <$> safeUnsnoc xs++-- | Safely deconstructs a list from the beginning, returning 'Nothing' if the list is empty.+safeUncons :: [a] -> Maybe (a, [a])+safeUncons [] = Nothing+safeUncons (x : xs) = Just (x, xs)
+ src/Error/Diagnose.hs view
@@ -0,0 +1,289 @@+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+ -- $diagnostic_pretty++ -- *** 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.Pretty as Export+import Error.Diagnose.Position as Export+import Error.Diagnose.Report as Export+import Error.Diagnose.Diagnostic 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 a message, a list of located markers and a list of hints.++ A very simple example is:++ > exampleReport :: Report String+ > exampleReport =+ > err+ > -- 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 /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 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.+-}++{- $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 'Text.PrettyPrint.ANSI.Leijen.Pretty')+ 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<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.+-}++{- $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'+ > , 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 ++ 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 = "<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 "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 @'Error.Diagnose.Compat.Megaparsec.HasHints' 'Data.Void.Void' msg@):++ > [error]: Parse error on input+ > ╭─▶ <interactive>@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 = "<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 "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+ > ─────╯+-}++{- $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@.++-}
+ src/Error/Diagnose/Compat/Hints.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE MultiParamTypeClasses #-}++module Error.Diagnose.Compat.Hints where+ +-- | A class mapping custom errors of type @e@ with messages of type @msg@.+class HasHints e msg where+ -- | Defines all the hints associated with a given custom error.+ hints :: e -> [msg]++-- this is a sane default for 'Void'+-- but this can be redefined+--+-- instance HasHints Void msg where+-- hints _ = mempty
+ src/Error/Diagnose/Compat/Megaparsec.hs view
@@ -0,0 +1,111 @@+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE ViewPatterns #-}++{-# OPTIONS -Wno-name-shadowing #-}++-- |+-- Module : Error.Diagnose.Compat.Megaparsec+-- Description : Compatibility layer for megaparsec+-- Copyright : (c) Mesabloo, 2021+-- License : BSD3+-- Stability : experimental+-- Portability : Portable+module Error.Diagnose.Compat.Megaparsec+ ( diagnosticFromBundle,+ errorDiagnosticFromBundle,+ warningDiagnosticFromBundle,+ module Error.Diagnose.Compat.Hints,+ )+where++import Data.Bifunctor (second)+import Data.Function ((&))+import Data.Maybe (fromMaybe)+import qualified Data.Set as Set (toList)+import Data.String (IsString (..))+import Error.Diagnose+import Error.Diagnose.Compat.Hints (HasHints (..))+import qualified Text.Megaparsec as MP++-- | Transforms a megaparsec 'MP.ParseErrorBundle' into a well-formated 'Diagnostic' ready to be shown.+diagnosticFromBundle ::+ forall msg s e.+ (IsString msg, MP.Stream s, HasHints e msg, MP.ShowErrorComponent e, MP.VisualStream s, MP.TraversableStream s) =>+ -- | How to decide whether this is an error or a warning diagnostic+ (MP.ParseError s e -> Bool) ->+ -- | The error message of the diagnostic+ msg ->+ -- | Default hints when trivial errors are reported+ Maybe [msg] ->+ -- | The bundle to create a diagnostic from+ MP.ParseErrorBundle s e ->+ Diagnostic msg+diagnosticFromBundle isError msg (fromMaybe [] -> trivialHints) MP.ParseErrorBundle {..} =+ foldl addReport def (toLabeledPosition <$> bundleErrors)+ where+ toLabeledPosition :: MP.ParseError s e -> Report msg+ toLabeledPosition error =+ let (_, pos) = MP.reachOffset (MP.errorOffset error) bundlePosState+ source = fromSourcePos (MP.pstateSourcePos pos)+ msgs = fromString @msg <$> lines (MP.parseErrorTextPretty error)+ in flip+ (msg & if isError error then err else warn)+ (errorHints error)+ if+ | [m] <- msgs -> [(source, This m)]+ | [m1, m2] <- msgs -> [(source, This m1), (source, Where m2)]+ | otherwise -> [(source, This $ fromString "<<Unknown error>>")]++ fromSourcePos :: MP.SourcePos -> Position+ fromSourcePos MP.SourcePos {..} =+ let start = both (fromIntegral . MP.unPos) (sourceLine, sourceColumn)+ end = second (+ 1) start+ in Position start end sourceName++ errorHints :: MP.ParseError s e -> [msg]+ errorHints MP.TrivialError {} = trivialHints+ errorHints (MP.FancyError _ errs) =+ Set.toList errs >>= \case+ MP.ErrorCustom e -> hints e+ _ -> mempty++-- | Creates an error diagnostic from a megaparsec 'MP.ParseErrorBundle'.+errorDiagnosticFromBundle ::+ forall msg s e.+ (IsString msg, MP.Stream s, HasHints e msg, MP.ShowErrorComponent e, MP.VisualStream s, MP.TraversableStream s) =>+ -- | The error message of the diagnostic+ msg ->+ -- | Default hints when trivial errors are reported+ Maybe [msg] ->+ -- | The bundle to create a diagnostic from+ MP.ParseErrorBundle s e ->+ Diagnostic msg+errorDiagnosticFromBundle = diagnosticFromBundle (const True)++-- | Creates a warning diagnostic from a megaparsec 'MP.ParseErrorBundle'.+warningDiagnosticFromBundle ::+ forall msg s e.+ (IsString msg, MP.Stream s, HasHints e msg, MP.ShowErrorComponent e, MP.VisualStream s, MP.TraversableStream s) =>+ -- | The error message of the diagnostic+ msg ->+ -- | Default hints when trivial errors are reported+ Maybe [msg] ->+ -- | The bundle to create a diagnostic from+ MP.ParseErrorBundle s e ->+ Diagnostic msg+warningDiagnosticFromBundle = diagnosticFromBundle (const False)++------------------------------------+------------ INTERNAL --------------+------------------------------------++-- | Applies a computation to both element of a tuple.+--+-- > both f = bimap @(,) f f+both :: (a -> b) -> (a, a) -> (b, b)+both f ~(x, y) = (f x, f y)
+ src/Error/Diagnose/Compat/Parsec.hs view
@@ -0,0 +1,108 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE ViewPatterns #-}++{-# OPTIONS -Wno-name-shadowing #-}++-- |+-- Module : Error.Diagnose.Compat.Parsec+-- Description : Compatibility layer for parsec+-- Copyright : (c) Mesabloo, 2021+-- License : BSD3+-- Stability : experimental+-- Portability : Portable+module Error.Diagnose.Compat.Parsec+ ( diagnosticFromParseError,+ errorDiagnosticFromParseError,+ warningDiagnosticFromParseError,+ module Error.Diagnose.Compat.Hints,+ )+where++import Data.Bifunctor (second)+import Data.Function ((&))+import Data.List (intercalate)+import Data.Maybe (fromMaybe)+import Data.String (IsString (..))+import Data.Void (Void)+import Error.Diagnose+import Error.Diagnose.Compat.Hints (HasHints (..))+import qualified Text.Parsec.Error as PE+import qualified Text.Parsec.Pos as PP++-- | Generates a diagnostic from a 'PE.ParseError'.+diagnosticFromParseError ::+ forall msg.+ (IsString msg, HasHints Void msg) =>+ -- | Determine whether the diagnostic is an error or a warning+ (PE.ParseError -> Bool) ->+ -- | The main error of the diagnostic+ msg ->+ -- | Default hints+ Maybe [msg] ->+ -- | The 'PE.ParseError' to transform into a 'Diagnostic'+ PE.ParseError ->+ Diagnostic msg+diagnosticFromParseError isError msg (fromMaybe [] -> defaultHints) error =+ let pos = fromSourcePos $ PE.errorPos error+ markers = toMarkers pos $ PE.errorMessages error+ report = (msg & if isError error then err else warn) markers (defaultHints <> hints (undefined :: Void))+ in addReport def report+ where+ fromSourcePos :: PP.SourcePos -> Position+ fromSourcePos pos =+ let start = both fromIntegral (PP.sourceLine pos, PP.sourceColumn pos)+ end = second (+ 1) start+ in Position start end (PP.sourceName pos)++ toMarkers :: Position -> [PE.Message] -> [(Position, Marker msg)]+ toMarkers source [] = [(source, This $ fromString "<<unknown error>>")]+ toMarkers source msgs =+ let putTogether [] = ([], [], [], [])+ putTogether (PE.SysUnExpect thing : ms) = let (a, b, c, d) = putTogether ms in (thing : a, b, c, d)+ putTogether (PE.UnExpect thing : ms) = let (a, b, c, d) = putTogether ms in (a, thing : b, c, d)+ putTogether (PE.Expect thing : ms) = let (a, b, c, d) = putTogether ms in (a, b, thing : c, d)+ putTogether (PE.Message thing : ms) = let (a, b, c, d) = putTogether ms in (a, b, c, thing : d)++ (sysUnexpectedList, unexpectedList, expectedList, messages) = putTogether msgs+ in [ (source, marker) | unexpected <- if null unexpectedList then sysUnexpectedList else unexpectedList, let marker = This $ fromString $ "unexpected " <> unexpected+ ]+ <> [ (source, marker) | msg <- messages, let marker = This $ fromString msg+ ]+ <> [(source, Where $ fromString $ "expecting any of " <> intercalate ", " expectedList)]++-- | Generates an error diagnostic from a 'PE.ParseError'.+errorDiagnosticFromParseError ::+ forall msg.+ (IsString msg, HasHints Void msg) =>+ -- | The main error message of the diagnostic+ msg ->+ -- | Default hints+ Maybe [msg] ->+ -- | The 'PE.ParseError' to convert+ PE.ParseError ->+ Diagnostic msg+errorDiagnosticFromParseError = diagnosticFromParseError (const True)++-- | Generates a warning diagnostic from a 'PE.ParseError'.+warningDiagnosticFromParseError ::+ forall msg.+ (IsString msg, HasHints Void msg) =>+ -- | The main error message of the diagnostic+ msg ->+ -- | Default hints+ Maybe [msg] ->+ -- | The 'PE.ParseError' to convert+ PE.ParseError ->+ Diagnostic msg+warningDiagnosticFromParseError = diagnosticFromParseError (const False)++------------------------------------+------------ INTERNAL --------------+------------------------------------++-- | Applies a computation to both element of a tuple.+--+-- > both f = bimap @(,) f f+both :: (a -> b) -> (a, a) -> (b, b)+both f ~(x, y) = (f x, f y)
+ src/Error/Diagnose/Diagnostic.hs view
@@ -0,0 +1,26 @@+{-# LANGUAGE CPP #-}++-- |+-- Module : Error.Diagnose.Diagnostic+-- Description : Diagnostic definition and pretty printing+-- Copyright : (c) Mesabloo, 2021+-- License : BSD3+-- Stability : experimental+-- Portability : Portable+module Error.Diagnose.Diagnostic+ ( -- * Re-exports+ module Export,+ )+where++import Error.Diagnose.Diagnostic.Internal as Export+ ( Diagnostic,+#ifdef USE_AESON+ diagnosticToJson,+#endif+ addFile,+ addReport,+ def,+ printDiagnostic,+ )+import System.IO as Export (stderr, stdout)
+ src/Error/Diagnose/Diagnostic/Internal.hs view
@@ -0,0 +1,144 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleInstances #-}++-- |+-- Module : Error.Diagnose.Diagnostic.Internal+-- Description : Internal workings for diagnostic definitions and pretty printing.+-- Copyright : (c) Mesabloo, 2021+-- License : BSD3+-- Stability : experimental+-- Portability : Portable+--+-- /Warning/: The API of this module can break between two releases, therefore you should not rely on it.+-- It is also highly undocumented.+--+-- Please limit yourself to the "Error.Diagnose.Diagnostic" module, which exports some of the useful functions defined here.+module Error.Diagnose.Diagnostic.Internal (module Error.Diagnose.Diagnostic.Internal, def) where++import Control.Monad.IO.Class (MonadIO, liftIO)+#ifdef USE_AESON+import Data.Aeson (ToJSON(..), encode, object, (.=))+import Data.ByteString.Lazy (ByteString)+#endif+import Data.Default (Default, def)+import Data.Foldable (fold)+import Data.HashMap.Lazy (HashMap)+import qualified Data.HashMap.Lazy as HashMap+import Data.List (intersperse)+import Error.Diagnose.Report (Report)+import Error.Diagnose.Report.Internal (prettyReport)+import Prettyprinter (Doc, Pretty, hardline, unAnnotate)+import Prettyprinter.Render.Terminal (AnsiStyle, hPutDoc)+import System.IO (Handle)++-- | The data type for diagnostic containing messages of an abstract type.+--+-- The constructors are private, but users can use 'def' from the 'Default' typeclass+-- to create a new empty diagnostic, and 'addFile' and 'addReport' to alter its internal state.+data Diagnostic msg+ = Diagnostic+ [Report msg]+ -- ^ All the reports contained in a diagnostic.+ --+ -- Reports are output one by one, without connections in between.+ (HashMap FilePath [String])+ -- ^ A map associating files with their content as lists of lines.++instance Default (Diagnostic msg) where+ def = Diagnostic mempty mempty++instance Semigroup (Diagnostic msg) where+ Diagnostic rs1 file <> Diagnostic rs2 _ = Diagnostic (rs1 <> rs2) file++#ifdef USE_AESON+instance ToJSON msg => ToJSON (Diagnostic msg) where+ toJSON (Diagnostic reports files) =+ object [ "files" .= fmap toJSONFile (HashMap.toList files)+ , "reports" .= reports+ ]+ where+ toJSONFile (path, content) =+ object [ "name" .= path+ , "content" .= content+ ]+#endif++-- | Pretty prints a diagnostic into a 'Doc'ument that can be output using+-- 'Text.PrettyPrint.ANSI.Leijen.hPutDoc' or 'Text.PrettyPrint.ANSI.Leijen.displayIO'.+prettyDiagnostic ::+ Pretty msg =>+ -- | Should we use unicode when printing paths?+ Bool ->+ -- | The diagnostic to print+ Diagnostic msg ->+ Doc AnsiStyle+prettyDiagnostic withUnicode (Diagnostic reports file) =+ fold . intersperse hardline $ prettyReport file withUnicode <$> reports+{-# INLINE prettyDiagnostic #-}++-- | Prints a 'Diagnostic' onto a specific 'Handle'.+printDiagnostic ::+ (MonadIO m, Pretty msg) =>+ -- | The handle onto which to output the diagnostic.+ Handle ->+ -- | Should we print with unicode characters?+ Bool ->+ -- | 'False' to disable colors.+ Bool ->+ -- | The diagnostic to output.+ Diagnostic msg ->+ m ()+printDiagnostic handle withUnicode withColors diag =+ liftIO $ hPutDoc handle (unlessId withColors unAnnotate $ prettyDiagnostic withUnicode diag)+ where+ unlessId cond app = if cond then id else app+ {-# INLINE unlessId #-}+{-# INLINE printDiagnostic #-}++-- | Inserts a new referenceable file within the diagnostic.+addFile ::+ Diagnostic msg ->+ -- | The path to the file.+ FilePath ->+ -- | The content of the file as a single string, where lines are ended by @\n@.+ String ->+ Diagnostic msg+addFile (Diagnostic reports files) path content =+ Diagnostic reports (HashMap.insert path (lines content) files)+{-# INLINE addFile #-}++-- | Inserts a new report into a diagnostic.+addReport ::+ Diagnostic msg ->+ -- | The new report to add to the diagnostic.+ Report msg ->+ Diagnostic msg+addReport (Diagnostic reports files) report =+ Diagnostic (report : reports) files+{-# INLINE addReport #-}++#ifdef USE_AESON+-- | Creates a JSON object from a diagnostic, containing those fields (only types are indicated):+--+-- > { files:+-- > { name: string+-- > , content: string[]+-- > }[]+-- > , reports:+-- > { kind: 'error' | 'warning'+-- > , message: string+-- > , markers:+-- > { kind: 'this' | 'where' | 'maybe'+-- > , position:+-- > { beginning: { line: int, column: int }+-- > , end: { line: int, column: int }+-- > , file: string+-- > }+-- > , message: string+-- > }[]+-- > , hints: string[]+-- > }[]+-- > }+diagnosticToJson :: ToJSON msg => Diagnostic msg -> ByteString+diagnosticToJson = encode+#endif
+ src/Error/Diagnose/Position.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeApplications #-}++-- |+-- Module : Error.Diagnose.Diagnostic+-- Description : Defines location information as a simple record.+-- Copyright : (c) Mesabloo, 2021+-- License : BSD3+-- Stability : experimental+-- Portability : Portable+module Error.Diagnose.Position (Position (..)) where++#ifdef USE_AESON+import Data.Aeson (ToJSON(..), object, (.=))+#endif+import Data.Default (Default, def)+import Data.Hashable (Hashable)+import Data.Text (Text)+import GHC.Generics (Generic (..))+import Prettyprinter (Pretty (..), colon)++-- import Text.PrettyPrint.ANSI.Leijen (Pretty(..), text, colon, int)++-- | Contains information about the location of something.+--+-- It is best used in a datatype like:+--+-- > data Located a+-- > = a :@ Position+-- > deriving (Show, Eq, Ord, Functor, Traversable)+data Position = Position+ { -- | The beginning line and column of the span.+ begin :: (Int, Int),+ -- | The end line and column of the span.+ end :: (Int, Int),+ -- | The file this position spans in.+ file :: FilePath+ }+ deriving (Show, Eq, Generic)++instance Ord Position where+ Position b1 e1 _ `compare` Position b2 e2 _ = (b1, e1) `compare` (b2, e2)++instance Pretty Position where+ pretty (Position (bl, bc) (el, ec) f) = pretty f <> at <> pretty bl <> colon <> pretty bc <> dash <> pretty el <> colon <> pretty ec+ where+ at = pretty @Text "@"+ dash = pretty @Text "-"++instance Hashable Position++instance Default Position where+ def = Position (1, 1) (1, 1) "<no-file>"++#ifdef USE_AESON+instance ToJSON Position where+ toJSON (Position (bl, bc) (el, ec) file) =+ object [ "beginning" .= object [ "line" .= bl, "column" .= bc ]+ , "end" .= object [ "line" .= el, "column" .= ec ]+ , "file" .= file+ ]+#endif
+ src/Error/Diagnose/Pretty.hs view
@@ -0,0 +1,4 @@+module Error.Diagnose.Pretty (module Export) where++import qualified Prettyprinter as Export +import qualified Prettyprinter.Render.Terminal as Export
+ src/Error/Diagnose/Report.hs view
@@ -0,0 +1,14 @@+-- |+-- Module : Error.Diagnose.Report+-- Description : Report definition and pretty printing+-- Copyright : (c) Mesabloo, 2021+-- License : BSD3+-- Stability : experimental+-- Portability : Portable+module Error.Diagnose.Report+ ( -- * Re-exports+ module Export,+ )+where++import Error.Diagnose.Report.Internal as Export (Marker (..), Report, err, warn)
+ src/Error/Diagnose/Report/Internal.hs view
@@ -0,0 +1,543 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++{-# OPTIONS -Wno-name-shadowing #-}++-- |+-- Module : Error.Diagnose.Report.Internal+-- Description : Internal workings for report definitions and pretty printing.+-- Copyright : (c) Mesabloo, 2021+-- License : BSD3+-- Stability : experimental+-- Portability : Portable+--+-- /Warning/: The API of this module can break between two releases, therefore you should not rely on it.+-- It is also highly undocumented.+--+-- Please limit yourself to the "Error.Diagnose.Report" module, which exports some of the useful functions defined here.+module Error.Diagnose.Report.Internal where++#ifdef USE_AESON+import Data.Aeson (ToJSON(..), object, (.=))+#endif+import Data.Bifunctor (bimap, first, second)+import Data.Default (def)+import Data.Foldable (fold)+import Data.Function (on)+import Data.Functor ((<&>))+import Data.HashMap.Lazy (HashMap)+import qualified Data.HashMap.Lazy as HashMap+import qualified Data.List as List+import qualified Data.List.Safe as List+import Data.Ord (Down (..))+import Error.Diagnose.Position+import Prettyprinter (Doc, Pretty (..), align, annotate, colon, hardline, space, width, (<+>))+import Prettyprinter.Internal (Doc (..))+import Prettyprinter.Render.Terminal (AnsiStyle, Color (..), bold, color, colorDull)++-- | The type of diagnostic reports with abstract message type.+data Report msg+ = Report+ Bool+ -- ^ Is the report a warning or an error?+ msg+ -- ^ The message associated with the error.+ [(Position, Marker msg)]+ -- ^ A map associating positions with marker to show under the source code.+ [msg]+ -- ^ A list of hints to add at the end of the report.++instance Semigroup msg => Semigroup (Report msg) where+ Report isError1 msg1 pos1 hints1 <> Report isError2 msg2 pos2 hints2 =+ Report (isError1 || isError2) (msg1 <> msg2) (pos1 <> pos2) (hints1 <> hints2)++instance Monoid msg => Monoid (Report msg) where+ mempty = Report False mempty mempty mempty++#ifdef USE_AESON+instance ToJSON msg => ToJSON (Report msg) where+ toJSON (Report isError msg markers hints) =+ object [ "kind" .= (if isError then "error" else "warning" :: String)+ , "message" .= msg+ , "markers" .= fmap showMarker markers+ , "hints" .= hints+ ]+ where+ showMarker (pos, marker) =+ object $ [ "position" .= pos ]+ <> case marker of+ This m -> [ "message" .= m+ , "kind" .= ("this" :: String)+ ]+ Where m -> [ "message" .= m+ , "kind" .= ("where" :: String)+ ]+ Maybe m -> [ "message" .= m+ , "kind" .= ("maybe" :: String)+ ]+#endif++-- | The type of markers with abstract message type, shown under code lines.+data Marker msg+ = -- | A red or yellow marker under source code, marking important parts of the code.+ This msg+ | -- | A blue marker symbolizing additional information.+ Where msg+ | -- | A magenta marker to report potential fixes.+ Maybe msg++instance Eq (Marker msg) where+ This _ == This _ = True+ Where _ == Where _ = True+ Maybe _ == Maybe _ = True+ _ == _ = False+ {-# INLINEABLE (==) #-}++instance Ord (Marker msg) where+ This _ < _ = False+ Where _ < This _ = True+ Where _ < _ = False+ Maybe _ < _ = True+ {-# INLINEABLE (<) #-}++ m1 <= m2 = m1 < m2 || m1 == m2+ {-# INLINEABLE (<=) #-}++-- | Constructs a warning or an error report.+warn,+ err ::+ -- | The report message, shown at the very top.+ msg ->+ -- | A list associating positions with markers.+ [(Position, Marker msg)] ->+ -- | A possibly mempty list of hints to add at the end of the report.+ [msg] ->+ Report msg+warn = Report False+{-# INLINE warn #-}+err = Report True+{-# INLINE err #-}++-- | Pretty prints a report to a 'Doc' handling colors.+prettyReport ::+ Pretty msg =>+ -- | The content of the file the reports are for+ HashMap FilePath [String] ->+ -- | Should we print paths in unicode?+ Bool ->+ -- | The whole report to output+ Report msg ->+ Doc AnsiStyle+prettyReport fileContent withUnicode (Report isError message markers hints) =+ let sortedMarkers = List.sortOn (fst . begin . fst) markers+ -- sort the markers so that the first lines of the reports are the first lines of the file++ groupedMarkers = groupMarkersPerFile sortedMarkers+ -- group markers by the file they appear in, and put `This` markers at the top of the report++ maxLineNumberLength = maybe 3 (max 3 . length . show . fst . end . fst) $ List.safeLast markers+ -- if there are no markers, then default to 3, else get the maximum between 3 and the length of the last marker++ header = annotate bold if isError then annotate (color Red) "[error]" else annotate (color Yellow) "[warning]"+ in {-+ A report is of the form:+ (1) [error|warning]: <message>+ (2) +-> <file>+ (3) :+ (4) <line> | <line of code>+ : <marker lines>+ : <marker messages>+ (5) :+ : <hints>+ (6) -------++ -}++ {- (1) -} header <> colon <+> align (pretty message)+ <> {- (2), (3), (4) -} fold (uncurry (prettySubReport fileContent withUnicode isError maxLineNumberLength) <$> groupedMarkers)+ <> {- (5) -} ( if+ | null hints && null markers -> mempty+ | null hints -> mempty+ | otherwise -> hardline <+> dotPrefix maxLineNumberLength withUnicode+ )+ <> prettyAllHints hints maxLineNumberLength withUnicode+ <> hardline+ <> {- (6) -} ( if null markers && null hints+ then mempty+ else+ annotate (bold <> color Black) (pad (maxLineNumberLength + 2) (if withUnicode then '─' else '-') mempty <> if withUnicode then "╯" else "+")+ <> hardline+ )++-------------------------------------------------------------------------------------+----- INTERNAL STUFF ----------------------------------------------------------------+-------------------------------------------------------------------------------------++-- | Inserts a given number of character after a 'Doc'ument.+pad :: Int -> Char -> Doc ann -> Doc ann+pad n c d = width d \w -> pretty $ replicate (n - w) c++-- | Creates a "dot"-prefix for a report line where there is no code.+--+-- Pretty printing yields those results:+--+-- [with unicode] "@␣␣␣␣␣•␣@"+-- [without unicode] "@␣␣␣␣␣:␣@"+dotPrefix ::+ -- | The length of the left space before the bullet.+ Int ->+ -- | Whether to print with unicode characters or not.+ Bool ->+ Doc AnsiStyle+dotPrefix leftLen withUnicode = pad leftLen ' ' mempty <+> annotate (bold <> color Black) (if withUnicode then "•" else ":")+{-# INLINE dotPrefix #-}++-- | Creates a "pipe"-prefix for a report line where there is no code.+--+-- Pretty printing yields those results:+--+-- [with unicode] "@␣␣␣␣␣│␣@"+-- [without unicode] "@␣␣␣␣␣|␣@"+pipePrefix ::+ -- | The length of the left space before the pipe.+ Int ->+ -- | Whether to print with unicode characters or not.+ Bool ->+ Doc AnsiStyle+pipePrefix leftLen withUnicode = pad leftLen ' ' mempty <+> annotate (bold <> color Black) (if withUnicode then "│" else "|")+{-# INLINE pipePrefix #-}++-- | Creates a line-prefix for a report line containing source code+--+-- Pretty printing yields those results:+--+-- [with unicode] "@␣␣␣3␣│␣@"+-- [without unicode] "@␣␣␣3␣|␣@"+--+-- Results may be different, depending on the length of the line number.+linePrefix ::+ -- | The length of the amount of space to span before the vertical bar.+ Int ->+ -- | The line number to show.+ Int ->+ -- | Whether to use unicode characters or not.+ Bool ->+ Doc AnsiStyle+linePrefix leftLen lineNo withUnicode =+ let lineNoLen = length (show lineNo)+ in annotate (bold <> color Black) $ mempty <+> pad (leftLen - lineNoLen) ' ' mempty <> pretty lineNo <+> if withUnicode then "│" else "|"+{-# INLINE linePrefix #-}++groupMarkersPerFile ::+ Pretty msg =>+ [(Position, Marker msg)] ->+ [(Bool, [(Position, Marker msg)])]+groupMarkersPerFile [] = []+groupMarkersPerFile markers =+ let markersPerFile = List.foldl' (HashMap.unionWith (<>)) mempty $ markers <&> \tup@(p, _) -> HashMap.singleton (file p) [tup]+ in -- put all markers on the same file together+ -- NOTE: it's a shame that `HashMap.unionsWith f = foldl' (HashMap.unionWith f) mempty` does not exist++ onlyFirstToTrue $ putThisMarkersAtTop $ HashMap.elems markersPerFile+ where+ onlyFirstToTrue = go True []++ go _ acc [] = reverse acc+ go t acc (x : xs) = go False ((t, x) : acc) xs++ putThisMarkersAtTop = List.sortBy \ms1 ms2 ->+ if+ | any isThisMarker (snd <$> ms1) -> LT+ | any isThisMarker (snd <$> ms2) -> GT+ | otherwise -> EQ++-- | Prettyprint a sub-report, which is a part of the report spanning across a single file+prettySubReport ::+ Pretty msg =>+ -- | The content of files in the diagnostics+ HashMap FilePath [String] ->+ -- | Is the output done with Unicode characters?+ Bool ->+ -- | Is the current report an error report?+ Bool ->+ -- | The size of the biggest line number+ Int ->+ -- | Is this sub-report the first one in the list?+ Bool ->+ -- | The list of line-ordered markers appearing in a single file+ [(Position, Marker msg)] ->+ Doc AnsiStyle+prettySubReport fileContent withUnicode isError maxLineNumberLength isFirst markers =+ let (markersPerLine, multilineMarkers) = splitMarkersPerLine markers+ -- split the list on whether markers are multiline or not++ sortedMarkersPerLine = {- second (List.sortOn (first $ snd . begin)) <$> -} List.sortOn fst (HashMap.toList markersPerLine)++ reportFile = maybe (pretty @Position def) (pretty . fst) $ List.safeHead (List.sortOn snd markers)+ -- the reported file is the file of the first 'This' marker (only one must be present)++ allLineNumbers = List.sort $ List.nub $ (fst <$> sortedMarkersPerLine) <> (multilineMarkers >>= \(Position (bl, _) (el, _) _, _) -> [bl .. el])++ fileMarker =+ ( if isFirst+ then+ space <> pad maxLineNumberLength ' ' mempty+ <+> annotate (bold <> color Black) (if withUnicode then "╭──▶" else "+-->")+ else+ space <> dotPrefix maxLineNumberLength withUnicode <> hardline+ <> annotate (bold <> color Black) (pad (maxLineNumberLength + 2) (if withUnicode then '─' else '-') mempty)+ <> annotate (bold <> color Black) (if withUnicode then "┼──▶" else "+-->")+ )+ <+> annotate (bold <> colorDull Green) reportFile+ in {- (2) -} hardline <> fileMarker+ <> hardline+ <+> {- (3) -} pipePrefix maxLineNumberLength withUnicode+ <> {- (4) -} prettyAllLines fileContent withUnicode isError maxLineNumberLength sortedMarkersPerLine multilineMarkers allLineNumbers++isThisMarker :: Marker msg -> Bool+isThisMarker (This _) = True+isThisMarker _ = False++-- |+splitMarkersPerLine :: [(Position, Marker msg)] -> (HashMap Int [(Position, Marker msg)], [(Position, Marker msg)])+splitMarkersPerLine [] = (mempty, mempty)+splitMarkersPerLine (m@(Position {..}, _) : ms) =+ let (bl, _) = begin+ (el, _) = end+ in (if bl == el then first (HashMap.insertWith (<>) bl [m]) else second (m :))+ (splitMarkersPerLine ms)++-- |+prettyAllLines ::+ Pretty msg =>+ HashMap FilePath [String] ->+ Bool ->+ Bool ->+ Int ->+ [(Int, [(Position, Marker msg)])] ->+ [(Position, Marker msg)] ->+ [Int] ->+ Doc AnsiStyle+prettyAllLines _ _ _ _ _ [] [] = mempty+prettyAllLines _ withUnicode isError leftLen _ multiline [] =+ let colorOfLastMultilineMarker = maybe mempty (markerColor isError . snd) (List.safeLast multiline)+ -- take the color of the last multiline marker in case we need to add additional bars++ prefix = hardline <+> dotPrefix leftLen withUnicode <> space+ prefixWithBar color = prefix <> annotate color (if withUnicode then "│ " else "| ")++ showMultilineMarkerMessage (_, marker) isLast =+ annotate (markerColor isError marker) $+ ( if isLast+ then if withUnicode then "╰╸ " else "`- "+ else if withUnicode then "├╸ " else "|- "+ )+ <> replaceLinesWith (if isLast then prefix <> " " else prefixWithBar (markerColor isError marker) <> space) (pretty $ markerMessage marker)++ showMultilineMarkerMessages [] = []+ showMultilineMarkerMessages [m] = [showMultilineMarkerMessage m True]+ showMultilineMarkerMessages (m : ms) = showMultilineMarkerMessage m False : showMultilineMarkerMessages ms+ in prefixWithBar colorOfLastMultilineMarker <> prefix <> fold (List.intersperse prefix $ showMultilineMarkerMessages $ reverse multiline)+prettyAllLines files withUnicode isError leftLen inline multiline (line : ls) =+ {-+ A line of code is composed of:+ (1) <line> | <source code>+ (2) : <markers>+ (3) : <marker messages>++ Multline markers may also take additional space (2 characters) on the right of the bar+ -}+ let allInlineMarkersInLine = snd =<< filter ((==) line . fst) inline++ allMultilineMarkersInLine = flip filter multiline \(Position (bl, _) (el, _) _, _) -> bl == line || el == line++ allMultilineMarkersSpanningLine = flip filter multiline \(Position (bl, _) (el, _) _, _) -> bl < line && el > line++ inSpanOfMultiline = flip any multiline \(Position (bl, _) (el, _) _, _) -> bl <= line && el >= line++ colorOfFirstMultilineMarker = maybe id (annotate . markerColor isError . snd) (List.safeHead $ allMultilineMarkersInLine <> allMultilineMarkersSpanningLine)+ -- take the first multiline marker to color the entire line, if there is one++ !additionalPrefix = case allMultilineMarkersInLine of+ [] ->+ if not $ null multiline+ then+ if not $ null allMultilineMarkersSpanningLine+ then colorOfFirstMultilineMarker if withUnicode then "│ " else "| "+ else " "+ else mempty+ (p@(Position _ (el, _) _), marker) : _ ->+ let hasPredecessor = el == line || maybe False ((/=) p . fst . fst) (List.safeUncons multiline)+ in colorOfFirstMultilineMarker+ ( if+ | hasPredecessor && withUnicode -> "├"+ | hasPredecessor -> "|"+ | withUnicode -> "╭"+ | otherwise -> "+"+ )+ <> annotate (markerColor isError marker) (if withUnicode then "┤" else ">")+ <> space++ allMarkersInLine = {- List.sortOn fst $ -} allInlineMarkersInLine <> allMultilineMarkersInLine+ in hardline+ <> {- (1) -} linePrefix leftLen line withUnicode <+> additionalPrefix+ <> getLine_ files (allMarkersInLine <> allMultilineMarkersSpanningLine) line isError+ <> {- (2) -} showAllMarkersInLine (not $ null multiline) inSpanOfMultiline colorOfFirstMultilineMarker withUnicode isError leftLen allInlineMarkersInLine+ <> {- (3) -} prettyAllLines files withUnicode isError leftLen inline multiline ls++-- |+getLine_ :: HashMap FilePath [String] -> [(Position, Marker msg)] -> Int -> Bool -> Doc AnsiStyle+getLine_ files markers line isError = case List.safeIndex (line - 1) =<< (HashMap.!?) files . file . fst =<< List.safeHead markers of+ Nothing -> annotate (bold <> colorDull Magenta) "<no line>"+ Just code ->+ fold $+ indexed code <&> \(n, c) ->+ let colorizingMarkers = flip+ filter+ markers+ \(Position (bl, bc) (el, ec) _, _) ->+ if bl == el+ then n >= bc && n < ec+ else (bl == line && n >= bc) || (el == line && n < ec)+ in maybe id ((\m -> annotate (bold <> markerColor isError m)) . snd) (List.safeHead colorizingMarkers) (pretty c)+ where+ indexed :: [a] -> [(Int, a)]+ indexed = goIndexed 1++ goIndexed :: Int -> [a] -> [(Int, a)]+ goIndexed _ [] = []+ goIndexed n (x : xs) = (n, x) : goIndexed (n + 1) xs++-- |+showAllMarkersInLine :: Pretty msg => Bool -> Bool -> (Doc AnsiStyle -> Doc AnsiStyle) -> Bool -> Bool -> Int -> [(Position, Marker msg)] -> Doc AnsiStyle+showAllMarkersInLine _ _ _ _ _ _ [] = mempty+showAllMarkersInLine hasMultilines inSpanOfMultiline colorMultilinePrefix withUnicode isError leftLen ms =+ let maxMarkerColumn = snd $ end $ fst $ List.last $ List.sortOn (snd . end . fst) ms+ specialPrefix =+ if inSpanOfMultiline+ then colorMultilinePrefix (if withUnicode then "│ " else "| ") <> space+ else+ if hasMultilines+ then colorMultilinePrefix " " <> space+ else mempty+ in -- get the maximum end column, so that we know when to stop looking for other markers on the same line+ hardline <+> dotPrefix leftLen withUnicode <+> (if List.null ms then mempty else specialPrefix <> showMarkers 1 maxMarkerColumn <> showMessages specialPrefix ms maxMarkerColumn)+ where+ showMarkers n lineLen+ | n > lineLen = mempty -- reached the end of the line+ | otherwise =+ let allMarkers = flip filter ms \(Position (_, bc) (_, ec) _, _) -> n >= bc && n < ec+ in -- only consider markers which span onto the current column+ case allMarkers of+ [] -> space <> showMarkers (n + 1) lineLen+ (Position {..}, marker) : _ ->+ if snd begin == n+ then annotate (markerColor isError marker) (if withUnicode then "┬" else "^") <> showMarkers (n + 1) lineLen+ else annotate (markerColor isError marker) (if withUnicode then "─" else "-") <> showMarkers (n + 1) lineLen+ -- if the marker just started on this column, output a caret, else output a dash++ showMessages specialPrefix ms lineLen = case List.safeUncons ms of+ Nothing -> mempty -- no more messages to show+ Just ((Position b@(_, bc) _ _, msg), pipes) ->+ let filteredPipes = filter ((/= b) . begin . fst) pipes+ -- record only the pipes corresponding to markers on different starting positions+ nubbedPipes = List.nubBy ((==) `on` (begin . fst)) filteredPipes+ -- and then remove all duplicates++ allColumns _ [] = (1, [])+ allColumns n ms@((Position (_, bc) _ _, col) : ms')+ | n == bc = bimap (+ 1) (col :) (allColumns (n + 1) ms')+ | n < bc = bimap (+ 1) (space :) (allColumns (n + 1) ms)+ | otherwise = bimap (+ 1) (space :) (allColumns (n + 1) ms')+ -- transform the list of remaining markers into a single document line++ hasSuccessor = length filteredPipes /= length pipes++ lineStart pipes =+ let (n, docs) = allColumns 1 $ List.sortOn (snd . begin . fst) pipes+ in dotPrefix leftLen withUnicode <+> specialPrefix <> fold docs <> pretty (replicate (bc - n) ' ')+ -- the start of the line contains the "dot"-prefix as well as all the pipes for all the still not rendered marker messages++ prefix =+ let (pipesBefore, pipesAfter) = List.partition ((< bc) . snd . begin . fst) nubbedPipes+ -- split the list so that all pipes before can have `|`s but pipes after won't++ pipesBeforeRendered = pipesBefore <&> second \marker -> annotate (markerColor isError marker) (if withUnicode then "│" else "|")+ -- pre-render pipes which are before because they will be shown++ lastBeginPosition = snd . begin . fst <$> List.safeLast (List.sortOn (Down . snd . begin . fst) pipesAfter)++ lineLen = case lastBeginPosition of+ Nothing -> 0+ Just col -> col - bc++ currentPipe =+ if+ | withUnicode && hasSuccessor -> "├"+ | withUnicode -> "╰"+ | hasSuccessor -> "|"+ | otherwise -> "`"++ lineChar = if withUnicode then '─' else '-'+ pointChar = if withUnicode then "╸" else "-"+ in lineStart pipesBeforeRendered+ <> annotate (markerColor isError msg) (currentPipe <> pretty (replicate lineLen lineChar) <> pointChar)+ <+> annotate (markerColor isError msg) (replaceLinesWith (hardline <+> lineStart pipesBeforeRendered <+> " ") $ pretty $ markerMessage msg)+ in hardline <+> prefix <> showMessages specialPrefix pipes lineLen++-- WARN: uses the internal of the library+--+-- DO NOT use a wildcard here, in case the internal API exposes one more constructor++-- |+replaceLinesWith :: Doc ann -> Doc ann -> Doc ann+replaceLinesWith repl Line = repl+replaceLinesWith _ Fail = Fail+replaceLinesWith _ Empty = Empty+replaceLinesWith _ (Char c) = Char c+replaceLinesWith _ (Text n s) = Text n s+replaceLinesWith repl (FlatAlt f d) = FlatAlt (replaceLinesWith repl f) (replaceLinesWith repl d)+replaceLinesWith repl (Cat c d) = Cat (replaceLinesWith repl c) (replaceLinesWith repl d)+replaceLinesWith repl (Nest n d) = Nest n (replaceLinesWith repl d)+replaceLinesWith repl (Union c d) = Union (replaceLinesWith repl c) (replaceLinesWith repl d)+replaceLinesWith repl (Column f) = Column (replaceLinesWith repl . f)+replaceLinesWith repl (Nesting f) = Nesting (replaceLinesWith repl . f)+replaceLinesWith repl (Annotated ann doc) = Annotated ann (replaceLinesWith repl doc)+replaceLinesWith repl (WithPageWidth f) = WithPageWidth (replaceLinesWith repl . f)++-- | Extracts the color of a marker as a 'Doc' coloring function.+markerColor ::+ -- | Whether the marker is in an error context or not.+ -- This really makes a difference for a 'This' marker.+ Bool ->+ -- | The marker to extract the color from.+ Marker msg ->+ -- | A function used to color a 'Doc'.+ AnsiStyle+markerColor isError (This _) = if isError then color Red else color Yellow+markerColor _ (Where _) = colorDull Blue+markerColor _ (Maybe _) = color Magenta+{-# INLINE markerColor #-}++-- | Retrieves the message held by a marker.+markerMessage :: Marker msg -> msg+markerMessage (This m) = m+markerMessage (Where m) = m+markerMessage (Maybe m) = m+{-# INLINE markerMessage #-}++-- | Pretty prints all hints.+prettyAllHints :: Pretty msg => [msg] -> Int -> Bool -> Doc AnsiStyle+prettyAllHints [] _ _ = mempty+prettyAllHints (h : hs) leftLen withUnicode =+ {-+ A hint is composed of:+ (1) : Hint: <hint message>+ -}+ let prefix = hardline <+> pipePrefix leftLen withUnicode+ in prefix <+> annotate (color Cyan) (annotate bold "Hint:" <+> replaceLinesWith (prefix <+> " ") (pretty h))+ <> prettyAllHints hs leftLen withUnicode
+ test/megaparsec/Spec.hs view
@@ -0,0 +1,39 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++{-# OPTIONS -Wno-orphans #-}++import Data.Bifunctor (first)+import Data.Text (Text)+import qualified Data.Text as Text (unpack)+import Data.Void (Void)++import Error.Diagnose+import Error.Diagnose.Compat.Megaparsec++import qualified Text.Megaparsec as MP+import qualified Text.Megaparsec.Char as MP+import qualified Text.Megaparsec.Char.Lexer as MP++instance HasHints Void msg where+ hints _ = mempty++main :: IO ()+main = do+ let filename :: FilePath = "<interactive>"+ content1 :: Text = "0000000123456"+ content2 :: Text = "00000a2223266"++ let res1 = first (errorDiagnosticFromBundle "Parse error on input" Nothing) $ MP.runParser @Void (MP.some MP.decimal <* MP.eof) filename content1+ res2 = first (errorDiagnosticFromBundle "Parse error on input" Nothing) $ MP.runParser @Void (MP.some MP.decimal <* MP.eof) filename content2++ case res1 of+ Left diag -> printDiagnostic stdout True True (addFile diag filename (Text.unpack content1) :: Diagnostic String)+ Right res -> print res+ case res2 of+ Left diag -> printDiagnostic stdout True True (addFile diag filename (Text.unpack content2) :: Diagnostic String)+ Right res -> print res
+ test/parsec/Spec.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++{-# OPTIONS -Wno-orphans #-}++import Data.Bifunctor (first)+import Data.Text (Text)+import qualified Data.Text as Text (unpack)+import Data.Void (Void)++import Error.Diagnose+import Error.Diagnose.Compat.Parsec++import qualified Text.Parsec as P++instance HasHints Void msg where+ hints _ = mempty++main :: IO ()+main = do+ let filename :: FilePath = "<interactive>"+ content1 :: Text = "0000000123456"+ content2 :: Text = "00000a2223266"++ let res1 = first (errorDiagnosticFromParseError "Parse error on input" Nothing) $ P.parse (P.many1 P.digit <* P.eof) filename content1+ res2 = first (errorDiagnosticFromParseError "Parse error on input" Nothing) $ P.parse (P.many1 P.digit <* P.eof) filename content2++ case res1 of+ Left diag -> printDiagnostic stdout True True (addFile diag filename (Text.unpack content1) :: Diagnostic String)+ Right res -> print res+ case res2 of+ Left diag -> printDiagnostic stdout True True (addFile diag filename (Text.unpack content2) :: Diagnostic String)+ Right res -> print res+
+ test/rendering/Spec.hs view
@@ -0,0 +1,312 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE ScopedTypeVariables #-}++#ifdef USE_AESON+ diagnosticToJson,+#endif++#ifdef USE_AESON+import qualified Data.ByteString.Lazy as BS+#endif+import Data.HashMap.Lazy (HashMap)+import qualified Data.HashMap.Lazy as HashMap+import Error.Diagnose+ ( Marker (..),+ Position (..),+ Report,+ addFile,+ addReport,+ def,+ err,+ printDiagnostic,+ stdout,+ warn,+ )+import System.IO (hPutStrLn)++main :: IO ()+main = do+ let files :: HashMap FilePath String =+ HashMap.fromList+ [ ("test.zc", "let id<a>(x : a) : a := x + 1\nrec fix(f) := f(fix(f))\nlet const<a, b>(x : a, y : b) : a := x"),+ ("somefile.zc", "let id<a>(x : a) : a := x\n + 1"),+ ("err.nst", "\n\n\n\n = jmp g\n\n g: forall(s: Ts, e: Tc).{ %r0: *s64 | s -> e }"),+ ("unsized.nst", "main: forall(a: Ta, s: Ts, e: Tc).{ %r5: forall().{| s -> e } | s -> %r5 }\n = salloc a\n ; sfree\n")+ ]++ let reports =+ [ errorNoMarkersNoHints,+ errorSingleMarkerNoHints,+ warningSingleMarkerNoHints,+ errorTwoMarkersSameLineNoOverlapNoHints,+ errorSingleMarkerOutOfBoundsNoHints,+ errorTwoMarkersSameLineOverlapNoHints,+ errorTwoMarkersSameLinePartialOverlapNoHints,+ errorTwoMarkersTwoLinesNoHints,+ realWorldExample,+ errorTwoMarkersSamePositionNoHints,+ errorThreeMarkersWithOverlapNoHints,+ errorWithMultilineErrorNoMarkerNoHints,+ errorSingleMultilineMarkerMessageNoHints,+ errorTwoMarkersSameOriginOverlapNoHints,+ errorNoMarkersSingleHint,+ errorNoMarkersSingleMultilineHint,+ errorNoMarkersTwoHints,+ errorSingleMultilineMarkerNoHints,+ errorTwoMarkersWithMultilineNoHints,+ errorTwoMultilineMarkersNoHints,+ errorSingleMultilineMarkerMultilineMessageNoHints,+ errorTwoMultilineMarkersFirstMultilineMessageNoHints,+ errorThreeMultilineMarkersTwoMultilineMessageNoHints,+ errorOrderSensitive,+ errorMultilineAfterSingleLine,+ errorOnEmptyLine,+ errorMultipleFiles,+ beautifulExample+ ]++ let diag = HashMap.foldlWithKey' addFile (foldr (flip addReport) def reports) files++ hPutStrLn stdout "\n\nWith unicode: ─────────────────────────\n"+ printDiagnostic stdout True True diag+ hPutStrLn stdout "\n\nWithout unicode: ----------------------\n"+ printDiagnostic stdout False True diag+#ifdef USE_AESON+ hPutStrLn stdout "\n\nAs JSON: ------------------------------\n"+ BS.hPutStr stdout (diagnosticToJson diag)+#endif+ hPutStrLn stdout "\n"++errorNoMarkersNoHints :: Report String+errorNoMarkersNoHints =+ err+ "Error with no marker"+ []+ []++errorSingleMarkerNoHints :: Report String+errorSingleMarkerNoHints =+ err+ "Error with one marker in bounds"+ [(Position (1, 25) (1, 30) "test.zc", This "Required here")]+ []++warningSingleMarkerNoHints :: Report String+warningSingleMarkerNoHints =+ warn+ "Warning with one marker in bounds"+ [(Position (1, 25) (1, 30) "test.zc", This "Required here")]+ []++errorTwoMarkersSameLineNoOverlapNoHints :: Report String+errorTwoMarkersSameLineNoOverlapNoHints =+ err+ "Error with two markers in bounds (no overlap) on the same line"+ [ (Position (1, 5) (1, 10) "test.zc", This "First"),+ (Position (1, 15) (1, 22) "test.zc", Where "Second")+ ]+ []++errorSingleMarkerOutOfBoundsNoHints :: Report String+errorSingleMarkerOutOfBoundsNoHints =+ err+ "Error with one marker out of bounds"+ [(Position (10, 5) (10, 15) "test2.zc", This "Out of bounds")]+ []++errorTwoMarkersSameLineOverlapNoHints :: Report String+errorTwoMarkersSameLineOverlapNoHints =+ err+ "Error with two overlapping markers in bounds"+ [ (Position (1, 6) (1, 13) "test.zc", This "First"),+ (Position (1, 10) (1, 15) "test.zc", Where "Second")+ ]+ []++errorTwoMarkersSameLinePartialOverlapNoHints :: Report String+errorTwoMarkersSameLinePartialOverlapNoHints =+ err+ "Error with two partially overlapping markers in bounds"+ [ (Position (1, 5) (1, 25) "test.zc", This "First"),+ (Position (1, 12) (1, 20) "test.zc", Where "Second")+ ]+ []++errorTwoMarkersTwoLinesNoHints :: Report String+errorTwoMarkersTwoLinesNoHints =+ err+ "Error with two markers on two lines in bounds"+ [ (Position (1, 5) (1, 12) "test.zc", This "First"),+ (Position (2, 3) (2, 4) "test.zc", Where "Second")+ ]+ []++realWorldExample :: Report String+realWorldExample =+ err+ "Could not deduce constraint 'Num(a)' from the current context"+ [ (Position (1, 25) (1, 30) "test.zc", This "While applying function '+'"),+ (Position (1, 11) (1, 16) "test.zc", Where "'x' is supposed to have type 'a'"),+ (Position (1, 8) (1, 9) "test.zc", Where "type 'a' is bound here without constraints")+ ]+ ["Adding 'Num(a)' to the list of constraints may solve this problem."]++errorTwoMarkersSamePositionNoHints :: Report String+errorTwoMarkersSamePositionNoHints =+ err+ "Error with two markers on the same exact position in bounds"+ [ (Position (1, 6) (1, 10) "test.zc", This "First"),+ (Position (1, 6) (1, 10) "test.zc", Maybe "Second")+ ]+ []++errorThreeMarkersWithOverlapNoHints :: Report String+errorThreeMarkersWithOverlapNoHints =+ err+ "Error with three markers with overlapping in bounds"+ [ (Position (1, 9) (1, 15) "test.zc", This "First"),+ (Position (1, 9) (1, 18) "test.zc", Maybe "Second"),+ (Position (1, 6) (1, 10) "test.zc", Where "Third")+ ]+ []++errorWithMultilineErrorNoMarkerNoHints :: Report String+errorWithMultilineErrorNoMarkerNoHints =+ err+ "Error with multi\nline message and no markers"+ []+ []++errorSingleMultilineMarkerMessageNoHints :: Report String+errorSingleMultilineMarkerMessageNoHints =+ err+ "Error with single marker with multiline message"+ [(Position (1, 9) (1, 15) "test.zc", This "First\nmultiline")]+ []++errorTwoMarkersSameOriginOverlapNoHints :: Report String+errorTwoMarkersSameOriginOverlapNoHints =+ err+ "Error with two markers with same origin but partial overlap in bounds"+ [ (Position (1, 9) (1, 15) "test.zc", This "First"),+ (Position (1, 9) (1, 20) "test.zc", Maybe "Second")+ ]+ []++errorNoMarkersSingleHint :: Report String+errorNoMarkersSingleHint =+ err+ "Error with no marker and one hint"+ []+ ["First hint"]++errorNoMarkersSingleMultilineHint :: Report String+errorNoMarkersSingleMultilineHint =+ err+ "Error with no marker and one multiline hint"+ []+ ["First multi\nline hint"]++errorNoMarkersTwoHints :: Report String+errorNoMarkersTwoHints =+ err+ "Error with no markers and two hints"+ []+ [ "First hint",+ "Second hint"+ ]++errorSingleMultilineMarkerNoHints :: Report String+errorSingleMultilineMarkerNoHints =+ err+ "Error with single marker spanning across multiple lines"+ [(Position (1, 15) (2, 6) "test.zc", This "First")]+ []++errorTwoMarkersWithMultilineNoHints :: Report String+errorTwoMarkersWithMultilineNoHints =+ err+ "Error with two markers, one single line and one multiline, in bounds"+ [ (Position (1, 9) (1, 13) "test.zc", This "First"),+ (Position (1, 14) (2, 6) "test.zc", Where "Second")+ ]+ []++errorTwoMultilineMarkersNoHints :: Report String+errorTwoMultilineMarkersNoHints =+ err+ "Error with two multiline markers in bounds"+ [ (Position (1, 9) (2, 5) "test.zc", This "First"),+ (Position (2, 1) (3, 10) "test.zc", Where "Second")+ ]+ []++errorSingleMultilineMarkerMultilineMessageNoHints :: Report String+errorSingleMultilineMarkerMultilineMessageNoHints =+ err+ "Error with one multiline marker with a multiline message in bounds"+ [(Position (1, 9) (2, 5) "test.zc", This "Multi\nline message")]+ []++errorTwoMultilineMarkersFirstMultilineMessageNoHints :: Report String+errorTwoMultilineMarkersFirstMultilineMessageNoHints =+ err+ "Error with two multiline markers with one multiline message in bounds"+ [ (Position (1, 9) (2, 5) "test.zc", This "First"),+ (Position (1, 9) (2, 6) "test.zc", Where "Multi\nline message")+ ]+ []++errorThreeMultilineMarkersTwoMultilineMessageNoHints :: Report String+errorThreeMultilineMarkersTwoMultilineMessageNoHints =+ err+ "Error with three multiline markers with two multiline messages in bounds"+ [ (Position (1, 9) (2, 5) "test.zc", This "First"),+ (Position (1, 9) (2, 6) "test.zc", Where "Multi\nline message"),+ (Position (1, 9) (2, 7) "test.zc", Maybe "Multi\nline message #2")+ ]+ []++errorOrderSensitive :: Report String+errorOrderSensitive =+ err+ "Order-sensitive labels with crossing"+ [ (Position (1, 1) (1, 7) "somefile.zc", This "Leftmost label"),+ (Position (1, 9) (1, 16) "somefile.zc", Where "Rightmost label")+ ]+ []++beautifulExample :: Report String+beautifulExample =+ err+ "Could not deduce constraint 'Num(a)' from the current context"+ [ (Position (1, 25) (2, 6) "somefile.zc", This "While applying function '+'"),+ (Position (1, 11) (1, 16) "somefile.zc", Where "'x' is supposed to have type 'a'"),+ (Position (1, 8) (1, 9) "somefile.zc", Where "type 'a' is bound here without constraints")+ ]+ ["Adding 'Num(a)' to the list of constraints may solve this problem."]++errorMultilineAfterSingleLine :: Report String+errorMultilineAfterSingleLine =+ err+ "Multiline after single line"+ [ (Position (1, 17) (1, 18) "unsized.nst", Where "Kind is infered from here"),+ (Position (2, 14) (3, 0) "unsized.nst", This "is an error")+ ]+ []++errorOnEmptyLine :: Report String+errorOnEmptyLine =+ err+ "Error on empty line"+ [(Position (1, 5) (3, 8) "err.nst", This "error on empty line")]+ []++errorMultipleFiles :: Report String+errorMultipleFiles =+ err+ "Error on multiple files"+ [ (Position (1, 5) (1, 7) "test.zc", Where "Function already declared here"),+ (Position (1, 5) (1, 7) "somefile.zc", This "Function `id` is already declared in another module")+ ]+ []