Skip to content

Share DOM SAX position handling; fix stale parser and lexer comments - #5731

Merged
nlohmann merged 7 commits into
developfrom
techdebt/5712-parser-lexer-cleanup
Oct 1, 2026
Merged

nlohmann merged 7 commits into
developfrom
techdebt/5712-parser-lexer-cleanup

Conversation

@nlohmann

@nlohmann nlohmann commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

This PR implements the "do-now" items of the technical-debt umbrella issue #5712: deduplicating the diagnostic-position setter shared by the two DOM SAX parsers, and fixing a set of stale/incorrect comments and include lists in the parser, lexer and input headers. Each commit is one checklist item of #5712 and can be reviewed on its own. All changes are behavior-preserving; two commits are code changes (deduplication), the rest are comment-only or include-only fixes. The follow-up commits listed under "Remaining checklist items" implement every item that was left open, so this PR now closes #5712; see "Public API" for their effects.

Changes

Tests

Verified locally per commit: make amalgamate / make check-amalgamation produced a clean diff on every commit (amalgamate + make pretty with astyle 3.4.13, BUILD.bazel unchanged). For the position-deduplication commit, unit-diagnostic-positions and unit-class_parser (including with JSON_DIAGNOSTIC_POSITIONS=1, which exercises the callback parser's discarded path formerly excluded from coverage) passed, along with unit-alt-string, unit-ordered_json, unit-allocator, unit-diagnostics, unit-deserialization (JSON_USE_IMPLICIT_CONVERSIONS=0) and unit-disabled_exceptions (JSON_NOEXCEPTION), built with ASan/UBSan under clang++ and g++-16 at -std=c++11, with no new warnings under -Wall -Wextra -Wshadow or clang -Weverything. For the include-hygiene commit, each changed header (input_adapters.hpp, json_sax.hpp, lexer.hpp, binary_reader.hpp) was compiled standalone with -fsyntax-only (clang++, -std=c++11) to confirm the corrected include lists are self-sufficient, and unit-deserialization, unit-class_lexer, unit-cbor and unit-bon8 were run (ASan/UBSan), including a targeted re-check of the BON8 endianness note against write_bon8_integer()'s own documentation and an empirical to_bon8() byte-order check. A standalone build/run smoke test was also done for every commit that touches include/, confirming the branch builds correctly at each step, not just at the tip.

CI must confirm:

  • The full CI compiler matrix (GCC 4.8+, Clang 3.4+, MSVC, ICC, NVHPC)
  • ci_clang_tidy and cppcheck
  • The check-amalgamation job on the final PR state

Public API

No breaking changes. All five commits are documented as behavior-preserving: items 1's deduplication only moves an identical private helper's body behind a new detail::diagnostic_positions struct that basic_json befriends (under JSON_DIAGNOSTIC_POSITIONS) and removes stale coverage/lint annotations; the remaining four commits change only comments and internal include lists. No public-namespace names are added or removed, no observable behavior or compile errors change, and the ABI is unaffected.

Follow-up commits:

  • Items 2 and 6 are internal refactors with no behavior change.

Remaining checklist items (follow-up commits)

Items 2 and 6, which were left open because they overlap open PRs, are now implemented here as well, so this PR closes #5712.

Tests for these commits:

  • unit-class_parser, unit-class_lexer, unit-deserialization, unit-wstring, unit-diagnostic-positions, unit-precise-stream-position (with JSON_PRECISE_STREAM_POSITION) and unit-disabled_exceptions (with JSON_NOEXCEPTION), with ASan/UBSan, C++11/17/20
  • parse microbenchmark over 1M escape sequences: unchanged (~73 ms)
  • make check-amalgamation clean

Item 2 overlaps draft #5601, item 6 overlaps #5704. Whichever PR lands second needs a rebase.

Closes #5712


This PR was written by Claude Code on behalf of @nlohmann.

🤖 Generated with Claude Code

json_sax_dom_parser and json_sax_dom_callback_parser each had a private
copy of handle_diagnostic_positions_for_json_value(), identical except
for comments. Move the body into one static member function,
detail::diagnostic_positions::set_from_lexer(value, lexer), which both
classes call with their lexer pointer. basic_json befriends the new
struct (only when JSON_DIAGNOSTIC_POSITIONS is enabled), as the position
members are private.

The discarded case is reached through the callback parser, so the
LCOV_EXCL markers that only the dom parser's copy had are gone. The
NOLINT on the unreachable default case loses the stray
"-warnings-as-errors", which is not a check name.

The start-position setup in start_object()/start_array() is left alone,
as #5706 is editing the callback parser's versions.

Behavior, the public API and the ABI are unchanged.

Part of #5712

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
The class documentation called the parser a recursive descent parser,
but sax_parse_internal() is a loop that keeps the open containers on an
explicit stack. The comment at the end of an array and of an object
said the flag is set to false while the code below it sets it to true.
Describe what the code does instead.

Comments only; behavior, the public API and the ABI are unchanged.

Part of #5712

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
The comments explaining the accept() shortcut in convert_number() and
the member documentation still argued in terms of strtoull()/strtoll()
and errno, which #5283 replaced with convert_integer(), and pointed at
scan_number() instead of convert_number(). They also did not say that
scan_number_bulk_contiguous() converts integers itself, so the shortcut
is only reached for input without bulk access, with
JSON_DIAGNOSTIC_POSITIONS, or when the bulk scanner falls back.

Rewrite both comments to describe the digit-count check in front of
convert_integer(), keeping the 18-digit bound and the json_sax_acceptor
argument. The stale <cstdlib> comment is left for after #5616, which
edits that include block.

Comments only; behavior, the public API and the ABI are unchanged.

Part of #5712

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
The documentation of decode() called the Hoehrmann DFA the single
source of truth for UTF-8 validation. It is used only by the serializer
and by is_valid_utf8() (CBOR/MessagePack/BSON/UBJSON/BJData text
strings). The lexer's scan_string() switch, validate_one_utf8() /
valid_utf8_prefix() (bulk string scan, BON8 bulk path and BON8 writer)
and the BON8 byte path in get_bon8_string() check the RFC 3629 ranges
on their own.

Replace the sentence with a list of the four validators, what each is
used for, and a note that they must accept the same sequences. Sharing
code between them was considered and dropped: it would save a few lines
in a validator that is entangled with BON8 pushback, and #5677 is
editing the BON8 byte path.

Comments only; behavior, the public API and the ABI are unchanged.

Part of #5712

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
input_adapters.hpp included <memory> and <numeric> for the removed
shared_ptr-based adapter design but used neither; it called
(std::min) without including <algorithm>. json_sax.hpp used
std::numeric_limits without including <limits>. Also corrected
comments that no longer matched the code: input_stream_adapter does
not skip the input's BOM (the lexer's skip_bom() does), the
span_input_adapter comment named the no-longer-existing
input_buffer_adapter type, lexer::get_string() does not reset the
token, binary_reader's get_number() doc opened with /* instead of
/*! (so Doxygen skipped it) and omitted BON8 from its endianness
note, and the UBJSON-binary-types note did not mention that BJData
'B' arrays are read as binary.

Left out: the lgtm suppression on lexer.hpp's scan_number() (in
#5616's hunk) and the "-1 if unknown" wording in json_sax.hpp's
start_object/start_array docs (in draft #5267's hunk), per the
verdict's conflict list.

Part of #5712

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
…arse()

json_sax_dom_callback_parser and json_sax_dom_parser branches of
parser::parse() ran the same ~25 lines after sax_parse_internal():
the strict-mode EOF check (raising parse_error.101 through the SAX
parser), release_lookahead() in non-strict mode, and mapping an
errored SAX parser to a discarded result. The two copies had already
drifted apart in formatting and in the second copy's "see above"
comment.

Add a private parse_dom(DomSax&, strict) member that runs this shared
sequence once and returns whether the SAX parser did not error; both
branches of parse() now only construct their DOM SAX parser, call
parse_dom(), and (for the callback parser) map a discarded top-level
value to null. sax_parse() is left untouched, since it only runs the
EOF check and release_lookahead() when sax_parse_internal() succeeded,
unlike parse(), which runs them unconditionally.

Behavior-preserving: same operations in the same order for both SAX
parser kinds. Verified with unit-class_parser (strict/non-strict,
callback and non-callback), unit-deserialization and
unit-disabled_exceptions (JSON_NOEXCEPTION), plus a clean
make amalgamate / make check-amalgamation diff.

Overlaps #5601, which touches the same lines.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

#5712 item 2
…s and the lexer

The 1/2/3/4-byte UTF-8 encoding ladder was written out by hand three
times: in wide_string_input_helper<..., 4>::fill_buffer() for a UTF-32
code point, in the UTF-16 helper for both a BMP code unit and a valid
surrogate pair, and in the lexer's \uXXXX/\uXXXX\uYYYY handling. The
copies had drifted: the UTF-32 helper masked the leading bits of each
byte (& 0x1Fu, & 0x0Fu, & 0x07u) where the others relied on the shift
alone, even though both give the same result for a code point that is
already known to be in range.

Add detail::encode_utf8(cp, out) in string_utils.hpp, a single encoder
that invokes a callable once per output byte, most significant byte
first. Use it in the three valid-code-point branches (UTF-32 code
points up to U+10FFFF, UTF-16 code units outside the surrogate range,
and valid UTF-16 surrogate pairs) and in the lexer's \u handling, where
out forwards to add(). The UTF-16 helper's deliberate pass-through of
malformed surrogate units and the UTF-32 helper's 0xFF sentinel for
code points above U+10FFFF are untouched, since neither reaches the new
helper.

Behavior-preserving: same bytes in the same order for every valid code
point, verified with unit-class_lexer, unit-class_parser,
unit-deserialization, unit-wstring and the non-test-data parts of
unit-unicode1..5 (ASan/UBSan, C++11/17/20), and an escape-heavy parse
microbenchmark that shows no change (about 73 ms either way, median of
3, 1M escape sequences). single_include/ regenerated with make
amalgamate; make check-amalgamation leaves a clean tree.

Overlaps #5704, which rewrites the wide_string_input_helper
specializations touched here.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

#5712 item 6
@nlohmann nlohmann added the review needed It would be great if someone could review the proposed changes. label Sep 30, 2026

#if JSON_DIAGNOSTIC_POSITIONS
handle_diagnostic_positions_for_json_value(ref_stack.back()->m_data.m_value.array->back());
diagnostic_positions::set_from_lexer(ref_stack.back()->m_data.m_value.array->back(), m_lexer_ref);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these calls all check m_lexer_ref != nullptr up front so they don't have to dig into stacks and such if they aren't going to do anything? That then removes a level of nesting in set_from_lexer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit weird that m_lexer_ref is a pointer, but that's not the fault of this PR.

@nlohmann nlohmann removed the review needed It would be great if someone could review the proposed changes. label Oct 1, 2026
@nlohmann nlohmann added this to the Release 3.13.0 milestone Oct 1, 2026
@nlohmann
nlohmann merged commit cff0a61 into develop Oct 1, 2026
61 of 160 checks passed
@nlohmann
nlohmann deleted the techdebt/5712-parser-lexer-cleanup branch October 1, 2026 05:32
@github-actions github-actions Bot added aspect: binary formats BSON, CBOR, MessagePack, UBJSON L labels Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

aspect: binary formats BSON, CBOR, MessagePack, UBJSON L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JSON parser, lexer and SAX DOM parsers: duplicated position/EOF/UTF-8 encoding code and stale comments and includes

2 participants