Skip to content

fix: config reader/writer, formatter, and ExtrasError bugs#1369

Merged
henryiii merged 2 commits into
mainfrom
fix/config-formatter-issues
Jul 1, 2026
Merged

fix: config reader/writer, formatter, and ExtrasError bugs#1369
henryiii merged 2 commits into
mainfrom
fix/config-formatter-issues

Conversation

@henryiii

Copy link
Copy Markdown
Collaborator

🤖 AI text below 🤖

Part of #1357

Fixes a batch of verified Config/Formatter/Error bugs. Each was reproduced before changing; regression tests accompany the behavioral fixes.

include/CLI/impl/Config_inl.hpp

  1. Section header with a trailing comment is now recognized. [section] # a comment (legal TOML) previously fell through the front()=='[' && back()==']' check (because back() was 't', not ']'), producing a bogus flag item named [section] and mis-attributing following keys. The reader now strips a quote-aware trailing comment for lines that open with [ but do not end with ], before the section check. Value lines are untouched (they still handle their own trailing comments downstream).
  2. A one-line multiline comment no longer swallows the rest of the file. '''comment on one line''' followed by real config now parses correctly. After detecting the '''/""" opener the same line is checked for a closer, guarded by a length >= 6 requirement so the opening quotes themselves are not counted as the closer.
  3. Off-by-two in Join-policy embedded-delimiter detection. A joined string of N elements has N-1+e delimiters; the array fallback should trigger when e > 0, i.e. opt->count() < delim_count + 1. The code tested < delim_count - 1, so embedded-delimiter cases slipped through and wrote a bare join (e.g. vals="c,[a,b]") instead of the array form (vals=['c', "[a,b]"]). Fixed the comparison (signedness preserved via the existing cast). A round-trip test exercises the array-fallback path via the [[ ]] vector escape, which is the reachable way an element retains an embedded delimiter (normal delimiter-splitting options never produce e > 0).
  4. clean_name_string namespace/signature mismatch. Config.hpp declared void detail::clean_name_string(std::string&, const std::string&), but the definition lived in the public CLI:: namespace and returned std::string&, so the detail declaration was never defined and the real function polluted CLI::. The definition is moved into namespace detail, the return type reconciled to void (matching the declaration; no caller used the return value), and the four call sites in to_config qualified as detail::clean_name_string.
  5. Consistent comment lead on group headers. The unnamed-subcommand group header wrote only commentLead (# Group Options); it now matches the main option-group header style commentChar << commentLead (## Group Options).

include/CLI/impl/Formatter_inl.hpp

  1. Hidden positionals no longer render as [] in the usage line. make_usage now filters positionals with !opt->get_group().empty(), matching make_positionals.
  2. Long subcommand names no longer glue onto the description. make_subcommand now mirrors make_option's long-name branch: when the name fills the column it emits a newline and disables the skip-first-line-prefix, so the description starts on its own indented line.
  3. Non-paragraph description path no longer emits an extra blank line. make_description already returns desc + "\n\n"; the enable_description_formatting(false) branch in make_help dropped its extra '\n' so toggling the flag no longer changes spacing. (Scope limited to make_help per the report; make_expanded left as-is.)

include/CLI/Error.hpp

  1. ExtrasError(name, args) reports the right error name. The two-argument constructor previously delegated to the protected 3-arg form, making get_name() return the app/subcommand name instead of "ExtrasError" (every other error class returns its class name, and App::exit/user code dispatch on get_name()). It now folds the app name into the message and constructs via the public single-arg path so get_name() == "ExtrasError", while preserving (and slightly enriching) the message content.

Tests

  • tests/ConfigFileTest.cpp: TomlSectionTrailingComment (1), TomlDocStringCommentOneLine (2), IniJoinEmbeddedDelimiterRoundTrip (3, verified to fail on the pre-fix comparison).
  • tests/FormatterTest.cpp: HiddenPositionalUsage (6), LongSubcommandName (7), DescriptionNoFormattingSpacing (8) — all verified to fail without their fix.
  • tests/AppTest.cpp: ExtrasErrorName (9).
  • Items 4 and 5 are covered by existing config round-trip tests, which continue to pass.

No false positives were found; all nine issues reproduced.

Verification

ctest over the full suite: 24/24 passed. The five primary binaries (ConfigFileTest, HelpTest, FormatterTest, AppTest, SubcommandTest) all pass; no existing tests required changes. prek -a --quiet is clean.

🤖 Generated with Claude Code

Config_inl.hpp:
- recognize section headers with a trailing comment ("[section] # ...")
- close one-line multiline comments ('''text''') instead of swallowing the file
- fix off-by-two in Join-policy embedded-delimiter detection (e>0 fallback)
- define detail::clean_name_string in namespace detail (was leaking into CLI::
  and never satisfying its declaration); qualify call sites
- use consistent comment lead on the unnamed-subcommand group header

Formatter_inl.hpp:
- exclude hidden positionals (empty group) from the usage line so they no
  longer render as "[]"
- wrap long subcommand names onto their own line like long option names
- drop the extra newline in the non-paragraph description path

Error.hpp:
- ExtrasError(name, args) now reports get_name()=="ExtrasError" like every
  other error class, folding the app name into the message

Adds regression tests in ConfigFileTest, FormatterTest, and AppTest.

Assisted-by: ClaudeCode:claude-opus-4-8
The comparison `opt->count() < delim_count + 1` is semantically
equivalent to `opt->count() <= delim_count` but the addend form
triggers GCC's -Wstrict-overflow=5 (assumes no signed wrap), which
is treated as an error in the Azure CI Docker jobs (gcc9, gcc13_17,
gcc15_23).  Rewrite as `<= delim_count` to suppress the warning
without changing the logic.

Assisted-by: ClaudeCode:claude-sonnet-4.6
@henryiii henryiii marked this pull request as ready for review June 16, 2026 04:21
@henryiii henryiii merged commit df42e6d into main Jul 1, 2026
73 checks passed
@henryiii henryiii deleted the fix/config-formatter-issues branch July 1, 2026 20:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant