Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Clear the Sonar code smells, first tranche - #362

#362
Open
Reefact wants to merge 13 commits into
mainReefact/first-class-errors:mainfrom
claude/sonar-code-smellsReefact/first-class-errors:claude/sonar-code-smellsCopy head branch name to clipboard
Open

Clear the Sonar code smells, first tranche#362
Reefact wants to merge 13 commits into
mainReefact/first-class-errors:mainfrom
claude/sonar-code-smellsReefact/first-class-errors:claude/sonar-code-smellsCopy head branch name to clipboard

Conversation

@Reefact

@Reefact Reefact commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Summary

Clears 144 of the 249 Sonar code smells that are not CA1510 (which is lot 2's netstandard2.0 question and is untouched here). Roughly half are real simplifications, half are findings that are correct to leave and are now recorded with the reason at the site.

This is a tranche, not the whole lot. The 105 that remain are inventoried at the bottom, with what each needs. I stopped rather than rush groups that turn on API-shape and refactoring judgement.

Type of change

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

Changes

Real defects fixed

  • BookingDate.Parse read yyyy-MM-dd through the DateOnly.TryParseExact overload with no format provider, so it parsed against the ambient culture. A machine whose culture uses different separators would have rejected a well-formed request — in the sample that exists to demonstrate good binding.
  • _train_field in trains.sh returned the empty string for an unrecognised field name — the same answer it gives for an unknown train, which require_train reports as "unknown train". A typo in a field name surfaced as a misleading message; it now names the field on stderr.
  • AmbientRandomSource.ReplaySnippet(int) and the nested AmbientState.ReplaySnippet property shared a name, and the method read the property one line below — one identifier meaning two things. The property is now Snippet, the word the file's own vocabulary reserves for the fragment as opposed to the guidance sentence.

Simplifications

  • 23 × X is false!X. Not a house style: the codebase negates with ! in ~192 places and read is false in eight files only.
  • 27 redundant null-forgiving operators removed — each behind a guard the flow analysis already understands ([NotNullWhen(false)]), or behind a ReflectionTypeLoadException.Types that netstandard2.0 declares unannotated. One left an identity Select behind, which went with it.
  • 11 index-arithmetic searches say what they mean: Contains, the char overloads of StartsWith/EndsWith, the generic Enum.IsDefined.
  • 10 of the 23 loop-with-a-guard bodies become Any, All, Count or a Where in the header. Five guard helpers that every caller invoked as a statement now return void, and the toolchain error messages use string.Create rather than allocating a FormattableString.

Findings recorded rather than "fixed"

Some would have made the code worse; four would have made it wrong:

  • OrdinalIntervalSpec and WideIntervalSpec advance inside the loop the accumulator their condition tests. A Where evaluates every predicate against the value held on entry — it would silently skip exclusions.
  • CollectionState, StringSpec, AnyDateTime and AnyDateTimeOffset read the collection their body mutates. Where is lazy, so the filter would run against a snapshot taken before the additions it exists to see.
  • The rest: loops that throw naming the first offending element (a filter discards which one failed); a null check that supplies the flow state its body needs; the Code.X error pattern whose constants deliberately mirror their factory names (same suppression the generator's own error types have always carried); Any.Combine's arity, which is ADR-0015; the private constructors of the "constrain once, draw many" engines, which carry the whole immutable state by design; four S125 findings that are prose, not disabled code.

Testing

  • dotnet build FirstClassErrors.sln — succeeded, 0 warnings after every commit
  • dotnet test FirstClassErrors.sln — 13 assemblies, 1 889 tests, 0 failures
  • Analyzer tests pass (FirstClassErrors.Analyzers.UnitTests)

Beyond the template: SonarAnalyzer.CSharp 10.30 was injected into the build to measure before and after. S1125, S107 and S2436 are gone from the solution; the S8969 and S3218 that remain are all in test projects SonarCloud does not report, as the lot-1 investigation established for S1244. Both shell scripts pass sh -n, and trains.sh was exercised end to end (train_ids, prefix_of, scopes_of, the new unknown-field path, require_train) to confirm unchanged behaviour. Every commit passes tools/commit-lint/lint-commit-message.sh.

Documentation

  • Public API / error documentation updated — no public API or error changed
  • README / doc/ updated
  • No documentation change required — no user-facing behaviour changed; the reasoning lives in the Justification text at each site

Architecture decisions

  • No architectural decision in this pull request
  • New decision recorded — ADR drafted as Proposed: ADR-____
  • Supersedes an existing ADR — successor proposed, status not flipped: ADR-____
  • ⚠️ Conflicts with an existing ADR — flagged for the maintainer: ADR-____

One item left for you, @Reefact: S3246 asks for BindingAssembler<TCommand> to be covariant (out). The variance is worth having, but it widens the public API surface and moves the committed public-API baseline, which this change did not ask for. It is suppressed with that reason rather than taken.

What remains — 105 findings

Rule Count What it needs
CA1861 21 Hoist constant array arguments to static readonly fields (test projects, mechanical)
CA1859 19 Change declared types from interface to concrete — must be checked per usage
CA1822 + S2325 19 "Make static" on fluent-builder methods — touches API shape, needs your call
S7682 + S7679 + S1192 20 Shell style. S7679 asks for local, which is not POSIX; these scripts are #!/bin/sh and the repo uses local nowhere — complying means a bashism or namespace-polluting assignments in one-line delegators. S7682 (explicit return) is cheap but pure ceremony on the same delegators.
SYSLIB1045 7 GeneratedRegex requires partial methods — changes the shape of the declaring classes
S3776 7 Genuine refactors; RegexParser sits at cognitive complexity 28 and deserves its own tests
S1192 6 Duplicated literals → constants
CA1870, CA2231, xUnit1042, S1210 6 Isolated: cached SearchValues, equality operators, typed theory data, comparison operators

Note on overlap with #358 (lot 1): both branches touch ContinuousIntervalSpec.cs and RandomSource.cs. The hunks are in different places and should merge cleanly, but whichever lands second is worth a glance.

Related issues

None — this batch comes from the SonarCloud report rather than a tracked issue.


Generated by Claude Code

claude added 13 commits July 28, 2026 21:03
Sonar S1125 reports 23 `X is false` comparisons. The idiom is a local habit
rather than a house style: the codebase negates with `!` in ~192 places and
reads `is false` in eight files of the CLI and the generator only. Converting
them settles on the dominant form.

Every left operand was a primary expression (an invocation or an identifier),
so `!` binds exactly as the pattern did and no parenthesis is needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXPkAL39tGpEy1LbKDk6PP
Sonar S8969 reports 27 `!` suppressions the compiler no longer needs. Each sits
behind a guard the flow analysis already understands — `string.IsNullOrWhiteSpace`
carries `[NotNullWhen(false)]`, so the value is known non-null in the branch that
dereferences it — or, in the reader, behind a `ReflectionTypeLoadException.Types`
that netstandard2.0 declares unannotated.

A `!` that suppresses nothing is worse than noise: it reads as a claim the author
checked something, and the next reader cannot tell it from the ones that are load
bearing. The one in `GetLoadableTypes` also left `Select(type => type)` behind
once removed, so that identity projection goes with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXPkAL39tGpEy1LbKDk6PP
Sonar S3218 reports eleven nested members shadowing an outer one. They are not
the same finding twice, so they do not get the same answer.

Ten are the library's documented error pattern: a `private static class Code`
whose constants deliberately mirror the names of the factory methods beside
them. Both sides are always qualified at the point of use — `Code.Malformed`
against the `Malformed(...)` call — so the shadowing is nominal, and the sample
error types now carry the same suppression the generator's own error types have
carried since they were written.

The eleventh is real. In `AmbientRandomSource`, the private static method
`ReplaySnippet(int)` and the nested `AmbientState.ReplaySnippet` property share
a name, and the method's body reads the property — one identifier meaning two
things one line apart. The property becomes `Snippet`, which is also the word
the file's own vocabulary reserves for the fragment as opposed to the guidance
sentence that embeds it. Internal throughout; no public surface moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXPkAL39tGpEy1LbKDk6PP
Sonar S107 (eight constructors over seven parameters) and S2436 (seven
declarations over the generic-parameter ceiling) both report arity. Neither is
accidental here, and the ceilings are the wrong lever for any of them.

The seven flagged constructors are the private ones of the generator engines.
The design is 'constrain once, draw many': every With* call rebuilds an
immutable spec, so the constructor takes the whole state by definition. No
caller ever writes that argument list — it is private — and a parameter object
would rename the list without shortening it.

Any.Combine's generic arity is ADR-0015: composition is heterogeneous, so it
needs one type parameter per part, and the arity-8 ceiling is the recorded
ergonomic decision. Two of these overloads already carried the S107 suppression
for the same reason; they now carry its generic-parameter twin beside it.
AnyCollection's third parameter is the CRTP self-type that keeps the fluent
methods returning the concrete generator instead of the base.

The benchmark's PlaceBookingCommand is a request DTO: its parameter count is the
payload's, and shrinking it would benchmark a different case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXPkAL39tGpEy1LbKDk6PP
Eleven searches spelled their intent as arithmetic on an index. `IndexOf(c) >= 0`
asks "does it contain c", `EndsWith("@", Ordinal)` asks about one character, and
`Enum.IsDefined(typeof(T), v)` boxes a value the generic overload takes as-is
(Sonar CA2249, CA1865, CA2263).

One of them is not like the others: `RegexParser` lives in JustDummies, which
still targets netstandard2.0, where `string.Contains(char)` does not exist. That
site takes the string overload, which does. The rest are in net8.0+ test and
sample projects, so they take the character overloads — which compare ordinally
by construction, making the dropped `StringComparison.Ordinal` a no-op rather
than a change of behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXPkAL39tGpEy1LbKDk6PP
`BookingDate.Parse` read `yyyy-MM-dd` through the `DateOnly.TryParseExact`
overload that takes no format provider, so it parsed against the ambient
culture (Sonar S6580). The format is fixed and ISO by contract — it is what the
request carries — so a machine whose culture uses different date separators or a
non-Gregorian calendar would reject a well-formed request, and the sample would
teach the defect it exists to demonstrate the absence of.

The parse now names `CultureInfo.InvariantCulture` and `DateTimeStyles.None`
explicitly. The file already reached for the invariant culture when rendering
(`ToString`), fully qualified; both sites now share one `using`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXPkAL39tGpEy1LbKDk6PP
The four toolchain error factories formatted their messages through
`FormattableString.Invariant($"…")`, which allocates a `FormattableString` and
its argument array before formatting (Sonar S6618). `string.Create` with the
same interpolated string takes the interpolation handler path instead and
formats straight into the result.

Same invariant culture, same text; GenDoc targets net8.0, where the overload
exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXPkAL39tGpEy1LbKDk6PP
`AnyString` and `CountConstraints` each wrap `SizeGuard` in local helpers that
validate and hand the value back. Every one of their fourteen call sites
discards the result and calls them as statements (Sonar S3241), so the return
type promised a conversion the code never used — and hid that these are guards.
They now return `void`. `SizeGuard`'s own helpers keep their return type; those
are used as values.

`UriSpec` also split on a one-element array where the `char` overload of
`string.Split` says the same thing without allocating it (S3878).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXPkAL39tGpEy1LbKDk6PP
The conflict message picked between three phrasings through a ternary nested in
a ternary (Sonar S3358), so the reader had to unpick operator nesting to see
that there are simply three cases: an allow-list nothing survives, a flags enum
whose combinations are all excluded, and a plain enum whose declared members
are. An if/else chain says that, and a comment now names the three.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXPkAL39tGpEy1LbKDk6PP
Sixteen findings across five rules that report structure rather than a defect.
None of them is fixable without making the code worse, so each is recorded with
the reason at the site.

S125 (four) reads prose as disabled code: an equation, a bracketed range or a
semicolon inside an explanatory sentence. Those comments carry the reasoning
this codebase asks every comment to carry.

S3459 and S1144 report the setters of `SnapshotDocument` as unassigned and
unused; they are the deserializer's entry point, called by reflection.

S4136 wants every same-named overload adjacent. The binder's argument sources
are grouped by shape first — scalars, then lists — the way OutcomeTaskExtensions
groups by receiver type; each source's scalar pair IS adjacent.

S3928 reports two exception `paramName`s as unknown. Both name the parameter the
CALLER wrote, which is the one they must fix; the private helper's own
parameters name reflection details no consumer can see. S3220 flags the
`Expression.Block` overload that is the intended one.

S1694 wants two internal abstract roots turned into interfaces; a class cannot
be implemented from outside the assembly and keeps the option of shared state.
S2094 flags a Spectre.Console settings type that must exist to bind its command.

S3246 is left for @Reefact: marking `BindingAssembler<TCommand>` covariant is
worth having but moves the committed public-API baseline, so it is flagged
rather than taken here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXPkAL39tGpEy1LbKDk6PP
Sonar S3267 reports 23 loops whose body is one guarded statement. Ten of them
genuinely read better as a filter and become `Any`, `All`, `Count` or a `Where`
in the header. The other thirteen do not, and four of those would be rewritten
into defects:

- `OrdinalIntervalSpec` and `WideIntervalSpec` advance the accumulator the
  condition tests, so each iteration changes what the next one compares against.
  A `Where` would evaluate every predicate against the value held on entry and
  skip exclusions.
- `CollectionState`, `StringSpec`, `AnyDateTime` and `AnyDateTimeOffset` read
  the collection their body mutates. `Where` is lazy, so the filter would run
  against a snapshot taken before the additions it exists to see.
- Three loops throw naming the FIRST offending element; a filter discards which
  one failed.
- One relies on a null check for the flow state its body needs, and would need
  back the null-forgiving operator this branch just removed.

Each of those carries its reason at the site.

This also corrects the indentation of the suppression attributes added in the
previous commit: they were emitted at column zero instead of matching the member
they annotate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXPkAL39tGpEy1LbKDk6PP
Sonar S131 reports three `case` statements with no default branch. Two are in
the history-hygiene hook, where anything unmatched really is the ordinary case;
they now say so with an explicit no-op rather than leaving the reader to infer
it.

The third mattered. `_train_field` returned the empty string for a field name
its row format does not carry — the same answer it gives for an unknown TRAIN,
which `require_train` reads as "no such train". A typo in a field name would
therefore have surfaced as a bogus "unknown train" message. It now says which
field it did not recognise, on stderr.

The other shell findings are left alone deliberately. S7679 asks for each
positional parameter to be assigned to a `local` variable, but `local` is not
POSIX and these scripts are `#!/bin/sh` — the repository uses none, which is why
`_train_field` reaches for prefixed globals instead. Complying would mean either
adopting a bashism or adding namespace-polluting assignments to one-line
delegators.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXPkAL39tGpEy1LbKDk6PP
The S3928 suppression on `Malformed` answered SonarAnalyzer but not CA2208,
which reports the same `paramName` from the .NET SDK analyzers and so left the
finding open. It now carries both, pointing at the same reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXPkAL39tGpEy1LbKDk6PP
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

Morty Proxy This is a proxified and sanitized view of the page, visit original site.