Skip to content

Commit 30e35b9

Browse files
committed
parser.c: include line and column in error messages
1 parent 832b5b1 commit 30e35b9

File tree

5 files changed

+51
-20
lines changed

5 files changed

+51
-20
lines changed

CHANGES.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
### Unreleased
44

5+
* Include line and column in parser errors. Both in the message and as exception attributes.
56
* Handle non-string hash keys with broken `to_s` implementations.
67
* `JSON.generate` now uses SSE2 (x86) or NEON (arm64) instructions when available to escape strings.
78

ext/json/ext/parser/parser.c

+33-9
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,23 @@ static void raise_parse_error(const char *format, JSON_ParserState *state)
395395
{
396396
unsigned char buffer[PARSE_ERROR_FRAGMENT_LEN + 1];
397397

398+
const char *cursor = state->cursor;
399+
long column = 0;
400+
long line = 1;
401+
402+
while (cursor >= state->start) {
403+
if (*cursor-- == '\n') {
404+
break;
405+
}
406+
column++;
407+
}
408+
409+
while (cursor >= state->start) {
410+
if (*cursor-- == '\n') {
411+
line++;
412+
}
413+
}
414+
398415
const char *ptr = state->cursor;
399416
size_t len = ptr ? strnlen(ptr, PARSE_ERROR_FRAGMENT_LEN) : 0;
400417

@@ -413,7 +430,14 @@ static void raise_parse_error(const char *format, JSON_ParserState *state)
413430
ptr = (const char *)buffer;
414431
}
415432

416-
rb_enc_raise(enc_utf8, rb_path2class("JSON::ParserError"), format, ptr);
433+
VALUE msg = rb_sprintf(format, ptr);
434+
VALUE message = rb_enc_sprintf(enc_utf8, "%s at line %ld column %ld", RSTRING_PTR(msg), line, column);
435+
RB_GC_GUARD(msg);
436+
437+
VALUE exc = rb_exc_new_str(rb_path2class("JSON::ParserError"), message);
438+
rb_ivar_set(exc, rb_intern("@line"), LONG2NUM(line));
439+
rb_ivar_set(exc, rb_intern("@column"), LONG2NUM(column));
440+
rb_exc_raise(exc);
417441
}
418442

419443
#ifdef RBIMPL_ATTR_NORETURN
@@ -508,11 +532,11 @@ json_eat_comments(JSON_ParserState *state)
508532
break;
509533
}
510534
default:
511-
raise_parse_error("unexpected token at '%s'", state);
535+
raise_parse_error("unexpected token '%s'", state);
512536
break;
513537
}
514538
} else {
515-
raise_parse_error("unexpected token at '%s'", state);
539+
raise_parse_error("unexpected token '%s'", state);
516540
}
517541
}
518542

@@ -870,15 +894,15 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config)
870894
return json_push_value(state, config, Qnil);
871895
}
872896

873-
raise_parse_error("unexpected token at '%s'", state);
897+
raise_parse_error("unexpected token '%s'", state);
874898
break;
875899
case 't':
876900
if ((state->end - state->cursor >= 4) && (memcmp(state->cursor, "true", 4) == 0)) {
877901
state->cursor += 4;
878902
return json_push_value(state, config, Qtrue);
879903
}
880904

881-
raise_parse_error("unexpected token at '%s'", state);
905+
raise_parse_error("unexpected token '%s'", state);
882906
break;
883907
case 'f':
884908
// Note: memcmp with a small power of two compile to an integer comparison
@@ -887,7 +911,7 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config)
887911
return json_push_value(state, config, Qfalse);
888912
}
889913

890-
raise_parse_error("unexpected token at '%s'", state);
914+
raise_parse_error("unexpected token '%s'", state);
891915
break;
892916
case 'N':
893917
// Note: memcmp with a small power of two compile to an integer comparison
@@ -896,15 +920,15 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config)
896920
return json_push_value(state, config, CNaN);
897921
}
898922

899-
raise_parse_error("unexpected token at '%s'", state);
923+
raise_parse_error("unexpected token '%s'", state);
900924
break;
901925
case 'I':
902926
if (config->allow_nan && (state->end - state->cursor >= 8) && (memcmp(state->cursor, "Infinity", 8) == 0)) {
903927
state->cursor += 8;
904928
return json_push_value(state, config, CInfinity);
905929
}
906930

907-
raise_parse_error("unexpected token at '%s'", state);
931+
raise_parse_error("unexpected token '%s'", state);
908932
break;
909933
case '-':
910934
// Note: memcmp with a small power of two compile to an integer comparison
@@ -913,7 +937,7 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config)
913937
state->cursor += 9;
914938
return json_push_value(state, config, CMinusInfinity);
915939
} else {
916-
raise_parse_error("unexpected token at '%s'", state);
940+
raise_parse_error("unexpected token '%s'", state);
917941
}
918942
}
919943
// Fallthrough

lib/json/common.rb

+3-1
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,9 @@ def self.create_id
230230
class JSONError < StandardError; end
231231

232232
# This exception is raised if a parser error occurs.
233-
class ParserError < JSONError; end
233+
class ParserError < JSONError
234+
attr_reader :line, :column
235+
end
234236

235237
# This exception is raised if the nesting of parsed data structures is too
236238
# deep.

test/json/json_ext_parser_test.rb

+8-4
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,19 @@ def test_allocate
1515

1616
def test_error_messages
1717
ex = assert_raise(ParserError) { parse('Infinity') }
18-
assert_equal "unexpected token at 'Infinity'", ex.message
18+
unless RUBY_PLATFORM =~ /java/
19+
assert_equal "unexpected token 'Infinity' at line 1 column 1", ex.message
20+
end
1921

22+
ex = assert_raise(ParserError) { parse('-Infinity') }
2023
unless RUBY_PLATFORM =~ /java/
21-
ex = assert_raise(ParserError) { parse('-Infinity') }
22-
assert_equal "unexpected token at '-Infinity'", ex.message
24+
assert_equal "unexpected token '-Infinity' at line 1 column 1", ex.message
2325
end
2426

2527
ex = assert_raise(ParserError) { parse('NaN') }
26-
assert_equal "unexpected token at 'NaN'", ex.message
28+
unless RUBY_PLATFORM =~ /java/
29+
assert_equal "unexpected token 'NaN' at line 1 column 1", ex.message
30+
end
2731
end
2832

2933
if GC.respond_to?(:stress=)

test/json/json_parser_test.rb

+6-6
Original file line numberDiff line numberDiff line change
@@ -638,32 +638,32 @@ def test_parse_error_message_length
638638
error = assert_raise(JSON::ParserError) do
639639
JSON.parse('{"foo": ' + ('A' * 500) + '}')
640640
end
641-
assert_operator 60, :>, error.message.bytesize
641+
assert_operator 80, :>, error.message.bytesize
642642
end
643643

644644
def test_parse_error_incomplete_hash
645645
error = assert_raise(JSON::ParserError) do
646646
JSON.parse('{"input":{"firstName":"Bob","lastName":"Mob","email":"[email protected]"}')
647647
end
648648
if RUBY_ENGINE == "ruby"
649-
assert_equal %(expected ',' or '}' after object value, got: ''), error.message
649+
assert_equal %(expected ',' or '}' after object value, got: '' at line 1 column 72), error.message
650650
end
651651
end
652652

653653
def test_parse_error_snippet
654654
omit "C ext only test" unless RUBY_ENGINE == "ruby"
655655

656656
error = assert_raise(JSON::ParserError) { JSON.parse("あああああああああああああああああああああああ") }
657-
assert_equal "unexpected character: 'ああああああああああ'", error.message
657+
assert_equal "unexpected character: 'ああああああああああ' at line 1 column 1", error.message
658658

659659
error = assert_raise(JSON::ParserError) { JSON.parse("aあああああああああああああああああああああああ") }
660-
assert_equal "unexpected character: 'aああああああああああ'", error.message
660+
assert_equal "unexpected character: 'aああああああああああ' at line 1 column 1", error.message
661661

662662
error = assert_raise(JSON::ParserError) { JSON.parse("abあああああああああああああああああああああああ") }
663-
assert_equal "unexpected character: 'abあああああああああ'", error.message
663+
assert_equal "unexpected character: 'abあああああああああ' at line 1 column 1", error.message
664664

665665
error = assert_raise(JSON::ParserError) { JSON.parse("abcあああああああああああああああああああああああ") }
666-
assert_equal "unexpected character: 'abcあああああああああ'", error.message
666+
assert_equal "unexpected character: 'abcあああああああああ' at line 1 column 1", error.message
667667
end
668668

669669
def test_parse_leading_slash

0 commit comments

Comments
 (0)