diff --git a/.github/workflows/check_amalgamation.yml b/.github/workflows/check_amalgamation.yml index 5a7efb07f..649d26907 100644 --- a/.github/workflows/check_amalgamation.yml +++ b/.github/workflows/check_amalgamation.yml @@ -67,7 +67,9 @@ jobs: python3 $TOOL_DIR/amalgamate.py -c $TOOL_DIR/config_json.json -s . python3 $TOOL_DIR/amalgamate.py -c $TOOL_DIR/config_json_fwd.json -s . - python3 $TOOL_DIR/amalgamate.py -c $TOOL_DIR/config_json_view.json -s . + # the configuration of json_view.hpp comes with the pull request until + # it is on develop; the tool itself is still develop's + python3 $TOOL_DIR/amalgamate.py -c $MAIN_DIR/tools/amalgamate/config_json_view.json -s . # the header list of the Bazel "json" target must match the files in include/ cmake -P cmake/scripts/gen_bazel_build_file.cmake diff --git a/docs/mkdocs/docs/examples/basic_json_view__cbegin.cpp b/docs/mkdocs/docs/examples/basic_json_view__cbegin.cpp index 811db6267..4e67a8041 100644 --- a/docs/mkdocs/docs/examples/basic_json_view__cbegin.cpp +++ b/docs/mkdocs/docs/examples/basic_json_view__cbegin.cpp @@ -15,7 +15,7 @@ int main() const auto values = measurements.root(); const int sum = std::accumulate(values.cbegin(), values.cend(), 0, - [](int total, const json_view & v) + [](int total, const json_view & v) { return total + v.materialize().get(); }); diff --git a/docs/mkdocs/docs/examples/basic_json_view__cend.cpp b/docs/mkdocs/docs/examples/basic_json_view__cend.cpp index 2999bd684..c72990673 100644 --- a/docs/mkdocs/docs/examples/basic_json_view__cend.cpp +++ b/docs/mkdocs/docs/examples/basic_json_view__cend.cpp @@ -15,7 +15,7 @@ int main() const auto records = batch.root(); const bool all_objects = std::all_of(records.cbegin(), records.cend(), - [](const json_view & v) + [](const json_view & v) { return v.is_object(); }); diff --git a/docs/mkdocs/docs/examples/basic_json_view__dump.cpp b/docs/mkdocs/docs/examples/basic_json_view__dump.cpp index 7a00c06d2..c9bd8198f 100644 --- a/docs/mkdocs/docs/examples/basic_json_view__dump.cpp +++ b/docs/mkdocs/docs/examples/basic_json_view__dump.cpp @@ -19,7 +19,7 @@ int main() // the source text; a json value's object_t is std::map, so // materialize().dump() of the very same view sorts the keys instead const json_document config = json_document::parse( - R"({"name": "cache", "host": "db1", "port": 6379, "timeout": 30})"); + R"({"name": "cache", "host": "db1", "port": 6379, "timeout": 30})"); std::cout << config.root().dump(2) << "\n\n"; std::cout << config.root().materialize().dump(2) << '\n'; } diff --git a/docs/mkdocs/docs/examples/basic_json_view__operator_eq.cpp b/docs/mkdocs/docs/examples/basic_json_view__operator_eq.cpp index ac59838f4..846d40fc0 100644 --- a/docs/mkdocs/docs/examples/basic_json_view__operator_eq.cpp +++ b/docs/mkdocs/docs/examples/basic_json_view__operator_eq.cpp @@ -10,9 +10,9 @@ int main() // directly as views, without ever building a nlohmann::json value for // either one const json_document previous = json_document::parse( - R"({"name": "cache", "port": 6379, "timeout": 30})"); + R"({"name": "cache", "port": 6379, "timeout": 30})"); const json_document current = json_document::parse( - R"({"port": 6379.0, "timeout": 30, "name": "cache"})"); + R"({"port": 6379.0, "timeout": 30, "name": "cache"})"); // same members, reordered, and 6379 written as a float -- operator== // treats them the same way BasicJsonType::operator== would @@ -20,7 +20,7 @@ int main() // an actually changed value is detected the same way const json_document changed = json_document::parse( - R"({"name": "cache", "port": 6380, "timeout": 30})"); + R"({"name": "cache", "port": 6380, "timeout": 30})"); std::cout << (previous.root() == changed.root()) << '\n'; // comparing a view directly against an expected json value -- handy in a diff --git a/docs/mkdocs/docs/examples/basic_json_view__operator_ne.cpp b/docs/mkdocs/docs/examples/basic_json_view__operator_ne.cpp index e284ae1a1..faa1d61cf 100644 --- a/docs/mkdocs/docs/examples/basic_json_view__operator_ne.cpp +++ b/docs/mkdocs/docs/examples/basic_json_view__operator_ne.cpp @@ -11,7 +11,7 @@ int main() // unwanted shape -- without ever materializing it into a json value just // to compare const json_document received = json_document::parse( - R"({"status": "ok", "code": 200})"); + R"({"status": "ok", "code": 200})"); const json unwanted = {{"status", "error"}, {"code", 500}}; std::cout << std::boolalpha << (received.root() != unwanted) << '\n'; diff --git a/docs/mkdocs/docs/examples/basic_json_view__type_name.cpp b/docs/mkdocs/docs/examples/basic_json_view__type_name.cpp index 9d59792ca..30661c0fd 100644 --- a/docs/mkdocs/docs/examples/basic_json_view__type_name.cpp +++ b/docs/mkdocs/docs/examples/basic_json_view__type_name.cpp @@ -13,7 +13,10 @@ int main() json_document bad = json_document::parse("[1, 2, 3]"); json_document failed = json_document::parse("not json", /* allow_exceptions */ false); - for (const json_view v : { good.root(), bad.root(), failed.root() }) + for (const json_view v : + { + good.root(), bad.root(), failed.root() + }) { if (v.is_object()) { diff --git a/include/nlohmann/detail/view/builder.hpp b/include/nlohmann/detail/view/builder.hpp index 7856fb642..9500c67da 100644 --- a/include/nlohmann/detail/view/builder.hpp +++ b/include/nlohmann/detail/view/builder.hpp @@ -195,8 +195,9 @@ class builder { const std::uint64_t done = static_cast(at - b) + 1; const std::uint64_t guess = static_cast(n) * static_cast(e - b + 1) / done; + const std::uint64_t grown = guess + (guess / 4) + 64; // a variable: GCC calls a cast of the sum useless where std::uint64_t is std::size_t doc.tape_size = n; - doc.reserve((std::max)(static_cast(guess + (guess / 4) + 64), n + (n / 2) + 64)); + doc.reserve((std::max)(static_cast(grown), n + (n / 2) + 64)); return doc.tape; } @@ -418,6 +419,14 @@ class builder return w; } + /// a compile-time option as a runtime condition: testing the template + /// argument directly makes a condition like `TrailingCommas && c == ']'` + /// constant when the option is off, which MSVC reports as C4127 + static NLOHMANN_VIEW_ALWAYS_INLINE bool enabled(bool option) noexcept + { + return option; + } + /// The parse state and the parser proper. The cursor is a local object of /// run() whose address never escapes (everything it calls out of line is a /// member of the builder and gets the positions it needs), so that the @@ -547,7 +556,7 @@ arr_next: { return false; } - if (TrailingCommas && cur() == ']') + if (enabled(TrailingCommas) && cur() == ']') { ++p; goto close_container; @@ -614,7 +623,7 @@ obj_next: { return false; } - if (TrailingCommas && cur() == '}') + if (enabled(TrailingCommas) && cur() == '}') { ++p; goto close_container; @@ -756,7 +765,7 @@ indent_done: { ++p; } - if (Comments && cur() == '/') + if (enabled(Comments) && cur() == '/') { const unsigned char* const q = cold.comment(p); if (q == nullptr) diff --git a/include/nlohmann/detail/view/pointer.hpp b/include/nlohmann/detail/view/pointer.hpp index 2c7b1e4db..d2ac497d7 100644 --- a/include/nlohmann/detail/view/pointer.hpp +++ b/include/nlohmann/detail/view/pointer.hpp @@ -69,7 +69,7 @@ index_status array_index(const StringType& s, std::size_t& idx) noexcept } v = (v * 10) + d; } - if (v >= static_cast((std::numeric_limits::max)())) + if (v >= (std::numeric_limits::max)()) // (std::size_t converts to std::uint64_t implicitly) { return index_status::too_large; } diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 6ba1f70df..92fe5a09c 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -1046,8 +1046,9 @@ class builder { const std::uint64_t done = static_cast(at - b) + 1; const std::uint64_t guess = static_cast(n) * static_cast(e - b + 1) / done; + const std::uint64_t grown = guess + (guess / 4) + 64; // a variable: GCC calls a cast of the sum useless where std::uint64_t is std::size_t doc.tape_size = n; - doc.reserve((std::max)(static_cast(guess + (guess / 4) + 64), n + (n / 2) + 64)); + doc.reserve((std::max)(static_cast(grown), n + (n / 2) + 64)); return doc.tape; } @@ -1269,6 +1270,14 @@ class builder return w; } + /// a compile-time option as a runtime condition: testing the template + /// argument directly makes a condition like `TrailingCommas && c == ']'` + /// constant when the option is off, which MSVC reports as C4127 + static NLOHMANN_VIEW_ALWAYS_INLINE bool enabled(bool option) noexcept + { + return option; + } + /// The parse state and the parser proper. The cursor is a local object of /// run() whose address never escapes (everything it calls out of line is a /// member of the builder and gets the positions it needs), so that the @@ -1398,7 +1407,7 @@ arr_next: { return false; } - if (TrailingCommas && cur() == ']') + if (enabled(TrailingCommas) && cur() == ']') { ++p; goto close_container; @@ -1465,7 +1474,7 @@ obj_next: { return false; } - if (TrailingCommas && cur() == '}') + if (enabled(TrailingCommas) && cur() == '}') { ++p; goto close_container; @@ -1607,7 +1616,7 @@ indent_done: { ++p; } - if (Comments && cur() == '/') + if (enabled(Comments) && cur() == '/') { const unsigned char* const q = cold.comment(p); if (q == nullptr) @@ -3185,7 +3194,7 @@ index_status array_index(const StringType& s, std::size_t& idx) noexcept } v = (v * 10) + d; } - if (v >= static_cast((std::numeric_limits::max)())) + if (v >= (std::numeric_limits::max)()) // (std::size_t converts to std::uint64_t implicitly) { return index_status::too_large; } diff --git a/tests/src/unit-class_lexer.cpp b/tests/src/unit-class_lexer.cpp index e16bf0bf1..e476c08c3 100644 --- a/tests/src/unit-class_lexer.cpp +++ b/tests/src/unit-class_lexer.cpp @@ -1465,7 +1465,8 @@ TEST_CASE("Eisel-Lemire float conversion") // than the distance to the rounding boundary, so it must not change std::string longer = token; const std::size_t e = longer.find('e'); - const std::string extra = longer.find('.') == std::string::npos ? ".000000000000000000001" : "000000000000000000001"; + const std::size_t dot = longer.find('.'); + const std::string extra = dot == std::string::npos ? ".000000000000000000001" : "000000000000000000001"; longer.insert(e == std::string::npos ? longer.size() : e, extra); CAPTURE(longer); if (eisel_lemire(longer, out)) @@ -1488,7 +1489,8 @@ TEST_CASE("Eisel-Lemire float conversion") CHECK(bits_of(json::parse("-65.613616999999977").get()) == bits_of(-65.613616999999977)); CHECK(bits_of(json::parse("2.2250738585072011e-308").get()) == 0x000FFFFFFFFFFFFFu); CHECK(bits_of(json::parse("4.9406564584124654e-324").get()) == 1u); - CHECK_THROWS_WITH_AS(json::parse("1.7976931348623159e308"), + json _; + CHECK_THROWS_WITH_AS(_ = json::parse("1.7976931348623159e308"), "[json.exception.out_of_range.406] number overflow parsing '1.7976931348623159e308'", json::out_of_range&); } } @@ -1555,16 +1557,23 @@ TEST_CASE("string scanning kernels") state ^= state << 17u; return state; }; + // the upper half as a 32-bit value: converts to std::size_t implicitly on + // every platform (a cast of std::uint64_t is useless where both are the + // same type, and required where std::size_t is 32 bits wide) + const auto next_small = [&next]() + { + return static_cast(next() >> 32u); + }; for (int round = 0; round < 100000; ++round) { // mostly ordinary text, so that runs span several words - std::string text(static_cast(next() % 8), '.'); - const auto count = static_cast(next() % 12); + std::string text(next_small() % 8u, '.'); + const std::size_t count = next_small() % 12u; for (std::size_t k = 0; k < count; ++k) { - const std::size_t p = (next() % 4 == 0) ? static_cast(next() % pieces.size()) : 0; + const std::size_t p = (next() % 4 == 0) ? next_small() % pieces.size() : 0; text += pieces[p]; - text += std::string(static_cast(next() % 10), 'x'); + text += std::string(next_small() % 10u, 'x'); } const auto* data = reinterpret_cast(text.data()); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) for (std::size_t offset = 0; offset < 3 && offset <= text.size(); ++offset) diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp index a6d34d5a1..f98c97f3a 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -39,6 +39,7 @@ using nlohmann::ordered_json_view; namespace { +#if !defined(JSON_NOEXCEPTION) // the exception parse() throws for a text, or "" if it accepts it std::string parse_exception(const std::string& text, bool comments = false, bool trailing_commas = false) { @@ -67,6 +68,7 @@ std::string view_exception(const std::string& text, bool comments = false, bool } return ""; } +#endif // a small deterministic generator of documents struct generator @@ -156,7 +158,7 @@ TEST_CASE("json_view") CHECK(v.materialize() == j); } - const json_view invalid; + const json_view invalid{}; CHECK(invalid.is_discarded()); CHECK(!static_cast(invalid)); CHECK(invalid.type() == json::value_t::discarded); @@ -201,9 +203,11 @@ TEST_CASE("json_view") }) { CAPTURE(text); +#if !defined(JSON_NOEXCEPTION) const std::string expected = parse_exception(text); REQUIRE(!expected.empty()); CHECK(view_exception(text) == expected); +#endif CHECK(!json_document::accept(text)); const json_document d = json_document::parse(text, false); CHECK(d.is_discarded()); @@ -211,8 +215,9 @@ TEST_CASE("json_view") CHECK(d.node_count() == 0); } // the exception types - CHECK_THROWS_AS(json_document::parse("[1,"), json::parse_error&); - CHECK_THROWS_AS(json_document::parse("1e400"), json::out_of_range&); + json_document _; + CHECK_THROWS_AS(_ = json_document::parse("[1,"), json::parse_error&); + CHECK_THROWS_AS(_ = json_document::parse("1e400"), json::out_of_range&); } SECTION("parse options") @@ -227,7 +232,9 @@ TEST_CASE("json_view") const bool comments = (options & 1) != 0; const bool trailing_commas = (options & 2) != 0; CHECK(json_document::accept(text, comments, trailing_commas) == json::accept(text, comments, trailing_commas)); +#if !defined(JSON_NOEXCEPTION) CHECK(view_exception(text, comments, trailing_commas) == parse_exception(text, comments, trailing_commas)); +#endif } } } @@ -239,7 +246,9 @@ TEST_CASE("json_view") const std::string nul_in_comment("[1, // c\0\n2]", 12); CHECK(json_document::accept(nul_in_comment, true) == json::accept(nul_in_comment, true)); CHECK(json_document::parse("\xEF\xBB\xBF[1]").root().materialize() == json::parse("\xEF\xBB\xBF[1]")); +#if !defined(JSON_NOEXCEPTION) CHECK(view_exception("\xEF\xBB") == parse_exception("\xEF\xBB")); +#endif } SECTION("inputs") @@ -385,6 +394,7 @@ TEST_CASE("json_view") namespace { +#if !defined(JSON_NOEXCEPTION) // the exception a call throws, or "" if it throws none template std::string exception_of(F f) @@ -399,6 +409,7 @@ std::string exception_of(F f) } return ""; } +#endif // compares a view with the ordered_json value materialize() gives for it: // types, sizes, elements and members (by index, key, and iteration), in @@ -567,6 +578,7 @@ TEST_CASE("json_view element access and iteration") const json_document d = json_document::parse(text); const json_view v = d.root(); const json j = v.materialize(); +#if !defined(JSON_NOEXCEPTION) if (!j.is_object()) { CHECK(exception_of([&] { static_cast(v["a"]); }) == exception_of([&] { static_cast(j["a"]); })); @@ -588,6 +600,7 @@ TEST_CASE("json_view element access and iteration") CHECK(exception_of([&] { static_cast(v.front()); }) == exception_of([&] { static_cast(j.front()); })); CHECK(exception_of([&] { static_cast(v.back()); }) == exception_of([&] { static_cast(j.back()); })); } +#endif CHECK(v.contains("a") == j.contains("a")); CHECK(v.count("a") == j.count("a")); CHECK((v.find("a") == v.end()) == (j.find("a") == j.end())); // NOLINT(readability-container-contains): find() is what is tested @@ -599,7 +612,7 @@ TEST_CASE("json_view element access and iteration") CHECK(!d.root()["a"][0]); CHECK_THROWS_WITH_AS(d.root()["a"].front(), "[json.exception.invalid_iterator.214] cannot get value", json::invalid_iterator&); CHECK_THROWS_WITH_AS(d.root()["a"].back(), "[json.exception.invalid_iterator.214] cannot get value", json::invalid_iterator&); - const json_view invalid; + const json_view invalid{}; CHECK(invalid.begin() == invalid.end()); CHECK(std::string(invalid.type_name()) == "discarded"); CHECK_THROWS_WITH_AS(invalid["a"], "[json.exception.type_error.305] cannot use operator[] with a string argument with discarded", json::type_error&); @@ -659,6 +672,7 @@ TEST_CASE("json_view element access and iteration") namespace { +#if !defined(JSON_NOEXCEPTION) // an exception message without the context that basic_json adds with // JSON_DIAGNOSTICS ("(/path) ") and JSON_DIAGNOSTIC_POSITIONS ("(bytes 1-2) "); // the view's exceptions have no such context @@ -676,6 +690,7 @@ std::string without_path(std::string msg) } return msg; } +#endif // the bits of a float, to compare values bit for bit std::uint64_t bits(double x) @@ -755,6 +770,7 @@ void check_values(const ordered_json_view& v, const ordered_json& j, const std:: break; } +#if !defined(JSON_NOEXCEPTION) // conversions to the wrong type throw what basic_json throws if (!j.is_number()) { @@ -771,6 +787,7 @@ void check_values(const ordered_json_view& v, const ordered_json& j, const std:: { CHECK(exception_of([&] { static_cast(v.get>()); }) == without_path(exception_of([&] { static_cast(j.get>()); }))); } +#endif if (v.is_array()) { @@ -931,7 +948,7 @@ TEST_CASE("json_view values") // a duplicate key: the last value, as parse() CHECK((json_document::parse(R"({"a":1,"a":2})").root().get>() == std::map {{"a", 2}})); - const json_view invalid; + const json_view invalid{}; CHECK_THROWS_WITH_AS(invalid.get(), "[json.exception.type_error.302] type must be number, but is discarded", json::type_error&); CHECK(invalid.get().is_discarded()); } @@ -952,10 +969,12 @@ TEST_CASE("json_view values") // with a JSON pointer, arrays can be asked as well CHECK(v["o"]["x"].value(json::json_pointer("/1"), 0) == j["o"]["x"].value(json::json_pointer("/1"), 0)); CHECK(v["o"]["x"].value(json::json_pointer("/7"), 3) == j["o"]["x"].value(json::json_pointer("/7"), 3)); +#if !defined(JSON_NOEXCEPTION) CHECK(exception_of([&] { static_cast(v["o"]["x"].value("k", 0)); }) == without_path(exception_of([&] { static_cast(j["o"]["x"].value("k", 0)); }))); CHECK(exception_of([&] { static_cast(v.value("s", 0)); }) == without_path(exception_of([&] { static_cast(j.value("s", 0)); }))); CHECK(exception_of([&] { static_cast(v["n"].value("x", 0)); }) == without_path(exception_of([&] { static_cast(j["n"].value("x", 0)); }))); CHECK(exception_of([&] { static_cast(v["n"].value(json::json_pointer("/x"), 0)); }) == without_path(exception_of([&] { static_cast(j["n"].value(json::json_pointer("/x"), 0)); }))); +#endif } } @@ -994,6 +1013,7 @@ TEST_CASE("json_view JSON pointers") } } +#if !defined(JSON_NOEXCEPTION) SECTION("errors are those of basic_json") { const std::string text = R"({"a": [1, {"b": null}], "c": "s", "": {"": 0}, "a~b": 1, "c/d": 2})"; @@ -1026,11 +1046,16 @@ TEST_CASE("json_view JSON pointers") // (basic_json::contains() throws out_of_range.404 for an empty // array index token, although it is not meant to throw; the view // answers false) - const std::string contains_error = exception_of([&] { static_cast(j.contains(p)); }); + const std::string contains_error = exception_of([&] + { + const bool found = j.contains(p); + static_cast(found); + }); CHECK(v.contains(p) == (contains_error.empty() && j.contains(p))); CHECK(exception_of([&] { static_cast(v.value(p, 5)); }) == without_path(exception_of([&] { static_cast(j.value(p, 5)); }))); } } +#endif } TEST_CASE("json_view dump") diff --git a/tests/src/unit-json_view_builder.cpp b/tests/src/unit-json_view_builder.cpp index 270e19771..5c676479a 100644 --- a/tests/src/unit-json_view_builder.cpp +++ b/tests/src/unit-json_view_builder.cpp @@ -9,8 +9,12 @@ #include "doctest_compatibility.h" #include -#include -#include +#if JSON_TEST_USING_MULTIPLE_HEADERS + #include + #include +#else + #include // the single header contains the internal headers +#endif using nlohmann::json; #include