Skip to content

Commit 438b362

Browse files
authored
feat: improve error struct (#42)
1 parent 3c9eab0 commit 438b362

13 files changed

+250
-98
lines changed

.github/workflows/ci.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ jobs:
3030
run: |
3131
cargo test --features serde
3232
cargo test --features preserve_order
33-
cargo test --verbose --all-features
33+
cargo test --all-features
3434
- name: Test release
3535
if: matrix.config.kind == 'test_release'
36-
run: cargo test --release --verbose --all-features
36+
run: cargo test --release --all-features
3737

3838
# CARGO PUBLISH
3939
- name: Cargo login
@@ -48,14 +48,14 @@ jobs:
4848
name: Benchmarks
4949
runs-on: ubuntu-latest
5050
steps:
51-
- uses: actions/checkout@v2
51+
- uses: actions/checkout@v4
5252
- name: Install latest nightly
5353
uses: actions-rs/toolchain@v1
5454
with:
5555
toolchain: nightly
5656
override: true
5757
- name: Cache cargo
58-
uses: actions/cache@v2
58+
uses: actions/cache@v4
5959
with:
6060
path: |
6161
~/.cargo/registry

.github/workflows/release.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ jobs:
2020

2121
steps:
2222
- name: Clone repository
23-
uses: actions/checkout@v3
23+
uses: actions/checkout@v4
2424
with:
2525
token: ${{ secrets.GH_DPRINTBOT_PAT }}
2626

27-
- uses: denoland/setup-deno@v1
27+
- uses: denoland/setup-deno@v2
2828
- uses: dsherret/rust-toolchain-file@v1
2929

3030
- name: Bump version and tag

Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ all-features = true
1313
[dependencies]
1414
indexmap = { version = "2.2.6", optional = true }
1515
serde_json = { version = "1.0", optional = true }
16+
unicode-width = { version = "0.2.0", optional = true }
1617

1718
[features]
1819
cst = []
1920
preserve_order = ["indexmap", "serde_json/preserve_order"]
2021
serde = ["serde_json"]
22+
error_unicode_width = ["unicode-width"]
2123

2224
[dev-dependencies]
2325
pretty_assertions = "1.0.0"

LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) 2020-2021 David Sherret
3+
Copyright (c) 2020 David Sherret
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,7 @@ let json_value = parse_to_value(text, &ParseOptions {
9292
allow_trailing_commas: false,
9393
})?;
9494
```
95+
96+
## Error column number with unicode-width
97+
98+
To to get more accurate display column numbers in error messages, enable the `error_unicode_width` cargo feature, which will pull in and use the [unicode-width](https://crates.io/crates/unicode-width) dependency internally. Otherwise it will use the character count, which isn't as accurate of a number, but will probably be good enough in most cases.

dprint.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
],
1414
"plugins": [
1515
"https://plugins.dprint.dev/markdown-0.17.8.wasm",
16-
"https://plugins.dprint.dev/exec-0.5.0.json@8d9972eee71fa1590e04873540421f3eda7674d0f1aae3d7c788615e7b7413d0"
16+
"https://plugins.dprint.dev/exec-0.5.0.json@8d9972eee71fa1590e04873540421f3eda7674d0f1aae3d7c788615e7b7413d0",
17+
"https://plugins.dprint.dev/json-0.19.3.wasm"
1718
]
1819
}

rust-toolchain.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[toolchain]
2-
channel = "1.81.0"
2+
channel = "1.82.0"
33
components = ["clippy", "rustfmt"]

src/errors.rs

+153-26
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,179 @@
1-
use std::error::Error;
21
use std::fmt;
32

3+
use crate::ParseStringErrorKind;
4+
45
use super::common::Range;
56

6-
/// Error that could occur while parsing or tokenizing.
7-
#[derive(Debug, PartialEq)]
8-
pub struct ParseError {
9-
/// Start and end position of the error.
10-
pub range: Range,
11-
/// Error message.
12-
pub message: String,
13-
/// Message with the range text.
14-
display_message: String,
7+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8+
pub enum ParseErrorKind {
9+
CommentsNotAllowed,
10+
ExpectedColonAfterObjectKey,
11+
ExpectedObjectValue,
12+
ExpectedDigit,
13+
ExpectedDigitFollowingNegativeSign,
14+
ExpectedPlusMinusOrDigitInNumberLiteral,
15+
ExpectedStringObjectProperty,
16+
MultipleRootJsonValues,
17+
String(ParseStringErrorKind),
18+
TrailingCommasNotAllowed,
19+
UnexpectedCloseBrace,
20+
UnexpectedCloseBracket,
21+
UnexpectedColon,
22+
UnexpectedComma,
23+
UnexpectedToken,
24+
UnexpectedTokenInObject,
25+
UnexpectedWord,
26+
UnterminatedArray,
27+
UnterminatedCommentBlock,
28+
UnterminatedObject,
1529
}
1630

17-
impl ParseError {
18-
pub(crate) fn new(range: Range, message: &str, file_text: &str) -> ParseError {
19-
let display_message = get_message_with_range(range, message, file_text);
20-
ParseError {
21-
message: message.to_string(),
22-
range,
23-
display_message,
31+
impl std::fmt::Display for ParseErrorKind {
32+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33+
use ParseErrorKind::*;
34+
match self {
35+
CommentsNotAllowed => {
36+
write!(f, "Comments are not allowed")
37+
}
38+
ExpectedColonAfterObjectKey => {
39+
write!(f, "Expected colon after the string or word in object property")
40+
}
41+
ExpectedDigit => {
42+
write!(f, "Expected digit")
43+
}
44+
ExpectedDigitFollowingNegativeSign => {
45+
write!(f, "Expected digit following negative sign")
46+
}
47+
ExpectedPlusMinusOrDigitInNumberLiteral => {
48+
write!(f, "Expected plus, minus, or digit in number literal")
49+
}
50+
ExpectedObjectValue => {
51+
write!(f, "Expected value after colon in object property")
52+
}
53+
ExpectedStringObjectProperty => {
54+
write!(f, "Expected string for object property")
55+
}
56+
MultipleRootJsonValues => {
57+
write!(f, "Text cannot contain more than one JSON value")
58+
}
59+
String(kind) => kind.fmt(f),
60+
TrailingCommasNotAllowed => {
61+
write!(f, "Trailing commas are not allowed")
62+
}
63+
UnexpectedCloseBrace => {
64+
write!(f, "Unexpected close brace")
65+
}
66+
UnexpectedCloseBracket => {
67+
write!(f, "Unexpected close bracket")
68+
}
69+
UnexpectedColon => {
70+
write!(f, "Unexpected colon")
71+
}
72+
UnexpectedComma => {
73+
write!(f, "Unexpected comma")
74+
}
75+
UnexpectedWord => {
76+
write!(f, "Unexpected word")
77+
}
78+
UnexpectedToken => {
79+
write!(f, "Unexpected token")
80+
}
81+
UnexpectedTokenInObject => {
82+
write!(f, "Unexpected token in object")
83+
}
84+
UnterminatedArray => {
85+
write!(f, "Unterminated array")
86+
}
87+
UnterminatedCommentBlock => {
88+
write!(f, "Unterminated comment block")
89+
}
90+
UnterminatedObject => {
91+
write!(f, "Unterminated object")
92+
}
2493
}
2594
}
2695
}
2796

28-
impl fmt::Display for ParseError {
29-
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
30-
write!(f, "{}", self.display_message)
97+
#[derive(Debug, Clone, PartialEq)]
98+
struct ParseErrorInner {
99+
range: Range,
100+
line_display: usize,
101+
column_display: usize,
102+
kind: ParseErrorKind,
103+
}
104+
105+
/// Error that could occur while parsing or tokenizing.
106+
#[derive(Debug, Clone, PartialEq)]
107+
pub struct ParseError(Box<ParseErrorInner>);
108+
109+
impl std::error::Error for ParseError {}
110+
111+
impl ParseError {
112+
pub(crate) fn new(range: Range, kind: ParseErrorKind, file_text: &str) -> ParseError {
113+
let (line_display, column_display) = get_line_and_column_display(range, file_text);
114+
ParseError(Box::new(ParseErrorInner {
115+
range,
116+
line_display,
117+
column_display,
118+
kind,
119+
}))
120+
}
121+
122+
/// Start and end position of the error.
123+
pub fn range(&self) -> Range {
124+
self.0.range
125+
}
126+
127+
/// 1-indexed line number the error occurred on.
128+
pub fn line_display(&self) -> usize {
129+
self.0.line_display
130+
}
131+
132+
/// 1-indexed column number the error occurred on.
133+
///
134+
/// Note: Use the `error_unicode_width` feature to get the correct column
135+
/// number for Unicode characters on the line, otherwise this is just the
136+
/// number of characters by default.
137+
pub fn column_display(&self) -> usize {
138+
self.0.column_display
139+
}
140+
141+
/// Error message.
142+
pub fn kind(&self) -> &ParseErrorKind {
143+
&self.0.kind
31144
}
32145
}
33146

34-
impl Error for ParseError {
35-
fn description(&self) -> &str {
36-
&self.display_message
147+
impl fmt::Display for ParseError {
148+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
149+
let inner = &*self.0;
150+
write!(
151+
f,
152+
"{} on line {} column {}",
153+
inner.kind, inner.line_display, inner.column_display
154+
)
37155
}
38156
}
39157

40-
fn get_message_with_range(range: Range, message: &str, file_text: &str) -> String {
158+
fn get_line_and_column_display(range: Range, file_text: &str) -> (usize, usize) {
41159
let mut line_index = 0;
42160
let mut column_index = 0;
43161
for c in file_text[..range.start].chars() {
44162
if c == '\n' {
45163
line_index += 1;
46164
column_index = 0;
47165
} else {
48-
column_index += 1;
166+
#[cfg(feature = "error_unicode_width")]
167+
{
168+
if let Some(width) = unicode_width::UnicodeWidthChar::width_cjk(c) {
169+
column_index += width;
170+
}
171+
}
172+
#[cfg(not(feature = "error_unicode_width"))]
173+
{
174+
column_index += 1;
175+
}
49176
}
50177
}
51-
format!("{} on line {} column {}.", message, line_index + 1, column_index + 1,)
178+
(line_index + 1, column_index + 1)
52179
}

0 commit comments

Comments
 (0)