Skip to content

Answer the remaining SonarQube findings, rule by rule - #374

Merged
Reefact merged 14 commits into
mainfrom
claude/sonar-points-restants-12zlnr
Jul 29, 2026
Merged

Answer the remaining SonarQube findings, rule by rule#374
Reefact merged 14 commits into
mainfrom
claude/sonar-points-restants-12zlnr

Conversation

@Reefact

@Reefact Reefact commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

Works through the 255 open SonarQube Cloud findings and answers each one rather than clearing the count: 249 are resolved, by complying where the advice is right and by declining — in writing, next to the reason — where the code it flags is deliberate or the support floor makes it impossible. The two findings blocking the Quality Gate are fixed.

Type of change

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

Changes

Quality Gate (2)

  • githubactions:S8264, the report's only two findings typed VULNERABILITY, in both mutation workflows. Permissions move from workflow level to per job. Both workflows already contradicted their own floor: the advisory gate job declares permissions: {} because it checks nothing out, while the workflow-level grant handed it read access anyway.

Applied (164)

  • IDE0028 (147, 13 projects) — collection initializers become collection expressions. These marked a genuine drift rather than a preference: the codebase spelled them both ways, 85 sites in brackets against 147 in new(), both forms inside the same file. Applied with dotnet format style --diagnostics IDE0028 iterated to a fixed point — the Roslyn fixer, not a text substitution. Nothing is added to .editorconfig: the rule fires at its default severity, which is what produced these 147.
  • CA2249 / CA1847 (7) — IndexOf(char) >= 0 becomes Contains(char).
  • csharpsquid:S1192 (7) — four literals repeated four times each become named constants, plus one in apply-fix.sh where a helper now mirrors the existing skip.
  • CA1864 (2) — ContainsKey-then-Add becomes TryAdd, "first entry wins" unchanged.
  • CA2231 / csharpsquid:S1210 (3) — equality and ordering operators on the binder's sample value objects. BookingDate was the reason: a class implementing IEquatable and IComparable where == compiled and compared references while Equals compared values.
  • xUnit1042 (1) — the surface-parity MemberData source becomes TheoryData<Type, string[]>.

Declined, each with its reason at the point of effect (83)

  • CA1859 (22) and CA1861 (22) in .editorconfig, shelldre:S7682 (12) and shelldre:S7679 (9) in the scanner invocation — the four rules ADR-0060 records. CA1861 is scoped to test projects only, so shipping code keeps the rule.
  • SYSLIB1045 (7) via a project NoWarn, and 10 findings via [SuppressMessage]CA2249 ×2, CA1865, CA1870 ×2, CA2263 for the support floor; csharpsquid:S125 and csharpsquid:S3267 because the rule is wrong at those two sites.

Not in this PR

  • csharpsquid:S3776 (6) — cognitive complexity 16 to 28, including RegexParser.cs at 28. Shipping code with real regression risk; it deserves its own PR and its own mutation attention.

Testing

  • dotnet build FirstClassErrors.sln
  • dotnet test FirstClassErrors.sln
  • Analyzer tests pass (FirstClassErrors.Analyzers.UnitTests)

Zero warnings, 2092 tests passing across 13 test projects after merging origin/main.

Beyond the checklist, because most of this change turns on which BCL surface the support floor carries:

  • The netstandard2.0 and net472 legs were built explicitly (-p:EnableNet472Floor=true -f net472). Collection expressions are a C# 12 compiler feature, not a BCL one, so the floor is unaffected by the 147 rewrites.
  • Each BCL-API rule was measured rather than assumed. Contains(char) compiles on net472 — it binds to Enumerable.Contains<char>, same ordinal answer — which is why 7 sites were fixed rather than declined. StartsWith(char) was applied, observed to fail with CS1503: cannot convert from 'char' to 'string', and reverted.
  • The IDE0028 result was verified through the SARIF log the scanner reads, filtering on suppressionStates: 0 live diagnostics in the sampled projects. JustDummies.UnitTests alone went from 90 findings to 0.

Documentation

  • Public API / error documentation updated
  • README / doc/ updated
  • French translation (doc/handwritten/for-users/README.fr.md) updated if user-facing behavior changed
  • No documentation change required

ADR-0060 is added in English and French, and indexed. No user-facing behaviour changes, so README.fr.md needs no update; the shipped public API is untouched, so no baseline regeneration. The operators added to FirstClassErrors.RequestBinder.Usage are in a sample project, which carries no public-API baseline.

Architecture decisions

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

ADR-0060Let stated intent outrank generic analyzer advice. Two limbs: a generic rule yields where the code it flags is a deliberate expression of intent, and readability outranks micro-performance unless a measured need is recorded. Its rationale states explicitly that volume is not a reason to decline a rule, citing IDE0028 as the case that proves it — the largest group in the report and the cheapest to silence, applied instead.

SYSLIB1045 is an application of ADR-0058, not a new decision: the rule is not contradicted by deliberate code, it is unsatisfiable on a target framework the project must still support.

Related issues

None.


Generated by Claude Code

claude added 14 commits July 29, 2026 13:08
Three rules account for 191 of the 255 findings left in the Sonar report, and
all three arrive under external_roslyn: they are compiler and BCL analyzer
diagnostics that the scanner republishes, so a severity of none removes them at
the source rather than dismissing them in the server's UI.

Each is declined for a reason the analyzer cannot see. CA1859 reads
IReadOnlyList<string> as an oversight where it is the contract, and honouring it
would hand .Add() to every caller to buy nanoseconds on error-message paths.
CA1861 would move the expected values of an assertion away from the check that
reads them, to save an allocation occurring a few hundred times in a suite.
IDE0028 asks for 147 edits across 13 projects that change no behaviour, on a
codebase already split 85 to 147 between the two spellings — applying it would
be adopting a convention, not fixing a defect.

Scope follows the reason. CA1861's argument is about tests and all 22 of its
findings sit in test projects, so it is declined under *Tests/ only and stays
live for shipping code, where a hot path can genuinely want it. The other two
are declined repository-wide, their justifications holding everywhere they fire.

Verified on JustDummies.UnitTests through the SARIF log the scanner reads: 78
diagnostics before (64 IDE0028, 9 CA1859, 5 CA1861), 0 after, and the other 12
findings in that project untouched. The full solution still builds at zero
warnings.

ADR-0060 records the policy the three share: stated intent outranks generic
analyzer advice, and readability outranks micro-performance unless a measured
need says otherwise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bxVrnCNsXW3RvwmLc9Kvy
Sonar reports the workflow-level `permissions: contents: read` in both mutation
workflows as githubactions:S8264, typed VULNERABILITY. They are the only two
vulnerabilities in the report and the only findings standing between the project
and a green Quality Gate.

The rule is right here rather than merely loud. A workflow-level grant reaches
every job, including any added later that does not need it, and both workflows
already contradicted their own floor: the advisory `gate` job declares
`permissions: {}` precisely because it checks nothing out. The floor was
therefore granting read access to one job that had gone to the trouble of
refusing it.

Each job now states the narrowest scope it can — `contents: read` for `changed`
and `full`, which check the repository out, and the unchanged `{}` for `gate` —
and the header comment records that a job added later must carry its own block,
since without one it inherits the repository default rather than a floor set in
the workflow.

The sixteen other workflows keep their workflow-level declaration: Sonar does not
flag them, their jobs all needing the scope they are granted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bxVrnCNsXW3RvwmLc9Kvy
`skip` already encapsulated the `applied=false` side of the workflow's output
contract, but its counterpart was written out at each of the four branches that
succeed — the literal `applied=true` four times, and the `impact=`/`summary=`
keys with it (Sonar shelldre:S1192). The asymmetry was the defect: one side of
the same contract had a name, the other did not.

`applied <impact> <summary>` gives it one. Both keys the workflow reads are now
written in exactly two places, and each case branch ends by saying what it did
rather than by restating the output format.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bxVrnCNsXW3RvwmLc9Kvy
S7682 and S7679 account for 21 of the remaining findings, all of them in the
repository's shell tooling and Claude hooks. Unlike the Roslyn rules declined in
.editorconfig, these are SonarQube's own analysis rather than build diagnostics
the scanner republishes, so nothing the compiler does can reach them; they are
ignored in the scanner invocation instead, which is the only place that can
carry the refusal.

S7682 asks for an explicit return at the end of a shell function. Every function
it flags ends with the command whose exit status IS the function's result — a
cat heredoc, an awk invocation, a printf — so `return 0` would mask those
failures and a bare `return` restates the default. One of them ends with `exit`,
after which a return is unreachable at all.

S7679 asks that a positional parameter be assigned to a local variable. Every
script here declares #!/bin/sh and `local` is not POSIX. tools/trains.sh already
shows the price of obeying without it: the one helper needing named parameters
carries _tf_-prefixed globals. Paying that in every two-line helper, to name a $1
sitting one line below the function's own name, buys nothing.

ADR-0060 covers both under the policy it already stated, and now records where
each family of refusal is written and why the two cannot share a home.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bxVrnCNsXW3RvwmLc9Kvy
The report's largest group, 147 IDE0028 findings across 13 projects, marked a
drift rather than a preference: the codebase spelled collection initializers
both ways, 85 sites in brackets against 147 in `new()`, with both forms inside
the same file — JustDummies.UnitTests alone held 32 of the first and 64 of the
second. Declining the rule would not have stopped that; it would have removed
the only thing able to stop it, which is exactly how the explicit-type rule
drifted to 203 violations while it lived only in the DotSettings (ADR-0055).

So the rule is honoured instead, and .editorconfig gains nothing: at its default
severity IDE0028 already fires on every regression, which is what produced these
147 in the first place. Drift is caught by the report rather than blocked at the
build, as it is for every rule but the explicit-type one.

Applied with `dotnet format style --diagnostics IDE0028`, iterated to a fixed
point — the Roslyn fixer, not a text substitution, so target-typed and spread
forms are rewritten by the compiler's own rules. Two statements collapse into
one where the fixer could see a `new()` followed by `AddRange`.

Verified: IDE0028 down to zero in the three projects that carried 107 of the
147, through the SARIF log the scanner reads; the solution builds at zero
warnings and the full suite passes, 1960 tests. The net472 floor leg builds too
— collection expressions are a C# 12 compiler feature, not a BCL one, so the
netstandard2.0 and .NET Framework 4.7.2 targets are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bxVrnCNsXW3RvwmLc9Kvy
IDE0028 is applied rather than declined, so it leaves the ADR: what remains is
CA1859, CA1861, S7682 and S7679 — 65 findings whose common trait is that the
code they flag is deliberate, not defective.

Dropping it costs the ADR its largest number and improves it. The rationale now
says outright that volume is not a reason to decline a rule, and cites IDE0028
as the case that proves it: the biggest group in the report and the cheapest to
silence, applied because its findings marked a real drift. An ADR that had
declined 147 findings on cost would have been quotable as licence to decline the
next inconvenient rule on the same ground.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bxVrnCNsXW3RvwmLc9Kvy
SYSLIB1045 asks that every `new Regex("...")` become a partial method carrying
[GeneratedRegex], so a source generator emits the automaton at compile time. The
attribute arrived in .NET 7. All 7 findings are in JustDummies.UnitTests, whose
floor leg is net472 (ADR-0022), and the three suites they sit in — AnyPatternTests,
XmlDocCrefConventionTests, ConcurrentDrawTests — are all compiled on that leg:
none of them appears in the project's net472 <Compile Remove> list.

So this is CA1510's wall again, and it takes CA1510's remedy: a NoWarn in the
one project that owns the constraint, stated next to a comment naming the floor,
rather than in Directory.Build.props or .editorconfig where it would also switch
the rule off for projects that can honour it. It expires the same way, the day
this project stops building against net472.

This is an application of ADR-0058, not a new decision, and not a case of
ADR-0060 either: the rule is not contradicted by deliberate code, it is
unsatisfiable on a target framework the project must still support.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bxVrnCNsXW3RvwmLc9Kvy
Six S1192 findings, four literals. Each was repeated four times in a place where
the copies have to agree and nothing made them: the `breaking` fail-on policy,
read once to default, once to validate, once to decide the exit code and once to
word the error; the example assembly path the four worker-failure errors
illustrate themselves with, which the generated documentation shows side by
side; and the two benchmark fixture values every payload is built from, where a
drifting literal would silently make two "comparable" benchmarks measure
different work.

Naming them is the point, not the count: a constant is the only thing that makes
"the same value" a fact rather than a coincidence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bxVrnCNsXW3RvwmLc9Kvy
Fourteen findings asking for a newer BCL spelling. Each was checked against the
support floor (ADR-0022) rather than taken or refused wholesale, because the
floor decides them one at a time.

Taken, ten of them. CA1864's two ContainsKey-then-Add pairs become TryAdd, in a
net8.0-only project and with the "first entry wins" behaviour unchanged. CA2249
and CA1847 become Contains(char) at seven sites: measured, that binds to
String.Contains(char) on the modern leg and to Enumerable.Contains<char> on
net472, same ordinal answer, and both legs compile.

Refused, four, each with the reason at the site. CA2249's remaining two want
Contains(string, StringComparison), absent from netstandard2.0 — one of them in
the shipped library, where IndexOf with the same StringComparison.Ordinal is the
only spelling that exists. CA1865 wants StartsWith(char): measured, the net472
leg rejects it with CS1503, cannot convert from 'char' to 'string'. CA1870 wants
SearchValues<T>, a .NET 8 type. CA2263's suppression only writes down what the
call site already said about Enum.IsDefined<TEnum>.

Verified on both legs: netstandard2.0 and net472 build, 1960 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bxVrnCNsXW3RvwmLc9Kvy
CA2231 and S1210 fire on three model types in the binder's Usage samples — code
written to be read and copied. The gap they mark is real, and worse in a sample
than it would be in a library.

NightCount and RoomNumber are readonly structs implementing IEquatable. A struct
gets no `==` from the language, so a reader who copies the pattern and writes
`roomA == roomB` meets a compile error. BookingDate is worse: a sealed class
implementing IEquatable and IComparable, where `==` DID compile and compared
references while Equals compared values. Two instances of the same date answered
differently depending on which the reader reached for.

The operators are now declared on all three, delegating to Equals and CompareTo
so the answers cannot diverge. BookingDate's four ordering operators share one
private Compare, which is also the single place its null verdict — a null sorts
before any date, as CompareTo already said — is written down.

The project carries no public-API baseline, so nothing needs regenerating.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bxVrnCNsXW3RvwmLc9Kvy
xUnit1042 reports the Builders member as returning untyped rows: an
IEnumerable<object[]> whose shape only has to match the theory's parameters at
run time, so a row with the wrong arity or a swapped pair fails as a test error
rather than a compile error.

TheoryData<Type, string[]> states the shape once and the compiler checks every
row against it. The twenty-four rows and their comments are unchanged, the
net8-only block still sits behind its #if, and the suite still reports 632
tests — the same count as before, which is what says no row was lost in the
conversion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bxVrnCNsXW3RvwmLc9Kvy
S3267 asks that the exclusion loop in AnyPattern.Excluding become a Where
clause. It cannot: the loop body appends to the very list its condition reads,
so a duplicate later in `values` is rejected against the values already taken. A
Where clause reads as a filter over a fixed collection, which is exactly what
this is not — and the two would not even agree on the result.

S125 reports commented-out code in AnyString at a line that is an English
sentence. The rule reads the trailing "(ADR-0045);" of the prose explaining the
member's null handling as a statement. Nothing there is code.

Both carry their reason in a SuppressMessage rather than being quietly deleted
from the report, per ADR-0060: a refusal that is not written down is
indistinguishable from an oversight the next time someone reads it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bxVrnCNsXW3RvwmLc9Kvy
Merging origin/main brought in JD007-JD022, written on the branch point where
IDE0028 was still unanswered, so four of the new analyzer files reintroduced the
`new()` spelling this branch had just finished removing.

Same treatment as the rest — `dotnet format style --diagnostics IDE0028`, run to
a fixed point — for four single-line initializers. Nothing else in the merged
work needed touching, and the alignment of the surrounding declarations is left
exactly as main wrote it.

This is the drift the branch's own commit predicted: the rule fires at its
default severity, so a regression is visible in the report rather than blocked
at the build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bxVrnCNsXW3RvwmLc9Kvy
@Reefact
Reefact merged commit 9616bf1 into main Jul 29, 2026
28 checks passed
@Reefact
Reefact deleted the claude/sonar-points-restants-12zlnr branch July 29, 2026 16:18
Reefact pushed a commit that referenced this pull request Jul 29, 2026
The two rules share one walk over a ScalarConstraintState because they
read the same state from opposite sides: JD023 asks whether anything
remains, JD024 whether anything changed.

JD023 reports an integer chain narrowed to nothing — an empty interval,
an empty lattice, an emptied allow-list. The library computes this with
one emptiness test; the rule runs the same test over the constants
written at the call site and stays silent for every argument it cannot
fold.

JD024 is the only member of the constraint family the run time NEVER
reports. Every other contradiction throws eventually and loudly, while an
inert constraint leaves the test green exercising a domain the author did
not write. The dangerous case is an exclusion of a sentinel the generator
could never draw: it misses silently, and starts mattering the day
someone widens the range. Info rather than Warning, because a defensive
exclusion kept so the intent survives a future widening is a real and
reasonable style.

Dogfooding caught a genuine bug in the model, not a misread of the
library. Any.Int64().LessThanOrEqualTo(long.MinValue) is legal and yields
exactly that value, yet the first version declared it empty: the state
used -long.MaxValue as its "unbounded" sentinel, which made long.MinValue
unrepresentable. The bounds now run to the true extremes, and a bound
genuinely beyond the range — GreaterThan(long.MaxValue) — is carried by
an explicit saturation flag rather than by arithmetic that would
overflow. Four tests pin the extremes in both directions.

Scope is deliberately narrow: integer generators only, one expression,
every argument folded. A constraint the model has never seen ends the
walk rather than being guessed at — a rule claiming a chain is
unsatisfiable has to be certain.

Rebased onto main after #374, which carried the analyzer library to
collection expressions. The two new files were written before that
convention landed, so they are brought to it here the same way: `dotnet
format style --diagnostics IDE0028`, run to a fixed point, five single
initializers, nothing else touched.

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