Skip to content

fix: refuse a comma in a name only where the serializer does - #69

Merged
Reefact merged 1 commit into
mainfrom
claude/emn0004-justification
Aug 11, 2026
Merged

fix: refuse a comma in a name only where the serializer does#69
Reefact merged 1 commit into
mainfrom
claude/emn0004-justification

Conversation

@Reefact

@Reefact Reefact commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Summary

EMN0004 reported a comma inside a declared name on every enum, and the start-up check refused one, on the reading that a comma separates values everywhere so a name carrying one can never be read back. Measured against the serializer, that is false off [Flags] — and the binder did worse than refuse the shape: once past the check it read "a,b" as a | b, a different member, silently.

What System.Text.Json actually does

Measured, not read off a specification:

enum value result
ordinary, declaring a, b and a,b "a,b" Ab — the whole name wins over the split
ordinary, same enum "a, b" a | b — no name is spelled with that space
ordinary, declaring news,world write / read "news,world" round-trips
[Flags], declaring "read,write" first use InvalidOperationException: "Flags enums must additionally not contain commas"

So the serializer looks the trimmed value up as one name before it splits anything, and refuses a comma inside a name on [Flags] alone. Refusing it everywhere made a registered enum stricter than the same enum left alone — which EnumContract's own remarks call the one thing this package promises never to do.

Type of change

  • Bug fix
  • New feature
  • Breaking change to the public API
  • Refactoring
  • Analyzer / diagnostic change
  • Tests
  • Documentation
  • Build / CI / tooling

Changes

  • EnumContract.TryParse looks the whole trimmed value up as a name before splitting, and splits only when that misses. The reorder cannot change any contract that was legal before, since none of them could contain a comma — and the suite agrees: only the three tests pinning the old decision failed.
  • MalformedNameProblem takes isFlags; the comma test is the one that reads it, because it is the one the serializer scopes that way. Problem.CommaInNameCommaInFlagsName, message reworded.
  • EnumContractAnalyzer resolves System.FlagsAttribute at compilation start and reports EMN0004 only on an enum carrying it. CommaInNameCommaInFlagsName. Title, message and description in Resources.resx all say what the rule is scoped to.
  • Parity fixtures rather than flipped assertions: CommaInsideAName (a, b, a,b) and CommaBesideCsharpNames join ReadParityTests, whose derived corpus compares every casing and every ordered pair against the serializer; CommaInsideAName joins FormattingParityTests. Reverting TryParse fails both with the divergence spelled out — 'a,b': System.Text.Json reads 4, this library reads 3.
  • Topic joins the OpenAPI host, where the name reaches the published closed list: the document advertises news,world, the server binds it as one value, and news, world is refused — so the two readings are held apart rather than assumed distinct.
  • Analyzer tests: EMN0004 stays silent on an ordinary enum, still fires on a [Flags] one whose other members are ordinary, and its message names [Flags].
  • Docs: EMN0004.en/fr.md rewritten (the "Why it matters" section was two false sentences), both index tables, the EMN0002 cross-reference, and a paragraph in contract-rules.en/fr.md stating the whole-name-first order. The [Unreleased] changelog entry celebrating the widening is replaced by one describing the correction, in both languages — nothing had shipped, so leaving the pair would only contradict itself.

Testing

  • dotnet build -c Release — clean, warnings are errors here
  • dotnet test -c Release — 923 passed, 0 failed
  • tests/PackageSmokeTest/run.sh — run because the analyzers changed; "The published package behaves as documented."

Also run: tools/style/lint-layout.sh (nothing to report), and the parity fixtures against the previous TryParse, to confirm they fail without the fix.

Public API

  • No change to the public surface
  • The surface changed and the baseline was updated in the same commit

The renamed descriptor is on the analyzers assembly, which carries no baseline; both packable projects are untouched.

Documentation

  • README / docs/ updated
  • The French counterpart was updated to match
  • CHANGELOG.md and docs/for-users/CHANGELOG.fr.md both updated
  • No documentation change required

🤖 Generated with Claude Code

https://claude.ai/code/session_01M5U2BZXpHQr7YcfNHVx9dA


Generated by Claude Code

EMN0004 reported a comma inside a declared name on every enum, and the
start-up check refused one, on the reading that a comma separates values
everywhere so a name carrying one can never be read back. The first half
is true and the second does not follow.

System.Text.Json looks the trimmed value up as one name before it splits
anything. Measured on an enum declaring a, b and a,b: it answers "a,b"
with the member of that name and "a, b" with a | b. It refuses the shape
only on a [Flags] enum, which its own message spells out — "Flags enums
must additionally not contain commas".

So this package refused a contract the serializer accepts, which is what
EnumContract calls the one thing it promises never to do. Worse than the
refusal was what the binder did once the shape got past it: TryParse
split on the comma without ever trying the whole value, so "a,b" read as
a | b — a different member, silently.

EMN0004 and the start-up check now stop where the serializer stops, and
TryParse tries the whole value as a name before splitting. That reorder
cannot change any contract that was legal before, since none of them
could contain a comma — and the suite agrees: three tests failed, all
three the ones pinning the old decision.

They are replaced by fixtures rather than by flipped assertions. Two go
into the derived parity corpus, which compares every casing and every
ordered pair of a,b,"a,b" against the serializer; reverting TryParse
fails them with the divergence spelled out. One goes into the OpenAPI
host, where the name reaches the published closed list and the server
has to answer to it — and where "news, world" is refused, so the two
readings are held apart rather than assumed distinct.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M5U2BZXpHQr7YcfNHVx9dA
@Reefact
Reefact force-pushed the claude/emn0004-justification branch from 5e015a9 to 8c50dd9 Compare August 11, 2026 10:57
@Reefact
Reefact merged commit adfd999 into main Aug 11, 2026
10 checks passed
@Reefact
Reefact deleted the claude/emn0004-justification branch August 11, 2026 11:32
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.

2 participants