From 367336c83d47e26c3a36b8a58ef829ad43b3c217 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 7 Oct 2026 16:37:19 +0200 Subject: [PATCH] Fix CI on develop after #5585 (#5779) * Fix CI on develop after #5585 - test-diagnostics-optimized: -O3 makes GCC's -Winline and -Wsuggest-attribute=pure/const warnings fire with the ci_test_gcc flag set; turn them off for this test. - test-diagnostics-optimized: suppress Clang's -Wexit-time-destructors for the static table in to_json. - Infer: raise pulse-max-disjuncts from 20 to 40. With the default, Pulse loses the stored type in basic_json::replace_value() and reports false null dereferences of get_ptr() results in unit-pointer_access.cpp. Signed-off-by: Niels Lohmann * Ignore Infer's false USE_AFTER_DELETE in ordered_map::erase Infer's std::string model keeps the buffer of a moved-from string, so the destroy-and-reconstruct loop in erase(first, last) looks like it destroys a buffer twice. Signed-off-by: Niels Lohmann * Mark throw_on_discarded()'s parameters as used without exceptions With JSON_NOEXCEPTION, JSON_THROW expands to std::abort(), so Clang's -Wunused-parameter breaks test-disabled_exceptions (since #5761). Signed-off-by: Niels Lohmann * Skip the span_input_adapter sax_parse checks with deleted deprecated functions The #5676 regression test (#5740) calls the deprecated sax_parse(span_input_adapter&&, ...), which JSON_DELETE_DEPRECATED_FUNCTIONS deletes, so ci_test_delete_deprecated_functions failed to build. Signed-off-by: Niels Lohmann * Fall back to the first entry in test-diagnostics-optimized's to_json clang-tidy (clang-analyzer-security.ArrayBound) flagged it->second for a value not in the table. Use the same fallback as NLOHMANN_JSON_SERIALIZE_ENUM; the test still fails with -Werror=array-bounds on the headers from before #5585. Signed-off-by: Niels Lohmann * Fix clang-tidy findings in tests from #5762 and #5774 - unit-regression2.cpp (#5762): const/auto for the destroy() test values; NOLINT the intended copy in check_destroy_edge_case(). - unit-serialization.cpp (#5774): build the expected strings with += instead of chained operator+ (performance-inefficient-string-concatenation). Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- .inferconfig | 2 ++ include/nlohmann/detail/output/binary_writer.hpp | 2 ++ include/nlohmann/ordered_map.hpp | 3 +++ single_include/nlohmann/json.hpp | 5 +++++ tests/CMakeLists.txt | 7 +++++-- tests/src/unit-diagnostics-optimized.cpp | 8 +++++++- tests/src/unit-regression2.cpp | 14 +++++++------- tests/src/unit-regression3.cpp | 12 ++++++++++++ tests/src/unit-serialization.cpp | 11 +++++++++-- 9 files changed, 52 insertions(+), 12 deletions(-) diff --git a/.inferconfig b/.inferconfig index 9a5a36873..21f6229e9 100644 --- a/.inferconfig +++ b/.inferconfig @@ -1,5 +1,7 @@ { "_comment": "Used by the ci_infer CMake target (#5715 item 4b). fail-on-issue makes CI fail on Infer findings; disable-issue-type is a type-level baseline for the ~174 pre-existing findings (all PULSE_UNNECESSARY_COPY*/PULSE_RESOURCE_LEAK/PULSE_CONST_REFABLE, mostly in test code) triaged in run https://github.com/nlohmann/json/actions/runs/35829411620 on commit 1054b2097, so CI fails only on a NEW issue type. Remove an entry here once its findings have been fixed or explicitly accepted.", + "_comment_pulse": "Pulse stops exploring paths after pulse-max-disjuncts (default 20). With the default, basic_json::replace_value() (destroy + assert_invariant) exceeds the limit, Pulse loses the stored type, and reports false NULLPTR_DEREFERENCE findings for get_ptr() results in tests/src/unit-pointer_access.cpp.", + "pulse-max-disjuncts": 40, "fail-on-issue": true, "disable-issue-type": [ "PULSE_UNNECESSARY_COPY_ASSIGNMENT", diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index 8a26a451c..c4adf47a6 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -931,6 +931,8 @@ class binary_writer */ JSON_HEDLEY_NO_RETURN static void throw_on_discarded(const BasicJsonType& j, const char* format_name) { + static_cast(j); // unused when JSON_NOEXCEPTION is defined + static_cast(format_name); JSON_THROW(type_error::create(321, concat("cannot serialize discarded value to ", format_name), &j)); } diff --git a/include/nlohmann/ordered_map.hpp b/include/nlohmann/ordered_map.hpp index 2e0970726..74539d376 100644 --- a/include/nlohmann/ordered_map.hpp +++ b/include/nlohmann/ordered_map.hpp @@ -259,6 +259,9 @@ public: for (auto it = first; std::next(it, elements_affected) != Container::end(); ++it) { + // false positive: Infer's model of std::string keeps the buffer of a + // moved-from string, so it assumes a buffer is destroyed twice + // @infer-ignore USE_AFTER_DELETE it->~value_type(); // destroy but keep allocation new (&*it) value_type{std::move(*std::next(it, elements_affected))}; // "move" next element to it } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index b7d344b72..d0e43e79a 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -22402,6 +22402,8 @@ class binary_writer */ JSON_HEDLEY_NO_RETURN static void throw_on_discarded(const BasicJsonType& j, const char* format_name) { + static_cast(j); // unused when JSON_NOEXCEPTION is defined + static_cast(format_name); JSON_THROW(type_error::create(321, concat("cannot serialize discarded value to ", format_name), &j)); } @@ -27136,6 +27138,9 @@ public: for (auto it = first; std::next(it, elements_affected) != Container::end(); ++it) { + // false positive: Infer's model of std::string keeps the buffer of a + // moved-from string, so it assumes a buffer is destroyed twice + // @infer-ignore USE_AFTER_DELETE it->~value_type(); // destroy but keep allocation new (&*it) value_type{std::move(*std::next(it, elements_affected))}; // "move" next element to it } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index de415897f..e092616b7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -138,9 +138,12 @@ json_test_set_test_options(test-disabled_exceptions # only the #972 regression test needs thirdparty/fifo_map on its include path json_test_set_test_options(test-regression1 LINK_LIBRARIES fifo_map_include) -# GCC's false -Warray-bounds error with JSON_DIAGNOSTICS only shows up when optimizing (#5742) +# GCC's false -Warray-bounds error with JSON_DIAGNOSTICS only shows up when optimizing (#5742). +# -O3 makes the optimizer-driven warnings of the ci_test_gcc flag set (-Winline, +# -Wsuggest-attribute=...) fire on the library's inline functions; they are not +# what this test checks, so turn them off for it. json_test_set_test_options(test-diagnostics-optimized - COMPILE_OPTIONS $<$:-O3 -Werror=array-bounds> + COMPILE_OPTIONS $<$:-O3 -Werror=array-bounds -Wno-inline -Wno-suggest-attribute=pure -Wno-suggest-attribute=const> ) ############################################################################# diff --git a/tests/src/unit-diagnostics-optimized.cpp b/tests/src/unit-diagnostics-optimized.cpp index a020c2fe9..44473982e 100644 --- a/tests/src/unit-diagnostics-optimized.cpp +++ b/tests/src/unit-diagnostics-optimized.cpp @@ -34,6 +34,10 @@ using nlohmann::json; #include #include +// the static table in to_json has an exit-time destructor +DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors") + namespace { enum class diag_color @@ -55,7 +59,7 @@ void to_json(json& j, const diag_color& c) { return p.first == c; }); - j = it->second; + j = ((it != std::end(m)) ? it : std::begin(m))->second; // like NLOHMANN_JSON_SERIALIZE_ENUM } } // namespace @@ -78,3 +82,5 @@ TEST_CASE("diagnostics with optimization") CHECK_THROWS_WITH_AS(j[1].get(), "[json.exception.type_error.302] (/1) type must be number, but is string", json::type_error); } } + +DOCTEST_CLANG_SUPPRESS_WARNING_POP diff --git a/tests/src/unit-regression2.cpp b/tests/src/unit-regression2.cpp index eb140f157..2195c4714 100644 --- a/tests/src/unit-regression2.cpp +++ b/tests/src/unit-regression2.cpp @@ -1035,7 +1035,7 @@ TEST_CASE("regression test #5135 - destructor never allocates, even under memory failing_allocator_allocations = 0; failing_allocator_deallocations = 0; { - failing_json j = failing_json::array( + const failing_json j = failing_json::array( { failing_json::array({1, 2}), failing_json::object({{"key", failing_json::array({3})}}) @@ -1052,7 +1052,7 @@ TEST_CASE("regression test #5135 - destructor never allocates, even under memory { std::size_t allocations_before = 0; { - failing_json j = make_deep_nest(100000, false); + const auto j = make_deep_nest(100000, false); allocations_before = failing_allocator_allocations; fail_next_allocation = true; } @@ -1066,7 +1066,7 @@ TEST_CASE("regression test #5135 - destructor never allocates, even under memory { std::size_t allocations_before = 0; { - failing_json j = make_deep_nest(100000, true); + const auto j = make_deep_nest(100000, true); allocations_before = failing_allocator_allocations; fail_next_allocation = true; } @@ -1080,7 +1080,7 @@ TEST_CASE("regression test #5135 - destructor never allocates, even under memory { std::size_t allocations_before = 0; { - failing_ordered_json j = make_deep_nest(100000, true); + const auto j = make_deep_nest(100000, true); allocations_before = failing_allocator_allocations; fail_next_allocation = true; } @@ -1138,7 +1138,7 @@ BasicJsonType make_single_chain(std::size_t depth) template void check_destroy_edge_case(const BasicJsonType& value) { - const BasicJsonType copy = value; + const BasicJsonType copy = value; // NOLINT(performance-unnecessary-copy-initialization): the copy is the point CHECK(copy == value); } } // namespace @@ -1183,7 +1183,7 @@ TEST_CASE_TEMPLATE("regression test #5135 - destroy() edge cases", BasicJsonType SECTION("single-element chain, 1000 levels deep") { - BasicJsonType root = make_single_chain(1000); + auto root = make_single_chain(1000); check_destroy_edge_case(root); } @@ -1220,7 +1220,7 @@ TEST_CASE_TEMPLATE("regression test #5135 - destroy() edge cases", BasicJsonType SECTION("destruction via assignment on a deep tree") { - BasicJsonType root = make_single_chain(2000); + auto root = make_single_chain(2000); // assigning a new value destroys the old one in place root = nullptr; CHECK(root.is_null()); diff --git a/tests/src/unit-regression3.cpp b/tests/src/unit-regression3.cpp index 712be6b1a..69a95fb9b 100644 --- a/tests/src/unit-regression3.cpp +++ b/tests/src/unit-regression3.cpp @@ -22,6 +22,14 @@ // scoped enum, so get() (needed below to get>() // from a plain JSON array, not just from an already-binary value) relies on // enum serialization being enabled +// capture whether JSON_DELETE_DEPRECATED_FUNCTIONS was enabled on the command +// line *before* including json.hpp, since the library #undefs it once the header +// has been fully processed (see include/nlohmann/detail/macro_unscope.hpp); the +// tests of deprecated functions are skipped if these functions are deleted +#if defined(JSON_DELETE_DEPRECATED_FUNCTIONS) && (JSON_DELETE_DEPRECATED_FUNCTIONS == 1) + #define JSON_TEST_DEPRECATED_FUNCTIONS_DELETED +#endif + #if defined(JSON_DISABLE_ENUM_SERIALIZATION) && (JSON_DISABLE_ENUM_SERIALIZATION == 1) #define SKIP_TESTS_FOR_ENUM_SERIALIZATION #endif @@ -887,19 +895,23 @@ TEST_CASE("issue #5676 - SAX parsing of CBOR tags") true, false, false, json::cbor_tag_handler_t::store)); CHECK(iterator_parsed == expected); +#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED json span_parsed; nlohmann::detail::json_sax_dom_parser span_sax(span_parsed); CHECK(json::sax_parse(nlohmann::detail::span_input_adapter(cbor.data(), cbor.size()), &span_sax, json::input_format_t::cbor, true, false, false, json::cbor_tag_handler_t::store)); CHECK(span_parsed == expected); +#endif const std::string text = "null"; CHECK(json::sax_parse(text, &acceptor, json::input_format_t::json, true, false, false, json::cbor_tag_handler_t::store)); CHECK(json::sax_parse(text.begin(), text.end(), &acceptor, json::input_format_t::json, true, false, false, json::cbor_tag_handler_t::store)); +#ifndef JSON_TEST_DEPRECATED_FUNCTIONS_DELETED CHECK(json::sax_parse(nlohmann::detail::span_input_adapter(text.data(), text.size()), &acceptor, json::input_format_t::json, true, false, false, json::cbor_tag_handler_t::store)); +#endif } TEST_CASE("issue #5402 - update(merge_objects=true) overwrites a primitive with an object") diff --git a/tests/src/unit-serialization.cpp b/tests/src/unit-serialization.cpp index 9d7c41b33..19a9920b9 100644 --- a/tests/src/unit-serialization.cpp +++ b/tests/src/unit-serialization.cpp @@ -945,8 +945,15 @@ TEST_CASE("serialization boundary values for the string buffer") // newline escaping does not depend on ensure_ascii: only the // emoji differs (raw UTF-8 bytes vs. a \u-escaped surrogate pair) - CHECK(j.dump(-1, ' ', false) == '"' + expected_prefix + emoji + '"'); - CHECK(j.dump(-1, ' ', true) == '"' + expected_prefix + "\\ud83d\\ude00\""); + std::string expected_raw = "\""; + expected_raw += expected_prefix; + expected_raw += emoji; + expected_raw += '"'; + std::string expected_ascii = "\""; + expected_ascii += expected_prefix; + expected_ascii += R"(\ud83d\ude00")"; + CHECK(j.dump(-1, ' ', false) == expected_raw); + CHECK(j.dump(-1, ' ', true) == expected_ascii); CHECK(json::parse(j.dump(-1, ' ', true)) == j); CHECK(json::parse(j.dump(-1, ' ', false)) == j); }