From 4d6031cf078265f84f89402a273fdb92e729bddc Mon Sep 17 00:00:00 2001 From: FFMG Date: Fri, 30 Oct 2026 12:44:54 +0200 Subject: [PATCH] Added line + col count on error --- CHANGELOG.md | 12 + README.md | 28 +- src/TinyJSON.cpp | 294 +++++++--- src/TinyJSON.h | 24 +- tests/CMakeLists.txt | 1 + tests/testdefaultget.cpp | 2 +- tests/testduplicatekeys.cpp | 4 +- tests/testtinyjsonexceptions.cpp | 12 +- tests/testtinyjsonlinecolumn.cpp | 979 +++++++++++++++++++++++++++++++ tests/testtinyjsonversion.cpp | 4 +- tests/testtinyjsonwarnings.cpp | 2 +- 11 files changed, 1269 insertions(+), 93 deletions(-) create mode 100644 tests/testtinyjsonlinecolumn.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 82e761c..4024354 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.9] - 2026-10-29 + +### Added +- Line and column numbers for all the parse messages, (see https://github.com/FFMG/tinyjson/issues/51). The numbers are 1-based, `'\n'`, `'\r\n'` and a lone `'\r'` each count as a single new line, and `-1, -1` is given when there is no position (e.g. null source, file could not be opened, key not found). +- `TJParseException::line()` and `TJParseException::column()` give the position of the parse error. +- The duplicate key `trace` message now gives the position of the duplicate key. + +### Changed +- Incremented version to 0.2.9. +- **Breaking Change**: The `parse_options::callback_function` signature is now `std::function`, existing callbacks need the two extra `int` parameters. +- The "We found a value but we expected a comma." error now points to the start of the unexpected value. + ## [0.2.8] - 2026-10-15 ### Added diff --git a/README.md b/README.md index 1b5caf0..235397c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![CI](https://github.com/FFMG/TinyJSON/actions/workflows/c-cpp.yml/badge.svg)](https://github.com/FFMG/TinyJSON/actions/workflows/c-cpp.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -[![Version](https://img.shields.io/badge/version-0.2.8-blue.svg)](src/TinyJSON.h) +[![Version](https://img.shields.io/badge/version-0.2.9-blue.svg)](src/TinyJSON.h) A lightweight and lightning-fast C++ JSON & JSON5 parser designed for high performance and minimal footprint. @@ -131,8 +131,8 @@ The version is set in the `TinyJSON.h` file. ```cpp static const short TJ_VERSION_MAJOR = 0; static const short TJ_VERSION_MINOR = 2; -static const short TJ_VERSION_PATCH = 8; -static const char TJ_VERSION_STRING[] = "0.2.8"; +static const short TJ_VERSION_PATCH = 9; +static const char TJ_VERSION_STRING[] = "0.2.9"; ``` ### Simple Value Access @@ -167,8 +167,12 @@ if (tj) { * rfc7159 * rfc8259 * json5_1_0_0 -* Callback: (`callback_function:std::function`) Callback function called where there is an error, warning etc. +* Callback: (`callback_function:std::function`) Callback function called where there is an error, warning etc. NB: The Callback function is called even if Throw is false. + * `line` and `column` are 1-based and point to where in the source the message was raised, (the first character is line 1, column 1). + * `'\n'`, `'\r\n'` and a lone `'\r'` each count as a single new line, a new line resets the column to 1. + * The column counts `TJCHAR` code units, so a multi-byte utf-8 character counts as more than one column. A utf-8 BOM is not counted. + * If there is no position, (the source is null, the file could not be opened, a key was not found, a value could not be converted etc.), both `line` and `column` are `-1`. * If a getter method (`try_get_string`, `get_*(..)`, or `get<>(..)`) is called on a key that does not exist in a `TJValueObject`, a warning message indicating the missing key (e.g. `"The key 'missing' was not found!"`) is passed to the callback function. * trace * debug @@ -183,18 +187,21 @@ For example ... TinyJSON::parse_options options = {}; options.throw_exception = true; options.max_depth = 10; -options.Callback = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message) { +options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message, int line, int column) { // something, something ... + // line == 2, column == 3 for the example below. }; try { - auto blah = TinyJSON::TJ::parse("[0123]", options) + auto blah = TinyJSON::TJ::parse("[\n 0123]", options) ... } catch(TinyJSON::TJParseException ex) { - ex.what(); // Numbers cannot have leading zeros + ex.what(); // Numbers cannot have leading zeros + ex.line(); // 2 + ex.column(); // 3 } ``` @@ -237,6 +244,7 @@ catch(TinyJSON::TJParseException ex) #### Parsing Exception The parsing exception is `TinyJSON::TJParseException` and can be made optional in the `TinyJSON::parse_options` flag. +The exception also has the 1-based `line()` and `column()` of where the error happened, (both are `-1` if there is no position). ```cpp TinyJSON::parse_options options = {}; @@ -248,7 +256,9 @@ The parsing exception is `TinyJSON::TJParseException` and can be made optional i } catch(TinyJSON::TJParseException ex) { - ex.what(); // Numbers cannot have leading zeros + ex.what(); // Numbers cannot have leading zeros + ex.line(); // 1 + ex.column(); // 2 } ``` @@ -775,7 +785,7 @@ include(FetchContent) FetchContent_Declare( TinyJSON GIT_REPOSITORY https://github.com/FFMG/TinyJSON.git - GIT_TAG v0.2.8 # or main + GIT_TAG v0.2.9 # or main ) FetchContent_MakeAvailable(TinyJSON) target_link_libraries(your_target PUBLIC TinyJSON) diff --git a/src/TinyJSON.cpp b/src/TinyJSON.cpp index e293bfa..118f3e5 100644 --- a/src/TinyJSON.cpp +++ b/src/TinyJSON.cpp @@ -145,6 +145,59 @@ namespace TinyJSON return len; } + /// + /// A 1-based line and column position in a source, -1 if unknown. + /// + struct source_position + { + int _line; + int _column; + }; + + /// + /// Calculate the 1-based line and column of a position in the source. + /// '\n', '\r\n' and a lone '\r' all count as a single new line. + /// If either the start of the source or the position is unknown we return -1, -1. + /// + /// The start of the source. + /// The position we want the line/column of. + /// + static source_position calculate_source_position(const TJCHAR* source_begin, const TJCHAR* position) + { + if (nullptr == source_begin || nullptr == position || position < source_begin) + { + return { -1, -1 }; + } + + source_position result = { 1, 1 }; + for (const TJCHAR* p = source_begin; p < position; ++p) + { + if (*p == TJ_NULL_TERMINATOR) + { + break; + } + if (*p == TJ_ESCAPE_CARRIAGE_RETURN) + { + if (p + 1 < position && *(p + 1) == TJ_ESCAPE_LINE_FEED) + { + // '\r\n' is a single new line, the '\n' will move us to the next line. + continue; + } + ++result._line; + result._column = 1; + continue; + } + if (*p == TJ_ESCAPE_LINE_FEED) + { + ++result._line; + result._column = 1; + continue; + } + ++result._column; + } + return result; + } + static void report_file_open_error(const TJCHAR* file_path, int err_code, const parse_options& options) { const TJCHAR* prefix = nullptr; @@ -193,7 +246,7 @@ namespace TinyJSON } msg[total_len] = TJ_NULL_TERMINATOR; - options.callback_function(parse_options::message_type::error, msg); + options.callback_function(parse_options::message_type::error, msg, -1, -1); delete[] msg; } @@ -247,7 +300,10 @@ namespace TinyJSON ParseResult(const parse_options& parse_options) : _exception_message(nullptr), _options(parse_options), - _depth(0) + _depth(0), + _source_begin(nullptr), + _line(-1), + _column(-1) { } @@ -275,10 +331,29 @@ namespace TinyJSON } /// - /// Assign a parse error message. + /// Set where the source starts, all the line/column positions are relative to this. + /// + /// + void set_source_begin(const TJCHAR* source_begin) + { + _source_begin = source_begin; + } + + /// + /// Assign a parse error message without a known position. /// /// void assign_exception_message(const char* parse_exception_message) + { + assign_exception_message(nullptr, parse_exception_message); + } + + /// + /// Assign a parse error message at a given position in the source. + /// + /// Where in the source the error happened. + /// + void assign_exception_message(const TJCHAR* position, const char* parse_exception_message) { free_exception_message(); if (parse_exception_message != nullptr) @@ -287,6 +362,9 @@ namespace TinyJSON _exception_message = new char[length + 1]; std::strcpy(_exception_message, parse_exception_message); } + const auto position_in_source = calculate_source_position(_source_begin, position); + _line = position_in_source._line; + _column = position_in_source._column; } void assign_exception_message_and_throw(const char* parse_exception_message) @@ -295,18 +373,36 @@ namespace TinyJSON throw_if_exception(); } + void assign_exception_message_and_throw(const TJCHAR* position, const char* parse_exception_message) + { + assign_exception_message(position, parse_exception_message); + throw_if_exception(); + } + void throw_if_exception() { if (!has_exception_message()) { return; } - _options.callback_function(parse_options::message_type::fatal, _exception_message); + _options.callback_function(parse_options::message_type::fatal, _exception_message, _line, _column); if (!_options.throw_exception) { return; } - throw TJParseException(_exception_message); + throw TJParseException(_exception_message, _line, _column); + } + + /// + /// Raise a non fatal message at a given position in the source. + /// + /// + /// Where in the source the message was raised. + /// + void raise(parse_options::message_type message_type, const TJCHAR* position, const TJCHAR* message) const + { + const auto position_in_source = calculate_source_position(_source_begin, position); + _options.callback_function(message_type, message, position_in_source._line, position_in_source._column); } bool has_exception_message() const @@ -332,6 +428,9 @@ namespace TinyJSON char* _exception_message; parse_options _options; unsigned int _depth; + const TJCHAR* _source_begin; + int _line; + int _column; }; /////////////////////////////////////// @@ -413,14 +512,18 @@ namespace TinyJSON /////////////////////////////////////// /// Parsing Exception - TJParseException::TJParseException(const char* message) : - _message(nullptr) + TJParseException::TJParseException(const char* message, int line, int column) : + _message(nullptr), + _line(line), + _column(column) { assign_message(message); } TJParseException::TJParseException(const TJParseException& exception) - : _message(nullptr) + : _message(nullptr), + _line(-1), + _column(-1) { *this = exception; } @@ -435,6 +538,8 @@ namespace TinyJSON if (this != &exception) { assign_message(exception._message); + _line = exception._line; + _column = exception._column; } return *this; } @@ -444,6 +549,16 @@ namespace TinyJSON return _message == nullptr ? "Unknown" : _message; } + int TJParseException::line() const noexcept + { + return _line; + } + + int TJParseException::column() const noexcept + { + return _column; + } + void TJParseException::assign_message(const char* message) { free_message(); @@ -783,6 +898,19 @@ namespace TinyJSON /// we will replace the old value with the new value. /// void set(TJMember* value, const parse_options& options) + { + if (set_or_replace(value)) + { + options.callback_function(parse_options::message_type::trace, TJCHARPREFIX("Duplicate key found, overwriting."), -1, -1); + } + } + + /// + /// set a value in our dictionary, if the value exists, (by name) + /// we will replace the old value with the new value. + /// + /// True if an existing value was replaced. + bool set_or_replace(TJMember* value) { if (nullptr == _values) { @@ -797,7 +925,7 @@ namespace TinyJSON grow(); } _values[_number_of_items++] = value; - return; + return false; } // check if the key already exists, if it does simply update the value. @@ -805,9 +933,8 @@ namespace TinyJSON auto binary_search_result_cs = binary_search(key, true); if (true == binary_search_result_cs._was_found) { - options.callback_function(parse_options::message_type::trace, TJCHARPREFIX("Duplicate key found, overwriting.")); replace_dictionaries_data(value, binary_search_result_cs._dictionary_index); - return; + return true; } // check if the value can be added. @@ -858,6 +985,7 @@ namespace TinyJSON _values_dictionary_ci, _number_of_items_dictionary_ci ); + return false; } /// @@ -2325,7 +2453,7 @@ namespace TinyJSON { if (parse_result.options().specification != parse_options::json5_1_0_0) { - parse_result.assign_exception_message(TJCHARPREFIX("Comments are only allowed in json5_1_0_0 specification.")); + parse_result.assign_exception_message(p, TJCHARPREFIX("Comments are only allowed in json5_1_0_0 specification.")); return nullptr; } @@ -2389,14 +2517,14 @@ namespace TinyJSON if (!closed) { delete[] result; - parse_result.assign_exception_message("Multi-line comment was not closed."); + parse_result.assign_exception_message(p, "Multi-line comment was not closed."); return nullptr; } } else { delete[] result; - parse_result.assign_exception_message("Invalid comment start."); + parse_result.assign_exception_message(p, "Invalid comment start."); return nullptr; } @@ -2425,7 +2553,7 @@ namespace TinyJSON if (!add_char_to_string(TJ_NULL_TERMINATOR, result, result_pos, result_max_length)) { delete[] result; - parse_result.assign_exception_message("Maximum JSON string read size exceeded."); + parse_result.assign_exception_message(p, "Maximum JSON string read size exceeded."); return nullptr; } return result; @@ -2443,14 +2571,14 @@ namespace TinyJSON { // ERROR: invalid character inside the string. delete[] result; - parse_result.assign_exception_message("Invalid character inside the string."); + parse_result.assign_exception_message(p, "Invalid character inside the string."); return nullptr; } break; case TJ_ESCAPE_TAB: // % x74 / ; t tab U + 0009 // ERROR: invalid character inside the string. delete[] result; - parse_result.assign_exception_message("Invalid character inside the string."); + parse_result.assign_exception_message(p, "Invalid character inside the string."); return nullptr; } add_char_to_string(*p, result, result_pos, result_max_length); @@ -2463,12 +2591,12 @@ namespace TinyJSON delete[] result; if (result_max_length == 0) { - parse_result.assign_exception_message("Maximum JSON string read size exceeded."); + parse_result.assign_exception_message(p, "Maximum JSON string read size exceeded."); } else { // ERROR: invalid/unknown character after single reverse solidus. - parse_result.assign_exception_message("Invalid/unknown character after single reverse solidus."); + parse_result.assign_exception_message(p, "Invalid/unknown character after single reverse solidus."); } return nullptr; } @@ -2481,7 +2609,7 @@ namespace TinyJSON if (!add_char_to_string(*p, result, result_pos, result_max_length)) { delete[] result; - parse_result.assign_exception_message("Maximum JSON string read size exceeded."); + parse_result.assign_exception_message(p, "Maximum JSON string read size exceeded."); return nullptr; } p++; @@ -2491,7 +2619,7 @@ namespace TinyJSON case TJ_ESCAPE_FORM_FEED: // % x66 / ; f form feed U + 000C delete[] result; // ERROR: invalid character inside the string. - parse_result.assign_exception_message("Invalid character inside the string.."); + parse_result.assign_exception_message(p, "Invalid character inside the string.."); return nullptr; default: @@ -2499,7 +2627,7 @@ namespace TinyJSON if (!add_char_to_string(*p, result, result_pos, result_max_length)) { delete[] result; - parse_result.assign_exception_message("Maximum JSON string read size exceeded."); + parse_result.assign_exception_message(p, "Maximum JSON string read size exceeded."); return nullptr; } p++; @@ -2509,7 +2637,7 @@ namespace TinyJSON // // ERROR: we could not close the object. delete[] result; - parse_result.assign_exception_message("We could not close the string."); + parse_result.assign_exception_message(p, "We could not close the string."); return nullptr; } @@ -3053,7 +3181,7 @@ namespace TinyJSON if (found_spaces > 0) { // ERROR: Number has a space between it. - parse_result.assign_exception_message("Number has a space between it."); + parse_result.assign_exception_message(p, "Number has a space between it."); return nullptr; } p++; @@ -3089,7 +3217,7 @@ namespace TinyJSON auto whole_number = try_read_whole_number(p, parse_result); if (nullptr == whole_number) { - parse_result.assign_exception_message("Fraction does not have a number after the token '.'"); + parse_result.assign_exception_message(p, "Fraction does not have a number after the token '.'"); return nullptr; } // trip the trailling zeros are they are not needed in a fraction @@ -3241,6 +3369,7 @@ namespace TinyJSON /// static TJValue* try_read_number(const TJCHAR*& p, ParseResult& parse_result) { + const TJCHAR* number_start = p; bool is_negative = false; if (*p == '-') { @@ -3283,7 +3412,7 @@ namespace TinyJSON if (has_possible_double_zero(possible_number) && parse_result.options().specification != parse_options::json5_1_0_0) { // ERROR: Numbers cannot have leading zeros - parse_result.assign_exception_message("Numbers cannot have leading zeros."); + parse_result.assign_exception_message(number_start, "Numbers cannot have leading zeros."); delete[] possible_number; return nullptr; } @@ -3342,7 +3471,7 @@ namespace TinyJSON if (nullptr == possible_exponent) { // ERROR: we cannot have a number like '-12e' or '42e - parse_result.assign_exception_message("Number has exponent 'e' or 'E' but does not have a number."); + parse_result.assign_exception_message(p, "Number has exponent 'e' or 'E' but does not have a number."); return nullptr; } @@ -3371,7 +3500,8 @@ namespace TinyJSON /// /// /// - static void move_member_to_members(TJMember* member, TJDICTIONARY*& members, const parse_options& options) + /// True if a member with the same name was replaced. + static bool move_member_to_members(TJMember* member, TJDICTIONARY*& members) { if (nullptr == members) { @@ -3383,9 +3513,9 @@ namespace TinyJSON #if TJ_INCLUDE_STDVECTOR == 1 members->push_back(member); #else - members->set(member, options); + members->set_or_replace(member); #endif - return; + return false; } #if TJ_INCLUDE_STDVECTOR == 1 @@ -3397,18 +3527,34 @@ namespace TinyJSON }); if (current != members->end()) { - options.callback_function(parse_options::message_type::trace, TJCHARPREFIX("Duplicate key found, overwriting.")); delete* current; *current = member; - return; + return true; } } members->push_back(member); + return false; #else - members->set(member, options); + return members->set_or_replace(member); #endif } + /// + /// Move the member to the members while we are parsing, + /// if the member replaced a duplicate key we raise a trace message with the position of that key. + /// + /// + /// + /// + /// Where the member started in the source. + static void move_member_to_members(TJMember* member, TJDICTIONARY*& members, const ParseResult& parse_result, const TJCHAR* position) + { + if (move_member_to_members(member, members)) + { + parse_result.raise(parse_options::message_type::trace, position, TJCHARPREFIX("Duplicate key found, overwriting.")); + } + } + static TJMember* try_read_unquoted_string_and_value(const TJCHAR*& p, ParseResult& parse_result) { const TJCHAR* start = p; @@ -3424,7 +3570,7 @@ namespace TinyJSON if (!try_skip_colon(p)) { delete[] key; - parse_result.assign_exception_message("Could not locate the expected colon after the unquoted key."); + parse_result.assign_exception_message(p, "Could not locate the expected colon after the unquoted key."); return nullptr; } @@ -3448,7 +3594,7 @@ namespace TinyJSON if (parse_result.current_depth() >= parse_result.options().max_depth) { // Error: We reached the max depth - parse_result.assign_exception_message("Reached the max parse depth (object)."); + parse_result.assign_exception_message(p, "Reached the max parse depth (object)."); return nullptr; } // assume no members in that object. @@ -3475,7 +3621,7 @@ namespace TinyJSON else { free_members(members); - parse_result.assign_exception_message("Unknown character."); + parse_result.assign_exception_message(p, "Unknown character."); return nullptr; } @@ -3489,7 +3635,7 @@ namespace TinyJSON } TJCHAR* empty_key = nullptr; auto member = TJMember::move(empty_key, comment, parse_result.options()); - move_member_to_members(member, members, parse_result.options()); + move_member_to_members(member, members, parse_result, p); // we are NOT waiting for a string, and we are NOT after a string // so that we can find the next actual member or comma or end. } @@ -3502,7 +3648,7 @@ namespace TinyJSON // ERROR: unexpected end of object, there was a "," after // the last string and we expected a string now, not a close "}" free_members(members); - parse_result.assign_exception_message("Unexpected end of object, there was a ', ' after the last string."); + parse_result.assign_exception_message(p, "Unexpected end of object, there was a ', ' after the last string."); return nullptr; } p++; @@ -3516,7 +3662,7 @@ namespace TinyJSON if (parse_result.options().specification != parse_options::json5_1_0_0) { free_members(members); - parse_result.assign_exception_message("Single quotes are only allowed in json5_1_0_0 specification."); + parse_result.assign_exception_message(p, "Single quotes are only allowed in json5_1_0_0 specification."); return nullptr; } } @@ -3532,7 +3678,7 @@ namespace TinyJSON { // ERROR: expected a comma after the last element free_members(members); - parse_result.assign_exception_message("Expected a comma after the last element."); + parse_result.assign_exception_message(p, "Expected a comma after the last element."); return nullptr; } @@ -3544,6 +3690,7 @@ namespace TinyJSON // read the actual string and value // that's the way it has to be. + const TJCHAR* member_start = p; auto member = try_read_string_and_value(p, parse_result, quote_char); if (member == nullptr) { @@ -3553,7 +3700,7 @@ namespace TinyJSON } found_comma = false; - move_member_to_members(member, members, parse_result.options()); + move_member_to_members(member, members, parse_result, member_start); after_string = true; } @@ -3564,7 +3711,7 @@ namespace TinyJSON { // ERROR: found a comma out of order free_members(members); - parse_result.assign_exception_message("Found a comma out of order."); + parse_result.assign_exception_message(p, "Found a comma out of order."); return nullptr; } // we are no longer after the string @@ -3587,7 +3734,7 @@ namespace TinyJSON if (after_string && !found_comma) { free_members(members); - parse_result.assign_exception_message("Expected a comma after the last element."); + parse_result.assign_exception_message(p, "Expected a comma after the last element."); return nullptr; } @@ -3597,6 +3744,7 @@ namespace TinyJSON // we are no longer after the string after_string = false; + const TJCHAR* member_start = p; auto member = try_read_unquoted_string_and_value(p, parse_result); if (member == nullptr) { @@ -3605,7 +3753,7 @@ namespace TinyJSON } found_comma = false; - move_member_to_members(member, members, parse_result.options()); + move_member_to_members(member, members, parse_result, member_start); after_string = true; break; } @@ -3613,14 +3761,14 @@ namespace TinyJSON // ERROR: unknown character free_members(members); - parse_result.assign_exception_message("Unknown character."); + parse_result.assign_exception_message(p, "Unknown character."); return nullptr; } } // ERROR end of the string was found and we didn't find what we needed. free_members(members); - parse_result.assign_exception_message("End of the string was found and we didn't find what we needed."); + parse_result.assign_exception_message(p, "End of the string was found and we didn't find what we needed."); return nullptr; } @@ -3635,7 +3783,7 @@ namespace TinyJSON if (parse_result.current_depth() >= parse_result.options().max_depth) { // Error: We reached the max depth - parse_result.assign_exception_message("Reached the max parse depth (array)."); + parse_result.assign_exception_message(p, "Reached the max parse depth (array)."); return nullptr; } @@ -3662,7 +3810,7 @@ namespace TinyJSON else { free_values(values); - parse_result.assign_exception_message("Unknown character."); + parse_result.assign_exception_message(p, "Unknown character."); return nullptr; } @@ -3694,7 +3842,7 @@ namespace TinyJSON // ERROR: unexpected end of array, there was a "," after // the last value and we expected a value now, not a close "]" free_values(values); - parse_result.assign_exception_message("Unexpected end of array, there was a ', ' after the last string."); + parse_result.assign_exception_message(p, "Unexpected end of array, there was a ', ' after the last string."); return nullptr; } p++; @@ -3708,7 +3856,7 @@ namespace TinyJSON { // ERROR: found a comma out of order, (2 commas) free_values(values); - parse_result.assign_exception_message("Found a comma out of order, (2 commas)."); + parse_result.assign_exception_message(p, "Found a comma out of order, (2 commas)."); return nullptr; } // we are now waiting for a value @@ -3723,6 +3871,7 @@ namespace TinyJSON p++; break; } + const TJCHAR* value_start = p; const auto& value = try_read_Value(p, parse_result); if (value == nullptr) { @@ -3739,7 +3888,7 @@ namespace TinyJSON // ERROR: We found a value but we expected a comma. delete value; free_values(values); - parse_result.assign_exception_message("We found a value but we expected a comma."); + parse_result.assign_exception_message(value_start, "We found a value but we expected a comma."); return nullptr; } #if TJ_INCLUDE_STDVECTOR == 1 @@ -3755,7 +3904,7 @@ namespace TinyJSON // ERROR: end of the string was found and we didn't find what we needed. free_values(values); - parse_result.assign_exception_message("End of the string was found and we didn't find what we needed."); + parse_result.assign_exception_message(p, "End of the string was found and we didn't find what we needed."); return nullptr; } @@ -3779,7 +3928,7 @@ namespace TinyJSON } else { - parse_result.assign_exception_message("Unexpected Token while trying to read value."); + parse_result.assign_exception_message(p, "Unexpected Token while trying to read value."); return nullptr; } @@ -3787,7 +3936,7 @@ namespace TinyJSON { if (parse_result.options().specification != parse_options::json5_1_0_0) { - parse_result.assign_exception_message("Single quotes are only allowed in json5_1_0_0 specification."); + parse_result.assign_exception_message(p, "Single quotes are only allowed in json5_1_0_0 specification."); return nullptr; } } @@ -3813,7 +3962,7 @@ namespace TinyJSON if (nullptr == true_value) { // ERROR could not read the word 'true' - parse_result.assign_exception_message("Could not read the word 'true'."); + parse_result.assign_exception_message(p - 1, "Could not read the word 'true'."); return nullptr; } return true_value; @@ -3825,7 +3974,7 @@ namespace TinyJSON if (nullptr == false_value) { // ERROR: could not read the word 'false' - parse_result.assign_exception_message("Could not read the word 'false'."); + parse_result.assign_exception_message(p - 1, "Could not read the word 'false'."); return nullptr; } return false_value; @@ -3837,7 +3986,7 @@ namespace TinyJSON if (nullptr == null_value) { // ERROR: could not read the word 'null' - parse_result.assign_exception_message("Could not read the word 'null'."); + parse_result.assign_exception_message(p - 1, "Could not read the word 'null'."); return nullptr; } return null_value; @@ -3857,14 +4006,14 @@ namespace TinyJSON case 'I': if (parse_result.options().specification != parse_options::json5_1_0_0) { - parse_result.assign_exception_message("Unexpected Token while trying to read value."); + parse_result.assign_exception_message(p, "Unexpected Token while trying to read value."); return nullptr; } TJ_FALLTHROUGH; case '.': if (c == '.' && parse_result.options().specification != parse_options::json5_1_0_0) { - parse_result.assign_exception_message("Unexpected Token while trying to read value."); + parse_result.assign_exception_message(p, "Unexpected Token while trying to read value."); return nullptr; } TJ_FALLTHROUGH; @@ -3915,7 +4064,7 @@ namespace TinyJSON break; } // ERROR: Unexpected Token while trying to read value. - parse_result.assign_exception_message("Unexpected Token while trying to read value."); + parse_result.assign_exception_message(p, "Unexpected Token while trying to read value."); return nullptr; } } @@ -3940,7 +4089,7 @@ namespace TinyJSON { delete[] string_value; // ERROR: could not locate the expected colon after the key value - parse_result.assign_exception_message("Could not locate the expected colon after the key value."); + parse_result.assign_exception_message(p, "Could not locate the expected colon after the key value."); return nullptr; } @@ -4133,8 +4282,12 @@ namespace TinyJSON source += 3; } + // all the line/column positions are relative to the start of the source, (after the BOM). + parse_result.set_source_begin(source); + // we can only have one value and nothing else TJValue* value_found = nullptr; + const TJCHAR* value_start = nullptr; while (*source != TJ_NULL_TERMINATOR) { TJCHAR c = *source; @@ -4198,12 +4351,13 @@ namespace TinyJSON { // Error: Unexpected multiple JSON values in root. delete value_found; - parse_result.assign_exception_message_and_throw("Unexpected multiple JSON values in root."); + parse_result.assign_exception_message_and_throw(source, "Unexpected multiple JSON values in root."); return nullptr; } } // try and look for the value + value_start = source; value_found = TJHelper::try_read_Value(source, parse_result); if (nullptr == value_found) { @@ -4218,7 +4372,7 @@ namespace TinyJSON if (value_found != nullptr && value_found->is_comment()) { - parse_result.assign_exception_message(TJCHARPREFIX("A JSON text cannot contain only a comment.")); + parse_result.assign_exception_message(source, TJCHARPREFIX("A JSON text cannot contain only a comment.")); delete value_found; value_found = nullptr; @@ -4228,7 +4382,7 @@ namespace TinyJSON if (value_found == nullptr) { - parse_result.assign_exception_message_and_throw("The JSON text is empty or contains no value."); + parse_result.assign_exception_message_and_throw(source, "The JSON text is empty or contains no value."); return nullptr; } @@ -4240,7 +4394,7 @@ namespace TinyJSON // throw if the options want us to, otherwise return null. // error: RFC 4627: A JSON text must be either an object or an array. - parse_result.assign_exception_message_and_throw("RFC 4627: A JSON text must be either an object or an array."); + parse_result.assign_exception_message_and_throw(value_start, "RFC 4627: A JSON text must be either an object or an array."); return nullptr; } } @@ -5584,7 +5738,7 @@ namespace TinyJSON if (type == parse_options::message_type::fatal) { - _parse_options.callback_function(parse_options::message_type::fatal, msg); + _parse_options.callback_function(parse_options::message_type::fatal, msg, -1, -1); if (_parse_options.throw_exception) { #if TJ_USE_CHAR == 1 @@ -5599,7 +5753,7 @@ namespace TinyJSON } else { - _parse_options.callback_function(type, msg); + _parse_options.callback_function(type, msg, -1, -1); } delete[] msg; @@ -6020,7 +6174,11 @@ namespace TinyJSON void TJValueObject::move_member_to_members(TJMember* member) { - TJHelper::move_member_to_members(member, _members, _parse_options); + if (TJHelper::move_member_to_members(member, _members)) + { + // there is no position in a source when the member is moved outside of parsing. + _parse_options.callback_function(parse_options::message_type::trace, TJCHARPREFIX("Duplicate key found, overwriting."), -1, -1); + } TJNumberedValues::reset_number_of_items(); } diff --git a/src/TinyJSON.h b/src/TinyJSON.h index b54965d..9a286f0 100644 --- a/src/TinyJSON.h +++ b/src/TinyJSON.h @@ -50,10 +50,11 @@ // v0.2.6 - fixed a bug in the resizing of string buffers. // v0.2.7 - improved parse_file error reporting with specific system errors and file name. // v0.2.8 - conformed empty input parsing to RFC specifications (returns nullptr / throws instead of empty string). +// v0.2.9 - added line and column numbers to parse messages and TJParseException. static const short TJ_VERSION_MAJOR = 0; static const short TJ_VERSION_MINOR = 2; -static const short TJ_VERSION_PATCH = 8; -static const char TJ_VERSION_STRING[] = "0.2.8"; +static const short TJ_VERSION_PATCH = 9; +static const char TJ_VERSION_STRING[] = "0.2.9"; #ifndef TJ_USE_CHAR # define TJ_USE_CHAR 1 @@ -381,8 +382,11 @@ class TJDictionary; // 3 = warning // 4 = error // 5 = fatal/panic/exception + // The line and column are 1-based and point to where in the source the message was raised. + // If there is no position, (the source is null, the file could not be opened, a key was not found etc.) + // then both the line and column are -1. /// - std::function callback_function = [] (message_type, const TJCHAR*) { + std::function callback_function = [] (message_type, const TJCHAR*, int, int) { // do nothing }; }; @@ -433,17 +437,29 @@ class TJDictionary; class TJParseException : public std::exception { public: - TJParseException(const char* message); + TJParseException(const char* message, int line = -1, int column = -1); TJParseException(const TJParseException& exception); TJParseException& operator=(const TJParseException& exception); virtual ~TJParseException() noexcept; virtual const char* what() const noexcept; + /// + /// The 1-based line number where the error happened, -1 if unknown. + /// + int line() const noexcept; + + /// + /// The 1-based column number where the error happened, -1 if unknown. + /// + int column() const noexcept; + private: void free_message() noexcept; void assign_message(const char* message); char* _message; + int _line; + int _column; }; struct internal_dump_configuration; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b0b6b8a..970db52 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -38,6 +38,7 @@ set( Sources "testtinyjsonvaluesget.cpp" "testtinyjsonversion.cpp" "testtinyjsonwarnings.cpp" + "testtinyjsonlinecolumn.cpp" "testtinyjsonconstructors.cpp" "testtinyjsonwrite.cpp" "testtinyjsonset.cpp" diff --git a/tests/testdefaultget.cpp b/tests/testdefaultget.cpp index c1699f8..70d927c 100644 --- a/tests/testdefaultget.cpp +++ b/tests/testdefaultget.cpp @@ -94,7 +94,7 @@ struct CallbackTracker int* trace_count; std::string* last_message; - void operator()(parse_options::message_type type, const TJCHAR* message) const + void operator()(parse_options::message_type type, const TJCHAR* message, int, int) const { if (type == parse_options::message_type::warning) { diff --git a/tests/testduplicatekeys.cpp b/tests/testduplicatekeys.cpp index 1044ccd..60eba13 100644 --- a/tests/testduplicatekeys.cpp +++ b/tests/testduplicatekeys.cpp @@ -9,7 +9,7 @@ TEST(TestDuplicateKeys, DuplicateKeysShouldTriggerTrace) { bool trace_triggered = false; TinyJSON::parse_options options; - options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* message) { + options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* message, int, int) { if (message_type == TinyJSON::parse_options::message_type::trace) { trace_triggered = true; } @@ -31,7 +31,7 @@ TEST(TestDuplicateKeys, DuplicateKeysShouldTriggerTrace) { TEST(TestDuplicateKeys, DuplicateKeysInSetShouldTriggerTrace) { bool trace_triggered = false; TinyJSON::parse_options options; - options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* message) { + options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* message, int, int) { if (message_type == TinyJSON::parse_options::message_type::trace) { trace_triggered = true; } diff --git a/tests/testtinyjsonexceptions.cpp b/tests/testtinyjsonexceptions.cpp index 320151b..4042443 100644 --- a/tests/testtinyjsonexceptions.cpp +++ b/tests/testtinyjsonexceptions.cpp @@ -257,7 +257,7 @@ TEST(TestException, EscapedTabCharacterInStringWillLogAndThrow) { bool called = false; TinyJSON::parse_options options = {}; options.throw_exception = true; - options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message) { + options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message, int, int) { EXPECT_EQ(message_type, TinyJSON::parse_options::fatal); EXPECT_TRUE(nullptr != exception_message); called = true; @@ -270,7 +270,7 @@ TEST(TestException, EscapedTabCharacterInStringWillLogAndNotThrow) { bool called = false; TinyJSON::parse_options options = {}; options.throw_exception = false; - options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message) { + options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message, int, int) { EXPECT_EQ(message_type, TinyJSON::parse_options::fatal); EXPECT_TRUE(nullptr != exception_message); called = true; @@ -284,7 +284,7 @@ TEST(TestException, GettingNonExistentKeyWillLogAndNotThrow) { TinyJSON::parse_options options = {}; options.throw_exception = false; options.strict = true; - options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message) { + options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message, int, int) { EXPECT_EQ(message_type, TinyJSON::parse_options::fatal); EXPECT_STREQ(exception_message, "The key 'b' was not found!"); called = true; @@ -307,7 +307,7 @@ TEST(TestException, ParseFileNotFoundSmarterError) std::string error_msg; TinyJSON::parse_options options = {}; options.throw_exception = false; - options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message) + options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message, int, int) { if (message_type == TinyJSON::parse_options::error) { @@ -326,7 +326,7 @@ TEST(TestException, ParseDirectorySmarterError) std::string error_msg; TinyJSON::parse_options options = {}; options.throw_exception = false; - options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message) + options.callback_function = [&](TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message, int, int) { if (message_type == TinyJSON::parse_options::error) { @@ -350,7 +350,7 @@ namespace bool called = false; std::string error_msg; - void operator()(TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message) + void operator()(TinyJSON::parse_options::message_type message_type, const TJCHAR* exception_message, int, int) { if (message_type == TinyJSON::parse_options::fatal || message_type == TinyJSON::parse_options::error) { diff --git a/tests/testtinyjsonlinecolumn.cpp b/tests/testtinyjsonlinecolumn.cpp new file mode 100644 index 0000000..5222137 --- /dev/null +++ b/tests/testtinyjsonlinecolumn.cpp @@ -0,0 +1,979 @@ +// Licensed to Florent Guelfucci under one or more agreements. +// Florent Guelfucci licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +#include +#define TJ_USE_CHAR 1 +#include "../src/TinyJSON.h" + +#include +#include +#include +#include + +namespace +{ + /// + /// Keep track of the messages, (and where they happened), raised by the parser. + /// + struct position_tracker + { + int fatal_count = 0; + int error_count = 0; + int warning_count = 0; + int trace_count = 0; + + // the last fatal/error/warning message. + std::string message; + int line = -2; + int column = -2; + + // the last trace message. + std::string trace_message; + int trace_line = -2; + int trace_column = -2; + + void operator()(TinyJSON::parse_options::message_type message_type, const TJCHAR* new_message, int new_line, int new_column) + { + switch (message_type) + { + case TinyJSON::parse_options::fatal: + ++fatal_count; + break; + + case TinyJSON::parse_options::error: + ++error_count; + break; + + case TinyJSON::parse_options::warning: + ++warning_count; + break; + + case TinyJSON::parse_options::trace: + ++trace_count; + trace_message = nullptr == new_message ? "" : new_message; + trace_line = new_line; + trace_column = new_column; + return; + + default: + return; + } + message = nullptr == new_message ? "" : new_message; + line = new_line; + column = new_column; + } + }; + + position_tracker parse_and_track_with_options(const TJCHAR* source, TinyJSON::parse_options options) + { + position_tracker tracker; + options.callback_function = std::ref(tracker); + auto* json = TinyJSON::TJ::parse(source, options); + delete json; + return tracker; + } + + position_tracker parse_and_track(const TJCHAR* source) + { + return parse_and_track_with_options(source, {}); + } + + position_tracker parse_and_track_with_specification(const TJCHAR* source, enum TinyJSON::parse_options::specification parse_specification) + { + TinyJSON::parse_options options = {}; + options.specification = parse_specification; + return parse_and_track_with_options(source, options); + } + + position_tracker parse5_and_track(const TJCHAR* source) + { + return parse_and_track_with_specification(source, TinyJSON::parse_options::json5_1_0_0); + } + + ::testing::AssertionResult is_fatal_at(const position_tracker& tracker, const std::string& expected_message, int expected_line, int expected_column) + { + if (tracker.fatal_count != 1) + { + return ::testing::AssertionFailure() << "Expected 1 fatal message but got " << tracker.fatal_count << " (last message: '" << tracker.message << "')"; + } + if (tracker.message != expected_message) + { + return ::testing::AssertionFailure() << "Expected message '" << expected_message << "' but got '" << tracker.message << "'"; + } + if (tracker.line != expected_line || tracker.column != expected_column) + { + return ::testing::AssertionFailure() << "Expected line " << expected_line << ", column " << expected_column + << " but got line " << tracker.line << ", column " << tracker.column << " for '" << tracker.message << "'"; + } + return ::testing::AssertionSuccess(); + } + + ::testing::AssertionResult is_fatal_at(const position_tracker& tracker, int expected_line, int expected_column) + { + return is_fatal_at(tracker, tracker.message, expected_line, expected_column); + } + + const char* line_column_file_name = "test_line_column_parse_file.json"; +} + +/////////////////////////////////////////////////////////////////////////////// +// Basics +TEST(TestLineColumn, ErrorAtTheVeryFirstCharacter) +{ + auto tracker = parse_and_track("x"); + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected Token while trying to read value.", 1, 1)); +} + +TEST(TestLineColumn, ErrorFurtherAlongTheFirstLine) +{ + auto tracker = parse_and_track("[1,2,x]"); + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected Token while trying to read value.", 1, 6)); +} + +TEST(TestLineColumn, ErrorOnTheSecondLine) +{ + auto tracker = parse_and_track("[\n x]"); + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected Token while trying to read value.", 2, 3)); +} + +TEST(TestLineColumn, ErrorOnTheThirdLine) +{ + auto tracker = parse_and_track("[\n1,\n x\n]"); + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected Token while trying to read value.", 3, 3)); +} + +TEST(TestLineColumn, ErrorOnTheTenthLine) +{ + auto tracker = parse_and_track("[\n\n\n\n\n\n\n\n\n x]"); + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected Token while trying to read value.", 10, 2)); +} + +TEST(TestLineColumn, ErrorOnALongLine) +{ + std::string source = "["; + for (int i = 0; i < 100; ++i) + { + source += "1,"; + } + source += "x]"; + auto tracker = parse_and_track(source.c_str()); + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected Token while trying to read value.", 1, 202)); +} + +TEST(TestLineColumn, ErrorAfterManyLines) +{ + std::string source = "["; + for (int i = 0; i < 500; ++i) + { + source += "\n 1,"; + } + source += "\n x]"; + auto tracker = parse_and_track(source.c_str()); + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected Token while trying to read value.", 502, 3)); +} + +/////////////////////////////////////////////////////////////////////////////// +// New lines +TEST(TestLineColumn, LineFeedIsANewLine) +{ + auto tracker = parse_and_track("[1,\nx]"); + EXPECT_TRUE(is_fatal_at(tracker, 2, 1)); +} + +TEST(TestLineColumn, CarriageReturnLineFeedIsASingleNewLine) +{ + auto tracker = parse_and_track("[\r\n x]"); + EXPECT_TRUE(is_fatal_at(tracker, 2, 3)); +} + +TEST(TestLineColumn, LoneCarriageReturnIsANewLine) +{ + auto tracker = parse_and_track("[\r x]"); + EXPECT_TRUE(is_fatal_at(tracker, 2, 3)); +} + +TEST(TestLineColumn, TwoLoneCarriageReturnsAreTwoNewLines) +{ + auto tracker = parse_and_track("[\r\r x]"); + EXPECT_TRUE(is_fatal_at(tracker, 3, 2)); +} + +TEST(TestLineColumn, LineFeedCarriageReturnIsTwoNewLines) +{ + auto tracker = parse_and_track("[\n\r x]"); + EXPECT_TRUE(is_fatal_at(tracker, 3, 2)); +} + +TEST(TestLineColumn, MixedLineEndings) +{ + auto tracker = parse_and_track("[\n\r\n\r x]"); + EXPECT_TRUE(is_fatal_at(tracker, 4, 3)); +} + +TEST(TestLineColumn, ManyCarriageReturnLineFeeds) +{ + auto tracker = parse_and_track("[\r\n1,\r\n2,\r\n3,\r\n x\r\n]"); + EXPECT_TRUE(is_fatal_at(tracker, 5, 5)); +} + +TEST(TestLineColumn, BlankLinesBeforeTheError) +{ + auto tracker = parse_and_track("\n\n\nx"); + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected Token while trying to read value.", 4, 1)); +} + +TEST(TestLineColumn, ColumnIsResetAfterANewLine) +{ + auto tracker = parse_and_track("[1, \nx]"); + EXPECT_TRUE(is_fatal_at(tracker, 2, 1)); +} + +TEST(TestLineColumn, ColumnIsResetAfterACarriageReturnLineFeed) +{ + auto tracker = parse_and_track("[1, \r\nx]"); + EXPECT_TRUE(is_fatal_at(tracker, 2, 1)); +} + +TEST(TestLineColumn, ErrorRightAfterANewLine) +{ + auto tracker = parse_and_track("[1,2,3\n,,4]"); + EXPECT_TRUE(is_fatal_at(tracker, "Found a comma out of order, (2 commas).", 2, 2)); +} + +TEST(TestLineColumn, SameDocumentWithLineFeedAndCarriageReturnLineFeed) +{ + auto tracker_line_feed = parse_and_track("{\n \"a\": {\n \"b\": [1, 2, tru]\n }\n}"); + auto tracker_carriage_return = parse_and_track("{\r\n \"a\": {\r\n \"b\": [1, 2, tru]\r\n }\r\n}"); + EXPECT_TRUE(is_fatal_at(tracker_line_feed, "Could not read the word 'true'.", 3, 17)); + EXPECT_TRUE(is_fatal_at(tracker_carriage_return, "Could not read the word 'true'.", 3, 17)); +} + +/////////////////////////////////////////////////////////////////////////////// +// White spaces +TEST(TestLineColumn, TabsCountAsOneColumn) +{ + auto tracker = parse_and_track("[\t\tx]"); + EXPECT_TRUE(is_fatal_at(tracker, 1, 4)); +} + +TEST(TestLineColumn, TabsAfterANewLine) +{ + auto tracker = parse_and_track("[\n\t\t\tx]"); + EXPECT_TRUE(is_fatal_at(tracker, 2, 4)); +} + +TEST(TestLineColumn, LeadingWhiteSpaces) +{ + auto tracker = parse_and_track(" x"); + EXPECT_TRUE(is_fatal_at(tracker, 1, 4)); +} + +TEST(TestLineColumn, TrailingWhiteSpacesBeforeASecondValue) +{ + auto tracker = parse_and_track("[1] x"); + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected multiple JSON values in root.", 1, 7)); +} + +TEST(TestLineColumn, Utf8BomIsNotCounted) +{ + auto tracker = parse_and_track("\xEF\xBB\xBFx"); + EXPECT_TRUE(is_fatal_at(tracker, 1, 1)); +} + +TEST(TestLineColumn, Utf8BomIsNotCountedOnTheFirstLine) +{ + auto tracker = parse_and_track("\xEF\xBB\xBF[1,x]"); + EXPECT_TRUE(is_fatal_at(tracker, 1, 4)); +} + +TEST(TestLineColumn, Utf8BomWithNewLines) +{ + auto tracker = parse_and_track("\xEF\xBB\xBF\n x"); + EXPECT_TRUE(is_fatal_at(tracker, 2, 2)); +} + +TEST(TestLineColumn, Utf8CharactersCountAsCodeUnits) +{ + // "\xC3\xA9" is 'e' with an acute accent, two code units in utf-8. + auto tracker = parse_and_track("[\"\xC3\xA9\", x]"); + EXPECT_TRUE(is_fatal_at(tracker, 1, 8)); +} + +/////////////////////////////////////////////////////////////////////////////// +// Strings +TEST(TestLineColumn, StringIsNotClosed) +{ + auto tracker = parse_and_track("[\"abc"); + EXPECT_TRUE(is_fatal_at(tracker, "We could not close the string.", 1, 6)); +} + +TEST(TestLineColumn, StringIsNotClosedOnTheSecondLine) +{ + auto tracker = parse_and_track("[\n\"abc"); + EXPECT_TRUE(is_fatal_at(tracker, "We could not close the string.", 2, 5)); +} + +TEST(TestLineColumn, TabInsideAString) +{ + auto tracker = parse_and_track("[\"a\tb\"]"); + EXPECT_TRUE(is_fatal_at(tracker, "Invalid character inside the string.", 1, 4)); +} + +TEST(TestLineColumn, NewLineInsideAString) +{ + auto tracker = parse_and_track("[\"a\nb\"]"); + EXPECT_TRUE(is_fatal_at(tracker, "Invalid character inside the string.", 1, 4)); +} + +TEST(TestLineColumn, CarriageReturnInsideAString) +{ + auto tracker = parse_and_track("[1,\n\"a\rb\"]"); + EXPECT_TRUE(is_fatal_at(tracker, "Invalid character inside the string.", 2, 3)); +} + +TEST(TestLineColumn, InvalidEscapeInsideAString) +{ + auto tracker = parse_and_track("[\"a\\qb\"]"); + EXPECT_TRUE(is_fatal_at(tracker, "Invalid/unknown character after single reverse solidus.", 1, 4)); +} + +TEST(TestLineColumn, InvalidEscapeInsideAStringOnTheSecondLine) +{ + auto tracker = parse_and_track("{\n \"a\": \"x\\q\"\n}"); + EXPECT_TRUE(is_fatal_at(tracker, "Invalid/unknown character after single reverse solidus.", 2, 10)); +} + +TEST(TestLineColumn, ValidEscapesBeforeTheError) +{ + auto tracker = parse_and_track("[\"\\n\\t\\\"\", x]"); + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected Token while trying to read value.", 1, 12)); +} + +/////////////////////////////////////////////////////////////////////////////// +// Numbers +TEST(TestLineColumn, LeadingZeroPointsToTheStartOfTheNumber) +{ + auto tracker = parse_and_track("[0123]"); + EXPECT_TRUE(is_fatal_at(tracker, "Numbers cannot have leading zeros.", 1, 2)); +} + +TEST(TestLineColumn, NegativeLeadingZeroPointsToTheSign) +{ + auto tracker = parse_and_track("[1, -0123]"); + EXPECT_TRUE(is_fatal_at(tracker, "Numbers cannot have leading zeros.", 1, 5)); +} + +TEST(TestLineColumn, LeadingZeroOnTheSecondLine) +{ + auto tracker = parse_and_track("[\n 0123]"); + EXPECT_TRUE(is_fatal_at(tracker, "Numbers cannot have leading zeros.", 2, 3)); +} + +TEST(TestLineColumn, MissingFractionDigit) +{ + auto tracker = parse_and_track("[12.]"); + EXPECT_TRUE(is_fatal_at(tracker, "Fraction does not have a number after the token '.'", 1, 5)); +} + +TEST(TestLineColumn, MissingExponentDigit) +{ + auto tracker = parse_and_track("[1e]"); + EXPECT_TRUE(is_fatal_at(tracker, "Number has exponent 'e' or 'E' but does not have a number.", 1, 4)); +} + +TEST(TestLineColumn, MissingExponentDigitAfterTheSign) +{ + auto tracker = parse_and_track("[1e+]"); + EXPECT_TRUE(is_fatal_at(tracker, "Number has exponent 'e' or 'E' but does not have a number.", 1, 5)); +} + +TEST(TestLineColumn, SpaceInsideANumber) +{ + auto tracker = parse_and_track("[1 2]"); + EXPECT_TRUE(is_fatal_at(tracker, "Number has a space between it.", 1, 4)); +} + +/////////////////////////////////////////////////////////////////////////////// +// Words +TEST(TestLineColumn, MisspelledTruePointsToTheStartOfTheWord) +{ + auto tracker = parse_and_track("[tru]"); + EXPECT_TRUE(is_fatal_at(tracker, "Could not read the word 'true'.", 1, 2)); +} + +TEST(TestLineColumn, MisspelledFalsePointsToTheStartOfTheWord) +{ + auto tracker = parse_and_track("[1, fals]"); + EXPECT_TRUE(is_fatal_at(tracker, "Could not read the word 'false'.", 1, 5)); +} + +TEST(TestLineColumn, MisspelledNullPointsToTheStartOfTheWord) +{ + auto tracker = parse_and_track("[nul]"); + EXPECT_TRUE(is_fatal_at(tracker, "Could not read the word 'null'.", 1, 2)); +} + +TEST(TestLineColumn, MisspelledNullOnTheSecondLine) +{ + auto tracker = parse_and_track("{\n \"a\": nul\n}"); + EXPECT_TRUE(is_fatal_at(tracker, "Could not read the word 'null'.", 2, 8)); +} + +/////////////////////////////////////////////////////////////////////////////// +// Objects +TEST(TestLineColumn, MissingColon) +{ + auto tracker = parse_and_track("{\"a\" 1}"); + EXPECT_TRUE(is_fatal_at(tracker, "Could not locate the expected colon after the key value.", 1, 6)); +} + +TEST(TestLineColumn, MissingColonAcrossLines) +{ + auto tracker = parse_and_track("{\n \"a\"\n 1}"); + EXPECT_TRUE(is_fatal_at(tracker, "Could not locate the expected colon after the key value.", 3, 3)); +} + +TEST(TestLineColumn, MissingCommaInObject) +{ + auto tracker = parse_and_track("{\"a\":1 \"b\":2}"); + EXPECT_TRUE(is_fatal_at(tracker, "Expected a comma after the last element.", 1, 8)); +} + +TEST(TestLineColumn, TrailingCommaInObject) +{ + auto tracker = parse_and_track("{\"a\":1,}"); + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected end of object, there was a ', ' after the last string.", 1, 8)); +} + +TEST(TestLineColumn, CommaOutOfOrderInObject) +{ + auto tracker = parse_and_track("{,\"a\":1}"); + EXPECT_TRUE(is_fatal_at(tracker, "Found a comma out of order.", 1, 2)); +} + +TEST(TestLineColumn, UnknownCharacterInObject) +{ + auto tracker = parse_and_track("{\n x}"); + EXPECT_TRUE(is_fatal_at(tracker, "Unknown character.", 2, 3)); +} + +TEST(TestLineColumn, ObjectIsNotClosed) +{ + auto tracker = parse_and_track("{\"a\":1"); + EXPECT_TRUE(is_fatal_at(tracker, "End of the string was found and we didn't find what we needed.", 1, 7)); +} + +TEST(TestLineColumn, ObjectIsNotClosedOnTheThirdLine) +{ + auto tracker = parse_and_track("{\n \"a\":1,\n \"b\":2"); + EXPECT_TRUE(is_fatal_at(tracker, "End of the string was found and we didn't find what we needed.", 3, 8)); +} + +TEST(TestLineColumn, MaxDepthObject) +{ + TinyJSON::parse_options options = {}; + options.max_depth = 2; + auto tracker = parse_and_track_with_options("{\"a\":{\"b\":1}}", options); + EXPECT_TRUE(is_fatal_at(tracker, "Reached the max parse depth (object).", 1, 7)); +} + +/////////////////////////////////////////////////////////////////////////////// +// Arrays +TEST(TestLineColumn, MissingCommaInArrayPointsToTheValue) +{ + auto tracker = parse_and_track("[true false]"); + EXPECT_TRUE(is_fatal_at(tracker, "We found a value but we expected a comma.", 1, 7)); +} + +TEST(TestLineColumn, MissingCommaInArrayAfterANumber) +{ + auto tracker = parse_and_track("[1, 2 \"a\"]"); + EXPECT_TRUE(is_fatal_at(tracker, "We found a value but we expected a comma.", 1, 7)); +} + +TEST(TestLineColumn, MissingCommaInArrayOnTheSecondLine) +{ + auto tracker = parse_and_track("[\n \"a\"\n \"b\"\n]"); + EXPECT_TRUE(is_fatal_at(tracker, "We found a value but we expected a comma.", 3, 3)); +} + +TEST(TestLineColumn, TrailingCommaInArray) +{ + auto tracker = parse_and_track("[1,]"); + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected end of array, there was a ', ' after the last string.", 1, 4)); +} + +TEST(TestLineColumn, CommaOutOfOrderInArray) +{ + auto tracker = parse_and_track("[,1]"); + EXPECT_TRUE(is_fatal_at(tracker, "Found a comma out of order, (2 commas).", 1, 2)); +} + +TEST(TestLineColumn, TwoCommasInArray) +{ + auto tracker = parse_and_track("[1,,2]"); + EXPECT_TRUE(is_fatal_at(tracker, "Found a comma out of order, (2 commas).", 1, 4)); +} + +TEST(TestLineColumn, ArrayIsNotClosed) +{ + auto tracker = parse_and_track("[1,2"); + EXPECT_TRUE(is_fatal_at(tracker, "End of the string was found and we didn't find what we needed.", 1, 5)); +} + +TEST(TestLineColumn, ArrayIsNotClosedOnTheThirdLine) +{ + auto tracker = parse_and_track("[\n1,\n2"); + EXPECT_TRUE(is_fatal_at(tracker, "End of the string was found and we didn't find what we needed.", 3, 2)); +} + +TEST(TestLineColumn, MaxDepthArray) +{ + TinyJSON::parse_options options = {}; + options.max_depth = 2; + auto tracker = parse_and_track_with_options("[[1]]", options); + EXPECT_TRUE(is_fatal_at(tracker, "Reached the max parse depth (array).", 1, 3)); +} + +TEST(TestLineColumn, MaxDepthArrayAcrossLines) +{ + TinyJSON::parse_options options = {}; + options.max_depth = 2; + auto tracker = parse_and_track_with_options("[\n [\n 1\n ]\n]", options); + EXPECT_TRUE(is_fatal_at(tracker, "Reached the max parse depth (array).", 2, 4)); +} + +/////////////////////////////////////////////////////////////////////////////// +// Root +TEST(TestLineColumn, MultipleRootValues) +{ + auto tracker = parse_and_track("[1] [2]"); + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected multiple JSON values in root.", 1, 5)); +} + +TEST(TestLineColumn, MultipleRootValuesOnTheSecondLine) +{ + auto tracker = parse_and_track("{}\n{}"); + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected multiple JSON values in root.", 2, 1)); +} + +TEST(TestLineColumn, Rfc4627RootMustBeObjectOrArray) +{ + auto tracker = parse_and_track_with_specification("\n \"abc\"", TinyJSON::parse_options::rfc4627); + EXPECT_TRUE(is_fatal_at(tracker, "RFC 4627: A JSON text must be either an object or an array.", 2, 3)); +} + +TEST(TestLineColumn, Rfc4627RootNumber) +{ + auto tracker = parse_and_track_with_specification(" 42", TinyJSON::parse_options::rfc4627); + EXPECT_TRUE(is_fatal_at(tracker, "RFC 4627: A JSON text must be either an object or an array.", 1, 3)); +} + +TEST(TestLineColumn, EmptySource) +{ + auto tracker = parse_and_track(""); + EXPECT_TRUE(is_fatal_at(tracker, "The JSON text is empty or contains no value.", 1, 1)); +} + +TEST(TestLineColumn, WhiteSpaceOnlySource) +{ + auto tracker = parse_and_track(" \n "); + EXPECT_TRUE(is_fatal_at(tracker, "The JSON text is empty or contains no value.", 2, 3)); +} + +TEST(TestLineColumn, IsValidAlsoReportsThePosition) +{ + position_tracker tracker; + TinyJSON::parse_options options = {}; + options.callback_function = std::ref(tracker); + EXPECT_FALSE(TinyJSON::TJ::is_valid("[1,\nx]", options)); + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected Token while trying to read value.", 2, 1)); +} + +/////////////////////////////////////////////////////////////////////////////// +// Json5 +TEST(TestLineColumn, Json5ErrorAfterASingleLineComment) +{ + auto tracker = parse5_and_track("// comment\n[1,x]"); + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected Token while trying to read value.", 2, 4)); +} + +TEST(TestLineColumn, Json5ErrorAfterAMultiLineComment) +{ + auto tracker = parse5_and_track("/* line1\nline2\n*/ [1,x]"); + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected Token while trying to read value.", 3, 7)); +} + +TEST(TestLineColumn, Json5ErrorAfterAMultiLineCommentWithCarriageReturnLineFeed) +{ + auto tracker = parse5_and_track("/* line1\r\nline2\r\n*/ [1,x]"); + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected Token while trying to read value.", 3, 7)); +} + +TEST(TestLineColumn, Json5MultiLineCommentIsNotClosed) +{ + auto tracker = parse5_and_track("[1, /* abc\n def"); + EXPECT_TRUE(is_fatal_at(tracker, "Multi-line comment was not closed.", 2, 5)); +} + +TEST(TestLineColumn, Json5InvalidCommentStart) +{ + auto tracker = parse5_and_track("[1, /x]"); + EXPECT_TRUE(is_fatal_at(tracker, "Invalid comment start.", 1, 6)); +} + +TEST(TestLineColumn, Json5OnlyAComment) +{ + auto tracker = parse5_and_track("/* x */"); + EXPECT_TRUE(is_fatal_at(tracker, "A JSON text cannot contain only a comment.", 1, 8)); +} + +TEST(TestLineColumn, Json5OnlyASingleLineComment) +{ + auto tracker = parse5_and_track("// hi\n"); + EXPECT_TRUE(is_fatal_at(tracker, "A JSON text cannot contain only a comment.", 2, 1)); +} + +TEST(TestLineColumn, CommentsAreRejectedOutsideJson5) +{ + auto tracker = parse_and_track("[1,\n // c\n2]"); + EXPECT_TRUE(is_fatal_at(tracker, "Comments are only allowed in json5_1_0_0 specification.", 2, 2)); +} + +TEST(TestLineColumn, SingleQuotesAreRejectedOutsideJson5) +{ + auto tracker = parse_and_track("[\n 'a']"); + EXPECT_TRUE(is_fatal_at(tracker, "Single quotes are only allowed in json5_1_0_0 specification.", 2, 2)); +} + +TEST(TestLineColumn, SingleQuotedKeysAreRejectedOutsideJson5) +{ + auto tracker = parse_and_track("{'a':1}"); + EXPECT_TRUE(is_fatal_at(tracker, "Single quotes are only allowed in json5_1_0_0 specification.", 1, 2)); +} + +TEST(TestLineColumn, Json5UnquotedKeyMissingColon) +{ + auto tracker = parse5_and_track("{\n abc 1}"); + EXPECT_TRUE(is_fatal_at(tracker, "Could not locate the expected colon after the unquoted key.", 2, 7)); +} + +TEST(TestLineColumn, Json5NewLineInsideAStringIsCounted) +{ + auto tracker = parse5_and_track("['a\nb', x]"); + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected Token while trying to read value.", 2, 5)); +} + +TEST(TestLineColumn, Json5ErrorDeepInsideADocument) +{ + auto tracker = parse5_and_track("{\n // a comment\n a: 1,\n b: [\n 'x',\n tru\n ]\n}"); + EXPECT_TRUE(is_fatal_at(tracker, "Could not read the word 'true'.", 6, 5)); +} + +/////////////////////////////////////////////////////////////////////////////// +// Exceptions +TEST(TestLineColumn, ExceptionHasTheSameLineAndColumnAsTheCallback) +{ + position_tracker tracker; + TinyJSON::parse_options options = {}; + options.throw_exception = true; + options.callback_function = std::ref(tracker); + try + { + auto* json = TinyJSON::TJ::parse("[1,\n x]", options); + delete json; + FAIL() << "Expected a TJParseException"; + } + catch (const TinyJSON::TJParseException& ex) + { + EXPECT_EQ(2, ex.line()); + EXPECT_EQ(3, ex.column()); + EXPECT_STREQ("Unexpected Token while trying to read value.", ex.what()); + } + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected Token while trying to read value.", 2, 3)); +} + +TEST(TestLineColumn, ExceptionFromParse5HasTheLineAndColumn) +{ + TinyJSON::parse_options options = {}; + options.throw_exception = true; + try + { + auto* json = TinyJSON::TJ::parse5("{\n a: 1,\n b: nul\n}", options); + delete json; + FAIL() << "Expected a TJParseException"; + } + catch (const TinyJSON::TJParseException& ex) + { + EXPECT_EQ(3, ex.line()); + EXPECT_EQ(6, ex.column()); + EXPECT_STREQ("Could not read the word 'null'.", ex.what()); + } +} + +TEST(TestLineColumn, ExceptionDefaultsToUnknownPosition) +{ + TinyJSON::TJParseException ex("Something went wrong"); + EXPECT_EQ(-1, ex.line()); + EXPECT_EQ(-1, ex.column()); + EXPECT_STREQ("Something went wrong", ex.what()); +} + +TEST(TestLineColumn, ExceptionCopyKeepsTheLineAndColumn) +{ + TinyJSON::TJParseException ex("Something went wrong", 3, 4); + TinyJSON::TJParseException copy(ex); + EXPECT_EQ(3, copy.line()); + EXPECT_EQ(4, copy.column()); + EXPECT_STREQ("Something went wrong", copy.what()); + + TinyJSON::TJParseException assigned("Something else"); + assigned = ex; + EXPECT_EQ(3, assigned.line()); + EXPECT_EQ(4, assigned.column()); + EXPECT_STREQ("Something went wrong", assigned.what()); +} + +/////////////////////////////////////////////////////////////////////////////// +// Unknown positions +TEST(TestLineColumn, NullSourceHasNoPosition) +{ + auto tracker = parse_and_track(nullptr); + EXPECT_TRUE(is_fatal_at(tracker, "The given source is null.", -1, -1)); +} + +TEST(TestLineColumn, NullSourceExceptionHasNoPosition) +{ + TinyJSON::parse_options options = {}; + options.throw_exception = true; + try + { + auto* json = TinyJSON::TJ::parse(nullptr, options); + delete json; + FAIL() << "Expected a TJParseException"; + } + catch (const TinyJSON::TJParseException& ex) + { + EXPECT_EQ(-1, ex.line()); + EXPECT_EQ(-1, ex.column()); + } +} + +TEST(TestLineColumn, NullFilePathHasNoPosition) +{ + position_tracker tracker; + TinyJSON::parse_options options = {}; + options.callback_function = std::ref(tracker); + auto* json = TinyJSON::TJ::parse_file(nullptr, options); + EXPECT_EQ(nullptr, json); + EXPECT_TRUE(is_fatal_at(tracker, "The given file path is null!", -1, -1)); +} + +TEST(TestLineColumn, MissingFileHasNoPosition) +{ + position_tracker tracker; + TinyJSON::parse_options options = {}; + options.callback_function = std::ref(tracker); + auto* json = TinyJSON::TJ::parse_file("this_line_column_file_does_not_exist.json", options); + EXPECT_EQ(nullptr, json); + EXPECT_EQ(1, tracker.error_count); + EXPECT_EQ(-1, tracker.line); + EXPECT_EQ(-1, tracker.column); +} + +TEST(TestLineColumn, DirectoryHasNoPosition) +{ + position_tracker tracker; + TinyJSON::parse_options options = {}; + options.callback_function = std::ref(tracker); + auto* json = TinyJSON::TJ::parse_file(".", options); + EXPECT_EQ(nullptr, json); + EXPECT_EQ(1, tracker.error_count); + EXPECT_EQ(-1, tracker.line); + EXPECT_EQ(-1, tracker.column); +} + +TEST(TestLineColumn, KeyNotFoundWarningHasNoPosition) +{ + position_tracker tracker; + TinyJSON::parse_options options = {}; + options.callback_function = std::ref(tracker); + auto* json = TinyJSON::TJ::parse("{\n \"a\": 1\n}", options); + ASSERT_NE(nullptr, json); + auto* object = dynamic_cast(json); + ASSERT_NE(nullptr, object); + + object->try_get_string("missing"); + EXPECT_EQ(1, tracker.warning_count); + EXPECT_EQ("The key 'missing' was not found!", tracker.message); + EXPECT_EQ(-1, tracker.line); + EXPECT_EQ(-1, tracker.column); + delete json; +} + +TEST(TestLineColumn, StrictKeyNotFoundHasNoPosition) +{ + position_tracker tracker; + TinyJSON::parse_options options = {}; + options.strict = true; + options.throw_exception = true; + options.callback_function = std::ref(tracker); + auto* json = TinyJSON::TJ::parse("{\n \"a\": 1\n}", options); + ASSERT_NE(nullptr, json); + auto* object = dynamic_cast(json); + ASSERT_NE(nullptr, object); + + try + { + object->get_string("missing"); + FAIL() << "Expected a TJParseException"; + } + catch (const TinyJSON::TJParseException& ex) + { + EXPECT_EQ(-1, ex.line()); + EXPECT_EQ(-1, ex.column()); + } + EXPECT_TRUE(is_fatal_at(tracker, "The key 'missing' was not found!", -1, -1)); + delete json; +} + +TEST(TestLineColumn, ConversionErrorHasNoPosition) +{ + position_tracker tracker; + TinyJSON::parse_options options = {}; + options.strict = true; + options.throw_exception = true; + options.callback_function = std::ref(tracker); + auto* json = TinyJSON::TJ::parse("\n\n \"abc\"", options); + ASSERT_NE(nullptr, json); + + try + { + json->get_number(); + FAIL() << "Expected a TJParseException"; + } + catch (const TinyJSON::TJParseException& ex) + { + EXPECT_EQ(-1, ex.line()); + EXPECT_EQ(-1, ex.column()); + } + EXPECT_TRUE(is_fatal_at(tracker, "The value is not a number!", -1, -1)); + delete json; +} + +/////////////////////////////////////////////////////////////////////////////// +// Files +TEST(TestLineColumn, ParseFileReportsThePosition) +{ + { + std::ofstream file(line_column_file_name, std::ios::binary); + file << "{\n \"a\": 1,\n \"b\": x\n}"; + } + + position_tracker tracker; + TinyJSON::parse_options options = {}; + options.callback_function = std::ref(tracker); + auto* json = TinyJSON::TJ::parse_file(line_column_file_name, options); + EXPECT_EQ(nullptr, json); + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected Token while trying to read value.", 3, 8)); + + std::remove(line_column_file_name); +} + +TEST(TestLineColumn, ParseFileWithBomAndCarriageReturnLineFeed) +{ + { + std::ofstream file(line_column_file_name, std::ios::binary); + file << "\xEF\xBB\xBF{\r\n \"a\": 1,\r\n \"b\": x\r\n}"; + } + + position_tracker tracker; + TinyJSON::parse_options options = {}; + options.throw_exception = true; + options.callback_function = std::ref(tracker); + try + { + auto* json = TinyJSON::TJ::parse_file(line_column_file_name, options); + delete json; + FAIL() << "Expected a TJParseException"; + } + catch (const TinyJSON::TJParseException& ex) + { + EXPECT_EQ(3, ex.line()); + EXPECT_EQ(8, ex.column()); + } + EXPECT_TRUE(is_fatal_at(tracker, "Unexpected Token while trying to read value.", 3, 8)); + + std::remove(line_column_file_name); +} + +/////////////////////////////////////////////////////////////////////////////// +// Duplicate keys +TEST(TestLineColumn, DuplicateKeyTracePointsToTheSecondKey) +{ + auto tracker = parse_and_track("{\"a\":1,\"a\":2}"); + EXPECT_EQ(0, tracker.fatal_count); + EXPECT_EQ(1, tracker.trace_count); + EXPECT_EQ("Duplicate key found, overwriting.", tracker.trace_message); + EXPECT_EQ(1, tracker.trace_line); + EXPECT_EQ(8, tracker.trace_column); +} + +TEST(TestLineColumn, DuplicateKeyTraceOnALaterLine) +{ + auto tracker = parse_and_track("{\n \"a\": 1,\n \"a\": 2\n}"); + EXPECT_EQ(0, tracker.fatal_count); + EXPECT_EQ(1, tracker.trace_count); + EXPECT_EQ(3, tracker.trace_line); + EXPECT_EQ(3, tracker.trace_column); +} + +TEST(TestLineColumn, DuplicateKeyTraceInANestedObject) +{ + auto tracker = parse_and_track("{\n \"x\": {\n \"a\": 1,\n \"a\": 2\n }\n}"); + EXPECT_EQ(0, tracker.fatal_count); + EXPECT_EQ(1, tracker.trace_count); + EXPECT_EQ(4, tracker.trace_line); + EXPECT_EQ(5, tracker.trace_column); +} + +TEST(TestLineColumn, Json5DuplicateUnquotedKeyTrace) +{ + auto tracker = parse5_and_track("{a:1,\n a:2}"); + EXPECT_EQ(0, tracker.fatal_count); + EXPECT_EQ(1, tracker.trace_count); + EXPECT_EQ(2, tracker.trace_line); + EXPECT_EQ(2, tracker.trace_column); +} + +TEST(TestLineColumn, DuplicateKeyFromSetHasNoPosition) +{ + position_tracker tracker; + TinyJSON::parse_options options = {}; + options.callback_function = std::ref(tracker); + TinyJSON::TJValueObject object(options); + object.set("a", 1); + object.set("a", 2); + EXPECT_EQ(1, tracker.trace_count); + EXPECT_EQ(-1, tracker.trace_line); + EXPECT_EQ(-1, tracker.trace_column); +} + +/////////////////////////////////////////////////////////////////////////////// +// Success +TEST(TestLineColumn, ValidMultiLineDocumentHasNoMessages) +{ + auto tracker = parse_and_track("{\n \"a\": [1, 2, 3],\r\n \"b\": {\"c\": true}\r \n}\n"); + EXPECT_EQ(0, tracker.fatal_count); + EXPECT_EQ(0, tracker.error_count); + EXPECT_EQ(0, tracker.warning_count); + EXPECT_EQ(0, tracker.trace_count); +} + +TEST(TestLineColumn, ValidJson5MultiLineDocumentHasNoMessages) +{ + auto tracker = parse5_and_track("// comment\n{\n a: 1, /* block\n comment */\n b: 'x',\n}\n"); + EXPECT_EQ(0, tracker.fatal_count); + EXPECT_EQ(0, tracker.error_count); + EXPECT_EQ(0, tracker.warning_count); +} diff --git a/tests/testtinyjsonversion.cpp b/tests/testtinyjsonversion.cpp index ea2abee..9c9ecb5 100644 --- a/tests/testtinyjsonversion.cpp +++ b/tests/testtinyjsonversion.cpp @@ -17,10 +17,10 @@ TEST(TestVersion, CheckVersionMinor) TEST(TestVersion, CheckVersionPatch) { - ASSERT_EQ(8, TJ_VERSION_PATCH); + ASSERT_EQ(9, TJ_VERSION_PATCH); } TEST(TestVersion, CheckVersionString) { - ASSERT_STREQ("0.2.8", TJ_VERSION_STRING); + ASSERT_STREQ("0.2.9", TJ_VERSION_STRING); } diff --git a/tests/testtinyjsonwarnings.cpp b/tests/testtinyjsonwarnings.cpp index fccbc7f..0a7e77e 100644 --- a/tests/testtinyjsonwarnings.cpp +++ b/tests/testtinyjsonwarnings.cpp @@ -20,7 +20,7 @@ TEST(TestWarnings, GetterWarnings) int warning_count = 0; std::string last_warning_message = ""; TinyJSON::parse_options options = {}; - options.callback_function = [&](TinyJSON::parse_options::message_type type, const TJCHAR* message) + options.callback_function = [&](TinyJSON::parse_options::message_type type, const TJCHAR* message, int, int) { if (type == TinyJSON::parse_options::message_type::warning) {