1
1
mirror of https://github.com/tweag/nickel.git synced 2024-09-20 16:08:14 +03:00
nickel/notes/error-reporting-lib-choice.md
2020-07-16 17:27:42 +02:00

13 KiB

PRs #96, #97 and #98 aim to improve source mapping (conversion of offsets to line and column in a source file, as done e.g. in the rust compiler), and improve error reporting, as well as to decouple one from the other. However it requires some effort to write and maintain code for consistent, informative and pretty error messages and resolving positions from multiple sources: as @aspiwack suggested, it is worth to look if there isn't already a library that can achieve this for us.

Libraries review

We initially selected 4 candidates:

Language-reporting started as a fork of codespan, but its features have been backported and it seems to be now deprecated, so we exclude it from the test. All of them are based on or inspired by the rust compiler error reporting infrastructure and hence have similar models.

Criteria

  • Sustainability: will the library be maintained in the long term ?
  • Features
  • Usability
  • Modularity and ease of integration in Nickel
  • Configurability
  • Aesthetics

Sustainability

Libraries →
Stats ↴
Codespan Codemap Annotate-snippets
Downloads 290K (~0,75K/day) 27K (45/day) 574K (2,5K/day)
Reverse deps 28 7 12
Used by cargo-deny
CXX
gleam
gluon
nushell
starlark (rust impl) dhall (rust impl.)
rustfmt
rust
jrsonnet
Contributors 19 4 10
Last updated 19 days ago 1 year ago 11 days ago
Overall sustainability #00ff00High #ffa500Medium #00ff00 High

Features

Libraries →
Features ↴
Codespan Codemap Annotate-snippets
Multiple source files management #00ff00Yes #00ff00 Yes #ff0000 No
Source mapping #00ff00 Yes #00ff00 Yes #ff0000 No
Snippet annotation #00ff00 Yes #00ff00 Yes #00ff00 Yes
Colors #00ff00 Yes #00ff00 Yes #00ff00 Yes

Modularity

Libraries Codespan Codemap Annotate-snippets
Modularity #00ff00Good:
codespan-reporting defines a file trait for source mapping that is independent from codespan
#ffa500Partial:
codemap-diagnostic is built on the types of codemap and depends on it
N/A

Usability, configurability and aesthetics

On these aspects, the three libraries are relatively similar: both on source mapping, where a dictionary of source files allows to manage multiple sources and map absolute offsets to line and columns, and on error reporting, where the user constructs a structured representation of an annotation with labels, spans, error code, severity, and so on. Each one gives reasonable freedom in the configuration of the output format, and produces messages that resemble to the ones produced by the rust compiler.

Conclusion

Annotate-snippets is a well established library, backed by the rust compiler itself, but it does not handle source mapping, which is also needed in Nickel. Codespan and codemap are quite similar, with respect to features as well as usage, but codespan seems more active, more used and more modular: accordingly, we recommend the usage of codespan.

Annex 1: code examples

Codespan

use codespan_reporting::diagnostic::{Diagnostic, Label};
use codespan_reporting::files::SimpleFiles;
use codespan_reporting::term::termcolor::{ColorChoice, StandardStream};

// `files::SimpleFile` and `files::SimpleFiles` help you get up and running with
// `codespan-reporting` quickly! More complicated use cases can be supported
// by creating custom implementations of the `files::Files` trait.

let mut files = SimpleFiles::new();

let file_id = files.add(
    "FizzBuzz.fun",
    unindent::unindent(
        r#"
            module FizzBuzz where

            fizz₁ : Nat → String
            fizz₁ num = case (mod num 5) (mod num 3) of
                0 0 => "FizzBuzz"
                0 _ => "Fizz"
                _ 0 => "Buzz"
                _ _ => num

            fizz₂ : Nat → String
            fizz₂ num =
                case (mod num 5) (mod num 3) of
                    0 0 => "FizzBuzz"
                    0 _ => "Fizz"
                    _ 0 => "Buzz"
                    _ _ => num
        "#,
    ),
);

// We normally recommend creating a custom diagnostic data type for your
// application, and then converting that to `codespan-reporting`'s diagnostic
// type, but for the sake of this example we construct it directly.

let diagnostic = Diagnostic::error()
    .with_message("`case` clauses have incompatible types")
    .with_code("E0308")
    .with_labels(vec![
        Label::primary(file_id, 328..331).with_message("expected `String`, found `Nat`"),
        Label::secondary(file_id, 211..331).with_message("`case` clauses have incompatible types"),
        Label::secondary(file_id, 258..268).with_message("this is found to be of type `String`"),
        Label::secondary(file_id, 284..290).with_message("this is found to be of type `String`"),
        Label::secondary(file_id, 306..312).with_message("this is found to be of type `String`"),
        Label::secondary(file_id, 186..192).with_message("expected type `String` found here"),
    ])
    .with_notes(vec![unindent::unindent(
        "
            expected type `String`
                found type `Nat`
        ",
    )]);

// We now set up the writer and configuration, and then finally render the
// diagnostic to standard error.

let writer = StandardStream::stderr(ColorChoice::Always);
let config = codespan_reporting::term::Config::default();

term::emit(&mut writer.lock(), &config, &files, &diagnostic)?;

Codemap

extern crate codemap_diagnostic;
extern crate codemap;
use codemap::{ CodeMap };
use codemap_diagnostic::{ Level, SpanLabel, SpanStyle, Diagnostic, ColorConfig, Emitter };


fn main() {
    let file = r#"
pub fn look_up_pos(&self, pos: Pos) -> Loc {
    let file = self.find_file(pos);
    let position = file.find_line_col(pos);
    Loc { file, position }
}
"#;

    let mut codemap = CodeMap::new();
    let file_span = codemap.add_file("test.rs".to_owned(), file.to_owned()).span;
    let fn_span = file_span.subspan(8, 19);
    let ret_span = file_span.subspan(40, 43);
    let var_span = file_span.subspan(54, 58);

    let sl = SpanLabel { span: fn_span, style: SpanStyle::Primary, label:Some("function name".to_owned()) };
    let sl2 = SpanLabel { span: ret_span, style: SpanStyle::Primary, label:Some("returns".to_owned()) };
    let d1 = Diagnostic { level:Level::Error, message:"Test error".to_owned(), code:Some("C000".to_owned()), spans: vec![sl, sl2] };

    let sl3 = SpanLabel { span: var_span, style: SpanStyle::Primary, label:Some("variable".to_owned()) };
    let d2 = Diagnostic { level:Level::Warning, message:"Test warning".to_owned(), code:Some("W000".to_owned()), spans: vec![sl3] };

    let d3 = Diagnostic { level: Level::Help, message:"Help message".to_owned(), code: None, spans: vec![] };

    let mut emitter = Emitter::stderr(ColorConfig::Auto, Some(&codemap));
    emitter.emit(&[d1, d2, d3]);
}

Annotate-snippets

use annotate_snippets::{
    display_list::{DisplayList, FormatOptions},
    snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation},
};

fn main() {
    let snippet = Snippet {
        title: Some(Annotation {
            label: Some("expected type, found `22`"),
            id: None,
            annotation_type: AnnotationType::Error,
        }),
        footer: vec![],
        slices: vec![Slice {
            source: r#"                annotations: vec![SourceAnnotation {
                label: "expected struct `annotate_snippets::snippet::Slice`, found reference"
                    ,
                range: <22, 25>,"#,
            line_start: 26,
            origin: Some("examples/footer.rs"),
            fold: true,
            annotations: vec![
                SourceAnnotation {
                    label: "",
                    annotation_type: AnnotationType::Error,
                    range: (205, 207),
                },
                SourceAnnotation {
                    label: "while parsing this struct",
                    annotation_type: AnnotationType::Info,
                    range: (34, 50),
                },
            ],
        }],
        opt: FormatOptions {
            color: true,
            ..Default::default()
        },
    };

    let dl = DisplayList::from(snippet);
    println!("{}", dl);
}

Annex 2: Look & feel

Codespan

Example preview

Codemap

screenshot

Annotate-snippets

error[E0308]: mismatched types
  --> src/format.rs:52:1
   |
51 |   ) -> Option<String> {
   |        -------------- expected `Option<String>` because of return type
52 | /     for ann in annotations {
53 | |         match (ann.range.0, ann.range.1) {
54 | |             (None, None) => continue,
55 | |             (Some(start), Some(end)) if start > end_index => continue,
...  |
71 | |         }
72 | |     }
   | |_____^ expected enum `std::option::Option`, found ()