diff --git a/docs/mkdocs/docs/api/basic_json_view/at.md b/docs/mkdocs/docs/api/basic_json_view/at.md index c9f2daa8c..7b810c4ed 100644 --- a/docs/mkdocs/docs/api/basic_json_view/at.md +++ b/docs/mkdocs/docs/api/basic_json_view/at.md @@ -70,7 +70,7 @@ Strong guarantee: there are no changes to the view or the document it refers to. As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) these exceptions name the JSON Pointer path of the value within the document, and with -[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text (where +[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text (when [`materialize()`](materialize.md) sets them). ## Complexity diff --git a/docs/mkdocs/docs/api/basic_json_view/back.md b/docs/mkdocs/docs/api/basic_json_view/back.md index 17cd36a0e..f594928b0 100644 --- a/docs/mkdocs/docs/api/basic_json_view/back.md +++ b/docs/mkdocs/docs/api/basic_json_view/back.md @@ -22,7 +22,7 @@ Throws [`invalid_iterator.214`](../../home/exceptions.md#jsonexceptioninvalid_it As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the exception names the JSON Pointer path of the value within the document, and with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) -its bytes in the source text (where [`materialize()`](materialize.md) sets them). +its bytes in the source text (when [`materialize()`](materialize.md) sets them). ## Complexity diff --git a/docs/mkdocs/docs/api/basic_json_view/front.md b/docs/mkdocs/docs/api/basic_json_view/front.md index d983f5b3c..da48fcf88 100644 --- a/docs/mkdocs/docs/api/basic_json_view/front.md +++ b/docs/mkdocs/docs/api/basic_json_view/front.md @@ -22,7 +22,7 @@ Throws [`invalid_iterator.214`](../../home/exceptions.md#jsonexceptioninvalid_it As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the exception names the JSON Pointer path of the value within the document, and with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) -its bytes in the source text (where [`materialize()`](materialize.md) sets them). +its bytes in the source text (when [`materialize()`](materialize.md) sets them). ## Complexity diff --git a/docs/mkdocs/docs/api/basic_json_view/get_string.md b/docs/mkdocs/docs/api/basic_json_view/get_string.md index 039dff6bc..b7d4d4203 100644 --- a/docs/mkdocs/docs/api/basic_json_view/get_string.md +++ b/docs/mkdocs/docs/api/basic_json_view/get_string.md @@ -24,7 +24,7 @@ Throws [`type_error.302`](../../home/exceptions.md#jsonexceptiontype_error302) i As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the exception names the JSON Pointer path of the value within the document, and with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) -its bytes in the source text (where [`materialize()`](materialize.md) sets them). +its bytes in the source text (when [`materialize()`](materialize.md) sets them). ## Complexity diff --git a/docs/mkdocs/docs/api/basic_json_view/number_token.md b/docs/mkdocs/docs/api/basic_json_view/number_token.md index 3fa5b982c..6451cb1fa 100644 --- a/docs/mkdocs/docs/api/basic_json_view/number_token.md +++ b/docs/mkdocs/docs/api/basic_json_view/number_token.md @@ -24,7 +24,7 @@ Throws [`type_error.302`](../../home/exceptions.md#jsonexceptiontype_error302) i As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the exception names the JSON Pointer path of the value within the document, and with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) -its bytes in the source text (where [`materialize()`](materialize.md) sets them). +its bytes in the source text (when [`materialize()`](materialize.md) sets them). ## Complexity diff --git a/docs/mkdocs/docs/api/basic_json_view/operator[].md b/docs/mkdocs/docs/api/basic_json_view/operator[].md index 78972277b..a04bfd3b8 100644 --- a/docs/mkdocs/docs/api/basic_json_view/operator[].md +++ b/docs/mkdocs/docs/api/basic_json_view/operator[].md @@ -75,7 +75,7 @@ Strong guarantee: there are no changes to the view or the document it refers to. As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) these exceptions name the JSON Pointer path of the value within the document, and with -[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text (where +[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text (when [`materialize()`](materialize.md) sets them). ## Complexity diff --git a/docs/mkdocs/docs/api/basic_json_view/value.md b/docs/mkdocs/docs/api/basic_json_view/value.md index a987b34c9..ebce7b4bf 100644 --- a/docs/mkdocs/docs/api/basic_json_view/value.md +++ b/docs/mkdocs/docs/api/basic_json_view/value.md @@ -63,7 +63,7 @@ Strong guarantee: there are no changes to the view or the document it refers to. As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) these exceptions name the JSON Pointer path of the value within the document, and with -[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text (where +[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text (when [`materialize()`](materialize.md) sets them). ## Complexity diff --git a/docs/mkdocs/docs/features/json_view.md b/docs/mkdocs/docs/features/json_view.md index 53b05f848..4ce42bde7 100644 --- a/docs/mkdocs/docs/features/json_view.md +++ b/docs/mkdocs/docs/features/json_view.md @@ -134,14 +134,21 @@ document: `#!cpp auto v = json_document::parse(text).root();` does not compile. - **Macros.** [`JSON_STRICT_NUL_HANDLING`](../api/macros/json_strict_nul_handling.md) and [`JSON_NOEXCEPTION`](../api/macros/json_noexception.md)/[`JSON_THROW_USER`](../api/macros/json_throw_user.md) behave the same way they do for ``. These are the macros that change what the parser accepts or - how it reports errors. With [`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md) and - [`JSON_DIAGNOSTIC_POSITIONS`](../api/macros/json_diagnostic_positions.md), the exceptions of views name the path - and the bytes of the value as those of `#!cpp basic_json` do; as a view has neither parent pointers nor stored - positions, both are found from the document's index when the exception is thrown, which takes time linear in the - size of the document (only for an exception). One exception has neither: `key()` of an iterator over an array or a - primitive value ([`invalid_iterator.207`](../home/exceptions.md#jsonexceptioninvalid_iterator207)), as a view's - iterator does not know the value it iterates over. The other macros only affect the values `materialize()` produces, - through `BasicJsonType`. + how it reports errors. Two more macros add context to the exceptions of views, as they do for those of + `#!cpp basic_json`: + + - With [`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md), an exception names the JSON Pointer path of the + value within the document. + - With [`JSON_DIAGNOSTIC_POSITIONS`](../api/macros/json_diagnostic_positions.md), an exception names the bytes of + the value in the source text. + + A view has neither parent pointers nor stored positions, so both are found from the document's index when the + exception is thrown. This takes time linear in the size of the document. One exception has neither: + `key()` of an iterator over an array or a primitive value + ([`invalid_iterator.207`](../home/exceptions.md#jsonexceptioninvalid_iterator207)), because a view's iterator does + not know the value it iterates over. + + The other macros only affect the values `materialize()` produces, through `BasicJsonType`. ## What is different diff --git a/include/nlohmann/detail/view/errors.hpp b/include/nlohmann/detail/view/errors.hpp index adb61c8d9..6ce25316b 100644 --- a/include/nlohmann/detail/view/errors.hpp +++ b/include/nlohmann/detail/view/errors.hpp @@ -40,16 +40,11 @@ class error_site public: error_site() noexcept = default; +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS error_site(const document_data* d, const node* n) noexcept -#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS : m_doc(d), m_node(n) -#endif - { - static_cast(d); - static_cast(n); - } + {} -#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS const document_data* doc() const noexcept { return m_doc; @@ -62,6 +57,8 @@ class error_site private: const document_data* m_doc = nullptr; const node* m_node = nullptr; +#else + error_site(const document_data* /*d*/, const node* /*n*/) noexcept {} #endif }; @@ -78,6 +75,7 @@ struct view_access template error_site site_of(const View& v) noexcept { + static_assert(is_json_view::value, "the site of a value is only defined for views"); return view_access::site(v); } @@ -115,16 +113,18 @@ bool value_path(const document_data& d, const node* target, std::vector stack; stack.push_back(frame{nav::first(d, root), nav::end(d, root), 0, root->kind == node_kind::object}); // (tokens[i] leads from stack[i] to stack[i + 1], or to the target) - while (!stack.empty()) + for (;;) { + // (f is not used after the stack changes: pop_back() below, push_back() at the end) frame& f = stack.back(); if (f.pos == f.end) { stack.pop_back(); - if (!tokens.empty()) + if (stack.empty()) { - tokens.pop_back(); + return false; } + tokens.pop_back(); continue; } const node* entry = f.pos; @@ -145,20 +145,19 @@ bool value_path(const document_data& d, const node* target, std::vector child && target < child + child->next))) { tokens.push_back(token); stack.push_back(frame{nav::first(d, child), nav::end(d, child), 0, child->kind == node_kind::object}); } } - return false; } #endif @@ -195,32 +194,31 @@ void value_range(const document_data& d, const node* n, std::size_t& start, std: std::size_t pos = std::string::npos; for (const node* c = n;;) { - if (is_container(*c)) + if (!is_container(*c)) { - if ((c->flags & node_flags::is_new) != 0 || c->off >= d.size) - { - return; - } - ++closing; - const node* const first = nav::first(d, c); - const node* const last_end = nav::end(d, c); - if (first == last_end) - { - pos = std::size_t{c->off} + 1; - break; - } - const std::size_t value_offset = c->kind == node_kind::object ? 1 : 0; - const node* last = first; - for (const node* e = document_data::after(first + value_offset); e != last_end; e = document_data::after(e + value_offset)) - { - last = e; - } - c = nav::value(last + value_offset); - continue; + p.set_scalar(d, *c); + pos = p.end; + break; } - p.set_scalar(d, *c); - pos = p.end; - break; + if ((c->flags & node_flags::is_new) != 0 || c->off >= d.size) + { + return; + } + ++closing; + const node* const first = nav::first(d, c); + const node* const last_end = nav::end(d, c); + if (first == last_end) + { + pos = std::size_t{c->off} + 1; + break; + } + const std::size_t value_offset = c->kind == node_kind::object ? 1 : 0; + const node* last = first; + for (const node* e = document_data::after(first + value_offset); e != last_end; e = document_data::after(e + value_offset)) + { + last = e; + } + c = nav::value(last + value_offset); } for (; closing != 0 && pos != std::string::npos; --closing) { @@ -245,33 +243,17 @@ void value_range(const document_data& d, const node* n, std::size_t& start, std: /// function makes it), it changed GCC's early inlining of unrelated view /// functions, so that GCC 13 no longer inlined a recursive walk over a view /// into itself (2.7 times the instructions). +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS template std::string diagnostics(const error_site site) { -#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS if (site.doc() == nullptr || site.value() == nullptr) { return ""; } const document_data& d = *site.doc(); const bool edited = d.edits != nullptr; - std::string bytes; -#if JSON_DIAGNOSTIC_POSITIONS - std::size_t start = 0; - std::size_t end = 0; - if (edited) - { - value_range(d, site.value(), start, end); - } - else - { - value_range(d, site.value(), start, end); - } - if (start != std::string::npos && end != std::string::npos) - { - bytes = concat("(bytes ", std::to_string(start), "-", std::to_string(end), ") "); - } -#endif + std::string context; #if JSON_DIAGNOSTICS std::vector tokens; const bool found = edited ? value_path(d, site.value(), tokens) : value_path(d, site.value(), tokens); @@ -285,15 +267,33 @@ std::string diagnostics(const error_site site) path.push_back('/'); path += detail::escape(token); } - return concat('(', path, ") ", bytes); -#else - return bytes; + context = concat('(', path, ") "); #endif -#else - static_cast(site); - return ""; +#if JSON_DIAGNOSTIC_POSITIONS + std::size_t start = 0; + std::size_t end = 0; + if (edited) + { + value_range(d, site.value(), start, end); + } + else + { + value_range(d, site.value(), start, end); + } + if (start != std::string::npos && end != std::string::npos) + { + context += concat("(bytes ", std::to_string(start), "-", std::to_string(end), ") "); + } #endif + return context; } +#else +template +std::string diagnostics(const error_site /*site*/) +{ + return ""; +} +#endif /// the context of an exception about view @a v (see exception_prefix()) template @@ -305,6 +305,13 @@ std::string diagnostics_of(const View& v) // Exceptions are thrown out of line, so that the accessors that may throw stay // small enough to be inlined. The overloads with an error_site put its context // in front of the message. +// +// The overloads without one are for exceptions that are not about a value of a +// document: limits and invalid input of the editing and image functions, a +// discarded view (it has no node), a view of another document (its path would +// be one in that document), and those that basic_json creates without a +// context too (the errors of json_pointer, invalid UTF-8). The one exception +// about a value that has no site is key(), see view_iterator::key(). [[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_type_error(exception_id id, const char* prefix, const char* type) { diff --git a/include/nlohmann/detail/view/pointer.hpp b/include/nlohmann/detail/view/pointer.hpp index b7df4224d..c17561134 100644 --- a/include/nlohmann/detail/view/pointer.hpp +++ b/include/nlohmann/detail/view/pointer.hpp @@ -44,8 +44,8 @@ index_status array_index(const StringType& s, std::size_t& idx) noexcept } /// throws the exception json_pointer::array_index throws for this status -template /// (without a context, as json_pointer::array_index) +template [[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_error(index_status status, const StringType& s) { switch (status) diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index faaa06bf4..cfb879a09 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -4546,16 +4546,11 @@ class error_site public: error_site() noexcept = default; +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS error_site(const document_data* d, const node* n) noexcept -#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS : m_doc(d), m_node(n) -#endif - { - static_cast(d); - static_cast(n); - } + {} -#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS const document_data* doc() const noexcept { return m_doc; @@ -4568,6 +4563,8 @@ class error_site private: const document_data* m_doc = nullptr; const node* m_node = nullptr; +#else + error_site(const document_data* /*d*/, const node* /*n*/) noexcept {} #endif }; @@ -4584,6 +4581,7 @@ struct view_access template error_site site_of(const View& v) noexcept { + static_assert(is_json_view::value, "the site of a value is only defined for views"); return view_access::site(v); } @@ -4621,16 +4619,18 @@ bool value_path(const document_data& d, const node* target, std::vector stack; stack.push_back(frame{nav::first(d, root), nav::end(d, root), 0, root->kind == node_kind::object}); // (tokens[i] leads from stack[i] to stack[i + 1], or to the target) - while (!stack.empty()) + for (;;) { + // (f is not used after the stack changes: pop_back() below, push_back() at the end) frame& f = stack.back(); if (f.pos == f.end) { stack.pop_back(); - if (!tokens.empty()) + if (stack.empty()) { - tokens.pop_back(); + return false; } + tokens.pop_back(); continue; } const node* entry = f.pos; @@ -4651,20 +4651,19 @@ bool value_path(const document_data& d, const node* target, std::vector child && target < child + child->next))) { tokens.push_back(token); stack.push_back(frame{nav::first(d, child), nav::end(d, child), 0, child->kind == node_kind::object}); } } - return false; } #endif @@ -4701,32 +4700,31 @@ void value_range(const document_data& d, const node* n, std::size_t& start, std: std::size_t pos = std::string::npos; for (const node* c = n;;) { - if (is_container(*c)) + if (!is_container(*c)) { - if ((c->flags & node_flags::is_new) != 0 || c->off >= d.size) - { - return; - } - ++closing; - const node* const first = nav::first(d, c); - const node* const last_end = nav::end(d, c); - if (first == last_end) - { - pos = std::size_t{c->off} + 1; - break; - } - const std::size_t value_offset = c->kind == node_kind::object ? 1 : 0; - const node* last = first; - for (const node* e = document_data::after(first + value_offset); e != last_end; e = document_data::after(e + value_offset)) - { - last = e; - } - c = nav::value(last + value_offset); - continue; + p.set_scalar(d, *c); + pos = p.end; + break; } - p.set_scalar(d, *c); - pos = p.end; - break; + if ((c->flags & node_flags::is_new) != 0 || c->off >= d.size) + { + return; + } + ++closing; + const node* const first = nav::first(d, c); + const node* const last_end = nav::end(d, c); + if (first == last_end) + { + pos = std::size_t{c->off} + 1; + break; + } + const std::size_t value_offset = c->kind == node_kind::object ? 1 : 0; + const node* last = first; + for (const node* e = document_data::after(first + value_offset); e != last_end; e = document_data::after(e + value_offset)) + { + last = e; + } + c = nav::value(last + value_offset); } for (; closing != 0 && pos != std::string::npos; --closing) { @@ -4751,33 +4749,17 @@ void value_range(const document_data& d, const node* n, std::size_t& start, std: /// function makes it), it changed GCC's early inlining of unrelated view /// functions, so that GCC 13 no longer inlined a recursive walk over a view /// into itself (2.7 times the instructions). +#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS template std::string diagnostics(const error_site site) { -#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS if (site.doc() == nullptr || site.value() == nullptr) { return ""; } const document_data& d = *site.doc(); const bool edited = d.edits != nullptr; - std::string bytes; -#if JSON_DIAGNOSTIC_POSITIONS - std::size_t start = 0; - std::size_t end = 0; - if (edited) - { - value_range(d, site.value(), start, end); - } - else - { - value_range(d, site.value(), start, end); - } - if (start != std::string::npos && end != std::string::npos) - { - bytes = concat("(bytes ", std::to_string(start), "-", std::to_string(end), ") "); - } -#endif + std::string context; #if JSON_DIAGNOSTICS std::vector tokens; const bool found = edited ? value_path(d, site.value(), tokens) : value_path(d, site.value(), tokens); @@ -4791,15 +4773,33 @@ std::string diagnostics(const error_site site) path.push_back('/'); path += detail::escape(token); } - return concat('(', path, ") ", bytes); -#else - return bytes; + context = concat('(', path, ") "); #endif -#else - static_cast(site); - return ""; +#if JSON_DIAGNOSTIC_POSITIONS + std::size_t start = 0; + std::size_t end = 0; + if (edited) + { + value_range(d, site.value(), start, end); + } + else + { + value_range(d, site.value(), start, end); + } + if (start != std::string::npos && end != std::string::npos) + { + context += concat("(bytes ", std::to_string(start), "-", std::to_string(end), ") "); + } #endif + return context; } +#else +template +std::string diagnostics(const error_site /*site*/) +{ + return ""; +} +#endif /// the context of an exception about view @a v (see exception_prefix()) template @@ -4811,6 +4811,13 @@ std::string diagnostics_of(const View& v) // Exceptions are thrown out of line, so that the accessors that may throw stay // small enough to be inlined. The overloads with an error_site put its context // in front of the message. +// +// The overloads without one are for exceptions that are not about a value of a +// document: limits and invalid input of the editing and image functions, a +// discarded view (it has no node), a view of another document (its path would +// be one in that document), and those that basic_json creates without a +// context too (the errors of json_pointer, invalid UTF-8). The one exception +// about a value that has no site is key(), see view_iterator::key(). [[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_type_error(exception_id id, const char* prefix, const char* type) { @@ -8019,8 +8026,8 @@ index_status array_index(const StringType& s, std::size_t& idx) noexcept } /// throws the exception json_pointer::array_index throws for this status -template /// (without a context, as json_pointer::array_index) +template [[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_error(index_status status, const StringType& s) { switch (status) diff --git a/tests/src/unit-json_view_positions.cpp b/tests/src/unit-json_view_positions.cpp index c371d8941..c6afd7fcb 100644 --- a/tests/src/unit-json_view_positions.cpp +++ b/tests/src/unit-json_view_positions.cpp @@ -512,6 +512,26 @@ TEST_CASE("json_view exceptions name the path and the bytes of the value") CHECK(exception_of_call([&] { static_cast(d.root()["a"][2]["n"][0].get()); }) == expected); } + SECTION("deep documents with siblings after every nested container") + { + // the search for the path pushes and pops one frame per level (the + // stack reallocates while it grows, and shrinks again at every + // sibling): run under a sanitizer, a reference to a popped or moved + // frame would show here + std::string text = "7"; + for (int level = 0; level < 40; ++level) + { + text = R"([[1, [2]], )" + text + R"(, {"k": [3, {"l": [4]}]}, [5, [6, [7]]]])"; + } + CAPTURE(text) + const json j = json::parse(text); + const json_document d = json_document::parse(text); + check_exceptions(d.root(), j); + json_editable_document e = json_editable_document::parse(text); + e.set(e.root()[0][0], 9); + check_exceptions(e.root(), e.root().materialize()); + } + SECTION("examples") { const std::string text = R"({"people": [{"name": "a", "age": 1}, {"name": "c", "age": "x"}], "o": {"k": 1}})";