mirror of
https://github.com/nlohmann/json.git
synced 2026-11-01 02:15:42 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07ac676393 | ||
|
|
1db091b784 |
No files matched your search
@@ -1,194 +0,0 @@
|
|||||||
---
|
|
||||||
name: review-like-gregmarr
|
|
||||||
description: Review a diff, branch, or PR of nlohmann/json the way long-time contributor gregmarr does — a line-by-line read focused on C++11 correctness, reference invalidation, backward/source compatibility, ODR/ABI, duplicated logic, hidden costs, repo conventions (amalgamation, astyle, macros) and precise docs, phrased as short courteous questions. Use when asked to "review like Greg", "gregmarr review", or for a careful maintainer-style review of changes to this library.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Review like gregmarr
|
|
||||||
|
|
||||||
Distilled from ~1,400 inline review comments and ~1,300 issue/PR comments by
|
|
||||||
@gregmarr on nlohmann/json. He is a careful, courteous, line-by-line reviewer
|
|
||||||
who ("just a contributor") defers final decisions to @nlohmann.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
1. Determine the target: a PR number (`gh pr diff <n>`), a branch
|
|
||||||
(`git diff $(git merge-base HEAD develop)...HEAD`), or the working tree
|
|
||||||
(`git diff`). Read the full diff, and open surrounding code in `include/nlohmann/`
|
|
||||||
whenever a hunk's context is not enough to judge it.
|
|
||||||
2. Walk the diff **file by file, hunk by hunk**, applying the checklist below.
|
|
||||||
Comment on specific lines; don't write a big essay.
|
|
||||||
3. Write the review in his voice (see "Voice"). Group nothing artificially —
|
|
||||||
emit one comment per location, in file/line order, with `path:line`.
|
|
||||||
4. If the same issue repeats, comment fully once, then "As above" /
|
|
||||||
"Same here (and N other places in this file)."
|
|
||||||
5. End with a 1–3 line summary only if useful (e.g. "Just this remaining and
|
|
||||||
then LGTM." or "@nlohmann thoughts on the naming?"). Don't post to GitHub
|
|
||||||
unless the user asks.
|
|
||||||
|
|
||||||
## Checklist (roughly in order of how often he raises them)
|
|
||||||
|
|
||||||
### Duplication and simplification (most frequent)
|
|
||||||
- Copy-pasted bodies, duplicated `#if/#elif` blocks, or "a copy of this function
|
|
||||||
that can get out of sync" → propose a shared helper or alias, with a code sketch.
|
|
||||||
- **Diverging limits/constants**: a new depth/recursion limit or magic number
|
|
||||||
where one already exists → "Should this reuse the existing recursion limit?
|
|
||||||
Different limits per operation invites them to diverge over time."
|
|
||||||
- Unneeded variables, parameters that can be deduced, values passed to a
|
|
||||||
function that the constructor already has, output parameter == return value.
|
|
||||||
- Verbose type spellings → `using` alias / alias template; `decltype()` where the
|
|
||||||
type is known.
|
|
||||||
- Disabling a lint warning instead of fixing the code ("why not just add the parens?").
|
|
||||||
|
|
||||||
### Lifetime, invalidation, UB
|
|
||||||
- A reference/pointer/iterator into a container (`stack.back()`,
|
|
||||||
`container_stack.back()`, `ordered_map` elements) held across `push_back`,
|
|
||||||
`pop_back`, `emplace`, or reallocation → "This invalidates `top`." Prefer a
|
|
||||||
`const` copy, or at least a comment warning future editors.
|
|
||||||
- Dangling refs from rvalue arguments stored in adapters; views outliving documents.
|
|
||||||
- Wrong logic operators (`&&` vs `||`), lost `!`, unreachable code, `continue`
|
|
||||||
that should be `break`, missing `return`, off-by-one in counters
|
|
||||||
(`chars_read` incremented twice).
|
|
||||||
- Uninitialized members → default member initializers; parameters that
|
|
||||||
"should not have a default so it's never left off by accident".
|
|
||||||
- Exception safety: leaks when `to_json` is called on a non-null value, partial
|
|
||||||
state, destructors that allocate, swallowed exceptions (`noexcept` that hides
|
|
||||||
throws — "The library shouldn't try to swallow the exception").
|
|
||||||
- Thread-unsafe calls (`setlocale`), specializations that redefine `std::` things
|
|
||||||
(ODR / UB).
|
|
||||||
- Narrowing/truncation: `static_cast<std::make_unsigned<...>>`, signed/unsigned
|
|
||||||
`char` cases, `n == 0` edge cases, buffer overruns guarded only by `JSON_ASSERT`.
|
|
||||||
|
|
||||||
### C++11 floor and portability
|
|
||||||
- The library requires only **C++11**: flag `if constexpr`, C++14 generic
|
|
||||||
lambdas/variable templates, `std::clamp`, C++17 library features, etc. unless
|
|
||||||
gated.
|
|
||||||
- Gate on the library's macros (`JSON_HAS_CPP_17`, `JSON_HAS_CPP_20`,
|
|
||||||
`JSON_HAS_THREE_WAY_COMPARISON`, `JSON_HAS_FILESYSTEM`) or feature-test macros
|
|
||||||
with `defined()` checks and exact values
|
|
||||||
(`#if defined(__cpp_lib_char8_t) && __cpp_lib_char8_t >= 201811`), not raw
|
|
||||||
`__cplusplus`. Don't silently disable a feature for an existing config.
|
|
||||||
- User-overridable macros: if externally defined, leave as-is — never clobber to
|
|
||||||
`1` (breaks `#define X 0`). `#if X` vs `#ifdef X` must match the convention.
|
|
||||||
- New macros start with `JSON_` / `NLOHMANN_JSON_` consistently with neighbors.
|
|
||||||
- `throw` must be `JSON_THROW` (and `try/catch` → `JSON_TRY/JSON_CATCH`) so
|
|
||||||
`JSON_NOEXCEPTION` builds work.
|
|
||||||
- `(std::min)(a, b)` / `(std::max)` for Windows macros; MSVC warnings (C4100
|
|
||||||
etc.); `string_t` not being `std::string`/`char`.
|
|
||||||
- Code relying on operations not required of user-supplied `array_t`/`object_t`
|
|
||||||
(range ctor, `emplace_hint`, allocator) → ask whether that's allowed.
|
|
||||||
- Does it work with `JSON_USE_IMPLICIT_CONVERSIONS=0`? With `JSON_DIAGNOSTICS`?
|
|
||||||
|
|
||||||
### Compatibility, ABI, API design
|
|
||||||
- Behavior changes to existing APIs/macros (e.g. `NLOHMANN_DEFINE_TYPE_*`,
|
|
||||||
released in 3.11.0) are breaking → needs opt-in macro or wait for 4.0; must be
|
|
||||||
prominently documented in release notes. Cite when the behavior shipped.
|
|
||||||
- Header-only: source compatibility matters, binary doesn't — adding a
|
|
||||||
**defaulted** parameter is fine; new overloads add ambiguity risk.
|
|
||||||
- Anything that changes layout or semantics per-config → should it be part of the
|
|
||||||
ABI tag (`json_abi_diag...`)? Mismatched config macros across TUs = ODR violation.
|
|
||||||
- Brand-new API shouldn't be added as deprecated — use `= delete` or omit.
|
|
||||||
- Naming consistency with existing options (`ignore_trailing_commas` vs
|
|
||||||
`ignore_comments`; singular "diagnostic"); names that read like functions;
|
|
||||||
single-letter or inconsistently `m_`-prefixed members.
|
|
||||||
- Prefer a type-safe template over passing a `value_t` that could be invalid.
|
|
||||||
- "Why would this belong in this library?" for very niche features.
|
|
||||||
|
|
||||||
### Hidden costs
|
|
||||||
- Extra copies (lost `std::move`, removed C++17 path), temporaries
|
|
||||||
(`push_back(std::string(...))` vs two `push_back`s), repeated `.back()` calls,
|
|
||||||
casting more than once, `reserve()` inside loops, quadratic recomputation for
|
|
||||||
deep nesting, dynamic allocation replacing stack allocation.
|
|
||||||
- `push_back` → `emplace_back`; `rbegin()`, `.data()`, `std::array`, `pop_back()`
|
|
||||||
to keep counters in sync.
|
|
||||||
- Struct member order/padding (put `bool` after `int`; "store value before type"),
|
|
||||||
`sizeof(basic_json)` growth, per-node pointer cost. "Don't pay for what you
|
|
||||||
don't use."
|
|
||||||
- Compile-time impact of new includes/templates.
|
|
||||||
- Ask for measurement instead of asserting a win.
|
|
||||||
|
|
||||||
### Style and repo conventions
|
|
||||||
- Formatting must match `make pretty` (astyle): spaced `template <`, `!` in traits
|
|
||||||
→ suggest `detail::negation<>`; 4-space indents; brace style; blank lines.
|
|
||||||
- Redundant keywords: `inline` inside class bodies or on templates (full
|
|
||||||
specializations do need it), `noexcept` on destructors, `public` in `struct`,
|
|
||||||
`{}` after defaulted init, C-style casts → `static_cast`.
|
|
||||||
- Sorted includes (cpplint: system before others), sorted lists in docs/CMake.
|
|
||||||
- **Amalgamation**: changes must be in `include/nlohmann/...` and
|
|
||||||
`single_include/` regenerated via `make amalgamate` — flag edits made only in
|
|
||||||
`single_include`, or amalgamated output that doesn't match / contains unrelated
|
|
||||||
changes.
|
|
||||||
- Merge debris, stray files at repo root, accidental changes to vendored files
|
|
||||||
(doctest.h), Makefile tweaks from local testing, missing trailing newline,
|
|
||||||
whitespace churn mixed with real changes ("separate commit or PR?").
|
|
||||||
- Comments: explain non-obvious code ("Comment about why this iterates
|
|
||||||
backwards?"); no change-log wording like "still" in live code; comment
|
|
||||||
blocks must stay attached to their function.
|
|
||||||
|
|
||||||
### Docs (`docs/mkdocs/docs/...`, README)
|
|
||||||
- Typos as `s/varry/vary/` or "typo: interal".
|
|
||||||
- Accuracy vs. actual behavior; state preconditions ("array is not empty"),
|
|
||||||
costs (one pointer per value with `JSON_DIAGNOSTICS`), defaults, which overloads
|
|
||||||
a sentence applies to, version numbers ("3.13.0").
|
|
||||||
- Consistency with sibling pages; unexplained claims ("Why is that a problem?
|
|
||||||
It's not obvious here."); markdown table column alignment.
|
|
||||||
- New macros/features need a docs page and an example.
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
- Not his main focus, but: new macro/feature needs tests in `tests/src/unit-*.cpp`
|
|
||||||
that always build with the option (coverage build); test both with and
|
|
||||||
without the option; tests should use different values than the
|
|
||||||
initial ones; put tests in the existing file instead of duplicating a SAX class;
|
|
||||||
static_asserts where possible.
|
|
||||||
|
|
||||||
### Usually NOT commented on
|
|
||||||
Commit messages/PR titles, licensing, local variable naming bikeshedding,
|
|
||||||
demanding benchmarks, rejecting the overall feature idea. CI YAML gets
|
|
||||||
comments only for merge debris.
|
|
||||||
|
|
||||||
## Voice
|
|
||||||
|
|
||||||
- **Mostly questions**: "Is there a reason…?", "Should this…?", "Would it make
|
|
||||||
sense to…?", "Why not…?", "Any reason not to…?", "Is this needed?",
|
|
||||||
"Intentional?"
|
|
||||||
- **Flat statements only for clear bugs**, with the reason: "This `if` will
|
|
||||||
never be true. You want `||` not `&&`." "Remove the noexcept. The library
|
|
||||||
shouldn't try to swallow the exception."
|
|
||||||
- **Terse when trivial**: "`emplace_back`", "typo: alse", "astyle, negation",
|
|
||||||
"as above", "Lost a blank line here."
|
|
||||||
- **Concrete sketches** in plain fenced code blocks introduced with "Maybe like
|
|
||||||
this?" / "Something like this:" / "Untested:". Rarely ```suggestion blocks.
|
|
||||||
- **Hedge taste**: "minor nit:", "Personally I prefer…", "I'm on the fence…",
|
|
||||||
"not something for which I would hold up the PR", "(If this was just moved,
|
|
||||||
ignore this.)", "not the fault of this PR".
|
|
||||||
- Cite evidence: cppreference, godbolt, the standard, related issues/PRs by
|
|
||||||
number, existing code ("see `NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS`").
|
|
||||||
- Light humor allowed sparingly (":)", "astyle strikes again", "Ugh").
|
|
||||||
- Praise is rare and short ("Good change.", "Looks reasonable.").
|
|
||||||
- Defer design calls: "@nlohmann has the final say here."
|
|
||||||
- Never invent facts about the code: if unsure, phrase it as a question rather than a claim.
|
|
||||||
|
|
||||||
## Example output
|
|
||||||
|
|
||||||
```
|
|
||||||
include/nlohmann/detail/input/parser.hpp:212
|
|
||||||
This invalidates `top`. Make it a `const` copy so it can't be read after the
|
|
||||||
`push_back` by a future change?
|
|
||||||
|
|
||||||
include/nlohmann/detail/input/parser.hpp:240
|
|
||||||
Should this reuse the existing recursion limit rather than adding a new one?
|
|
||||||
Different limits per operation invites them to diverge over time.
|
|
||||||
|
|
||||||
include/nlohmann/detail/macro_scope.hpp:88
|
|
||||||
If this has been externally defined, we should leave it as-is. This will break
|
|
||||||
if someone has `#define JSON_HAS_FOO 0`.
|
|
||||||
|
|
||||||
include/nlohmann/json.hpp:1534
|
|
||||||
`throw` → `JSON_THROW`, or this can't be used by people that disable exceptions.
|
|
||||||
|
|
||||||
single_include/nlohmann/json.hpp:9001
|
|
||||||
Looks like this was edited directly. Changes go in `include/` and then
|
|
||||||
`make amalgamate`.
|
|
||||||
|
|
||||||
docs/mkdocs/docs/api/basic_json/back.md:12
|
|
||||||
Should this list the precondition that the array is not empty?
|
|
||||||
```
|
|
||||||
@@ -253,6 +253,34 @@ Two conversions never copy at all:
|
|||||||
|
|
||||||
Both results are only valid as long as the view — and, for a string with no escapes, the borrowed source text — is.
|
Both results are only valid as long as the view — and, for a string with no escapes, the borrowed source text — is.
|
||||||
|
|
||||||
|
### `string_view` members
|
||||||
|
|
||||||
|
A struct whose member is a `#!cpp std::string_view` (C++17) can be decoded from a view with the
|
||||||
|
`NLOHMANN_DEFINE_TYPE_*` macros, and the member is not copied either: for a string without escape sequences it points
|
||||||
|
into the document's source text, and for a string with escape sequences into the document's buffer of decoded strings.
|
||||||
|
The member is valid as long as the document is alive and, for a document that
|
||||||
|
[borrows its source text](#ownership-and-lifetime) (as [`parse()`](../api/basic_json_document/parse.md) does for
|
||||||
|
an lvalue `#!cpp std::string`, a `#!cpp std::string_view`, or a C string), as long as that text is.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct record
|
||||||
|
{
|
||||||
|
std::string_view name;
|
||||||
|
int count;
|
||||||
|
NLOHMANN_DEFINE_TYPE_INTRUSIVE(record, name, count)
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::string text = R"({"name": "widget", "count": 3})";
|
||||||
|
const json_document doc = json_document::parse(text); // borrows text
|
||||||
|
const record r = doc.root().get<record>(); // r.name points into text, valid while text and doc are
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning "Decoding into a `string_view` from a `json` value dangles at once"
|
||||||
|
|
||||||
|
The same struct also compiles with `#!cpp json::parse(text).get<record>()`, but there `name` points into the
|
||||||
|
temporary `json` value, which is destroyed at the end of the statement. Use a named
|
||||||
|
[`json_document`](../api/json_document.md), as above, to decode such a struct.
|
||||||
|
|
||||||
## Writing generic code
|
## Writing generic code
|
||||||
|
|
||||||
A view's type names its `BasicJsonType` and its [`view_mode`](../api/view_mode.md):
|
A view's type names its `BasicJsonType` and its [`view_mode`](../api/view_mode.md):
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ using nlohmann::ordered_json_document;
|
|||||||
|
|
||||||
#ifdef JSON_HAS_CPP_17
|
#ifdef JSON_HAS_CPP_17
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <string_view>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// NLOHMANN_JSON_SERIALIZE_ENUM uses a static std::pair; same() compares floating-point values on purpose
|
// NLOHMANN_JSON_SERIALIZE_ENUM uses a static std::pair; same() compares floating-point values on purpose
|
||||||
@@ -469,5 +470,70 @@ TEST_CASE("json_view from_json")
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef JSON_HAS_CPP_17
|
||||||
|
namespace view_from_json
|
||||||
|
{
|
||||||
|
// a string_view member is not copied: it points into the source text or, for a
|
||||||
|
// string with escape sequences, into the buffer of decoded strings of the document
|
||||||
|
struct named_view
|
||||||
|
{
|
||||||
|
// initialized for GCC -Weffc++
|
||||||
|
// NOLINTNEXTLINE(readability-redundant-member-init)
|
||||||
|
std::string_view name{};
|
||||||
|
int n = 0;
|
||||||
|
NLOHMANN_DEFINE_TYPE_INTRUSIVE(named_view, name, n)
|
||||||
|
};
|
||||||
|
} // namespace view_from_json
|
||||||
|
|
||||||
|
TEST_CASE("json_view from_json: string_view members borrow from the document")
|
||||||
|
{
|
||||||
|
using view_from_json::named_view;
|
||||||
|
|
||||||
|
// whether all of s is inside the bytes of text
|
||||||
|
const auto within = [](std::string_view s, std::string_view text)
|
||||||
|
{
|
||||||
|
const auto first = reinterpret_cast<std::uintptr_t>(text.data());
|
||||||
|
const auto begin = reinterpret_cast<std::uintptr_t>(s.data());
|
||||||
|
return begin >= first && begin + s.size() <= first + text.size();
|
||||||
|
};
|
||||||
|
|
||||||
|
SECTION("a string without escapes points into the source text")
|
||||||
|
{
|
||||||
|
const std::string text = R"({"name": "hello", "n": 1})";
|
||||||
|
const json_document d = json_document::parse(text);
|
||||||
|
REQUIRE_FALSE(d.owns_source());
|
||||||
|
const auto r = d.root().get<named_view>();
|
||||||
|
CHECK(r.name == "hello");
|
||||||
|
CHECK(r.n == 1);
|
||||||
|
CHECK(within(r.name, text));
|
||||||
|
CHECK(within(r.name, d.source()));
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a string with escapes points into the document, not into the source text")
|
||||||
|
{
|
||||||
|
const std::string text = R"({"name": "a\nb", "n": 2})";
|
||||||
|
json_document d = json_document::parse(text);
|
||||||
|
REQUIRE_FALSE(d.owns_source());
|
||||||
|
const auto r = d.root().get<named_view>();
|
||||||
|
CHECK(r.name == "a\nb");
|
||||||
|
CHECK(r.n == 2);
|
||||||
|
CHECK_FALSE(within(r.name, text));
|
||||||
|
// valid as long as the document is alive, even if the document is moved
|
||||||
|
const json_document moved = std::move(d);
|
||||||
|
CHECK(r.name == "a\nb");
|
||||||
|
CHECK(moved.root().get<named_view>().name == r.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a document that owns its text keeps the members readable while it is alive")
|
||||||
|
{
|
||||||
|
const json_document d = json_document::parse(std::string(R"({"name": "owned", "n": 3})"));
|
||||||
|
REQUIRE(d.owns_source());
|
||||||
|
const auto r = d.root().get<named_view>();
|
||||||
|
CHECK(r.name == "owned");
|
||||||
|
CHECK(within(r.name, d.source()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
DOCTEST_CLANG_SUPPRESS_WARNING_POP
|
DOCTEST_CLANG_SUPPRESS_WARNING_POP
|
||||||
DOCTEST_GCC_SUPPRESS_WARNING_POP
|
DOCTEST_GCC_SUPPRESS_WARNING_POP
|
||||||
Reference in new issue
Block a user