Coder Social home page Coder Social logo

mesabloo / diagnose Goto Github PK

View Code? Open in Web Editor NEW
232.0 3.0 17.0 1.57 MB

A simple library for reporting compiler/interpreter errors

Home Page: https://hackage.haskell.org/package/diagnose

License: BSD 3-Clause "New" or "Revised" License

Haskell 99.51% Nix 0.49%
error-reporting unicode ascii haskell library compiler-errors interpreter-errors

diagnose's Introduction

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, but ended up quickly becoming its own thing.

As a great example, here's the output of the last test:

first example

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:

second example

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!
  • Support for optional custom error codes, if you want to go the Rust way
  • Variable width Unicode characters are handled in a crossplatform manner
  • TAB characters have custom sizes specified when printing a diagnostic, so that you decide the width of a TAB, not your terminal emulator!
  • Colors can be tweaked thanks to the ability to export diagnostics as Documents

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 is an instance of Monoid, 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:

-- | An optional error code, shown right after @error@ or @warning@ in the square brackets
Maybe msg ->
-- | 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
[Note 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 file Handle (most likely stdout or stderr) using printDiagnostic;
  • create a Document which can be further altered using prettyDiagnostic;
  • 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:

let beautifulExample =
      err
        Nothing
        "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."]
        -- ^^^^ This is a 'Note' not a 'Hint', as specified by its 'IsString' instance

-- Create the diagnostic
let diagnostic  = addFile mempty "somefile.zc" "let id<a>(x : a) : a := x\n  + 1"
let diagnostic' = addReport diagnostic beautifulExample

-- Print with unicode characters, and the default (colorful) style
printDiagnostic stdout WithUnicode 4 defaultStyle diagnostic'

More examples are given in the test/rendering folder (execute stack test to see the output).

TODO list

<< empty, for now >>

License

This work is licensed under the BSD-3 clause license.

Copyright (c) 2021-2022 Mesabloo, all rights reserved.

diagnose's People

Contributors

byorgey avatar expipiplus1 avatar jaspa avatar luc-tielen avatar mesabloo avatar spacekitteh avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

diagnose's Issues

Consider exporting `prettyDiagnostic`

The current functionality requires an output to a file handle, but allowing the conversion to a Prettyprinter.Doc format would enable this to be embedded in other Doc output and managed as needed by client applications. Would exporting prettyDiagnostic be a reasonable update?

Unexpected addReport ordering

Currently addReport will prepend new reports to the report list. This causes the report generation to show the reports in the reverse order generated.

Here's a modified version of test/megaparsec/Spec.hs that captures multiple parse errors:

{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

{-# OPTIONS -Wno-orphans #-}

import Data.Bifunctor (first)
import Data.Char (isAlpha)
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"
      content3 = "aaa\naab\naba\na\nb\n"

  let res1 = first (errorDiagnosticFromBundle Nothing "Parse error on input" Nothing) $ MP.runParser @Void (MP.some MP.decimal <* MP.eof) filename content1
      res2 = first (errorDiagnosticFromBundle Nothing "Parse error on input" Nothing) $ MP.runParser @Void (MP.some MP.decimal <* MP.eof) filename content2
      res3 = first (errorDiagnosticFromBundle Nothing "Parse error on input" Nothing)
             $
             let a = MP.char 'a'
                 errSkip e = do MP.registerParseError e
                                _ <- MP.takeWhileP Nothing isAlpha <* MP.eol
                                return ""
             in MP.runParser @Void
                (MP.many (MP.withRecovery errSkip (MP.some a <* MP.eol)) <* MP.eof)
                filename content3

  case res1 of
    Left diag -> printDiagnostic stdout True True 4 (addFile diag filename (Text.unpack content1) :: Diagnostic String)
    Right res -> print res
  case res2 of
    Left diag -> printDiagnostic stdout True True 4 (addFile diag filename (Text.unpack content2) :: Diagnostic String)
    Right res -> print res
  putStrLn "------------- res3 ----------------"
  case res3 of
    Left diag -> printDiagnostic stdout True True 4 (addFile diag filename (Text.unpack content3) :: Diagnostic String)
    Right res -> print res

The above updated Megaparsec test demonstrates the inversion:

------------- res3 ----------------
[error]: Parse error on input
     ╭──▶ <interactive>@5:1-5:2
     │
   5 │ b
     • ┬
     • ├╸ unexpected 'b'
     • ╰╸ expecting 'a'
─────╯

[error]: Parse error on input
     ╭──▶ <interactive>@3:2-3:3
     │
   3 │ aba
     •  ┬
     •  ├╸ unexpected "ba"
     •  ╰╸ expecting 'a' or end of line
─────╯

[error]: Parse error on input
     ╭──▶ <interactive>@2:3-2:4
     │
   2 │ aab
     •   ┬
     •   ├╸ unexpected "b<newline>"
     •   ╰╸ expecting 'a' or end of line
─────╯

I would have expected the reports to be in the same order as the lines parsed. Perhaps addReport should append (and use Data.Sequence for performance), or else prettyDiagnostic should reverse the reports before generating them.

Incorrect bounds for unordered-containers

The bounds for unordered-containers are given as "==0.2.*":

, unordered-containers ==0.2.*

But this is incorrect: diagnose will not build with versions of unordered-containers < 0.2.11. It uses the !? function from Data.HashMap.Lazy, here:

case safeArrayIndex (line - 1) =<< (HashMap.!?) files . file . fst =<< List.safeHead markers of

and that function was not introduced into unordered-containers until version 0.2.11 (see here).

[Bug] Duplicated markers with Error.Diagnose.Compat.Parsec

Repro

When using parsec-diagnose 1.7.1 and parsec 3.1.14.0 and GHC version 9.0.2 I get duplicated markers with the simple code here:

{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}

import Error.Diagnose
import Error.Diagnose.Compat.Parsec
import Text.Parsec

import Data.Void
instance HasHints Void String where hints _ = []

type Parser = Parsec String ()

diagParse :: Parser a -> SourceName -> String -> Either (Diagnostic String) a
diagParse p filename content =
    either (Left . diag) Right (parse p filename content)
  where
    diag e = addFile (errorDiagnosticFromParseError Nothing "Parse error on input" Nothing e) filename content

parser :: Parser Char
parser = op' "\\" *> letter

main :: IO ()
main = either (printDiagnostic stderr True True) print $
  diagParse parser "issues/2.txt" "\\1"

-- smaller example
op' :: String -> Parser String
op' name = string name <* spaces

Output

[error]: Parse error on input
     ╭──▶ issues/2.txt@1:2-1:3
     │
   1 │ \1
     •  ┬ 
     •  ├╸ unexpected "1"
     •  ├╸ unexpected "1"
     •  ╰╸ expecting any of white space, letter
─────╯

Description

I stumbled on this while using the TokenParser from parsec:

import Text.Parsec.Token

op :: String -> Parser ()
op = reservedOp $ makeTokenParser LanguageDef {
    commentStart   = "{-"
  , commentEnd     = "-}"
  , commentLine    = "--"
  , reservedOpNames = ["\\"]
  , opStart        = oneOf "\\"
  , opLetter       = oneOf "\\"
  }

Using op as defined above I get:

[error]: Parse error on input
     ╭──▶ issues/2.txt@1:2-1:3
     │
   1 │ \1
     •  ┬ 
     •  ├╸ unexpected "1"
     •  ├╸ unexpected "1"
     •  ├╸ unexpected "1"
     •  ├╸ unexpected "1"
     •  ├╸ unexpected "1"
     •  ╰╸ expecting any of end of "\\", , letter
─────╯

Expected

I expect the above to only print a single line containing unexpected "1" for both cases.

What do columns in `Position` mean?

"Columns" of text are not well-defined in the presence of unicode input. I'd suggest specifying that it means unicode code points, and maybe testing with some awkward unicode text.

Multiline spans look weird

Here's my output:
image

  1. The middle row does not get highlighted
  2. I think the text associated with the multiline span would look better right under it's source, as opposed to the very bottom. It's also weird because there are no dots indicating there's a jump in lines between the first and the second source spans..

Thanks for making this awesome library!

Incorrect bounds for prettyprinter-ansi-terminal

The bounds for prettyprinter-ansi-terminal are given as ">=1.1.0 && <2":

, prettyprinter-ansi-terminal >=1.1.0 && <2

However, these bounds are incorrect: diagnose requires prettyprinter-ansi-terminal >= 1.1.2, and compilation will fail if versions of prettyprinter-ansi-terminal lower than that are used.

Specifically, in module Error.Diagnose.Diagnostic.Internal, Prettyprinter.Render.Terminal is imported:

import Prettyprinter.Render.Terminal (hPutDoc)

This module does not exist in versions of prettyprinter-ansi-terminal < 1.2 (see e.g. prettyprinter-ansi-terminal-1.1.1.2).

Integration with LSP?

The docs mention integration with LSP via the diagnosticToJson function, but no other information is given on how to do this.

I'm new to LSP myself, could you provide some pointers where to start looking for more information?
(Or add a section to Diagnose docs somewhere on how to quickly/easily integrate with LSP?)

Thanks!

Alternative `codespan-reporting` style formatting?

Thanks for your great library!

I use both Rust and Haskell, and have learned about error reporting libraries like ariadne and codespan-reporting for a while. Previously when working on a Rust project, I chose codespan-reporting because I preferred its formatting (formatting of ariadne is also beautiful, but a bit too fancy for me), given their interfaces are pretty similar. Is it possible to extend this library to have an alternative codespan-reporting-style formatting for the diagnostic messages? Or is there some design space to expose an API for custom report formatting?

FYI, here is a comparison of the said two styles (BTW it is amusing to see a more Haskell-like syntax for illustration in codespan-reporting the Rust library but a more Rust-like style here):

Name Illustration
(current) ariadne-style
codespan-reporting-style codespan_reporting_preview

If this is deemed non-trivial, I am willing to work on this, in that case would you please provide some guidance?

How to deal with big content

Hello,

I'm currently testing your library and the diagnostics look really good!

I am just having one issue and it's unclear to me how to solve it from the docs. If I have content that spans over multiple lines and it breaks the layout. Is it possible to use say Docs instead of Text/String to create a Report? Or what would be the way of doing something like this? Say showing actual/expected types in a type error, or having a type that is long ( record type or function types for example ).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.