Skip to content

chore(sonar): burn down csharpsquid smell cluster across API - #389

Merged
thomasluizon merged 1 commit into
mainfrom
chore/sonar-csharpsquid-smell-burndown
Jul 13, 2026
Merged

chore(sonar): burn down csharpsquid smell cluster across API#389
thomasluizon merged 1 commit into
mainfrom
chore/sonar-csharpsquid-smell-burndown

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

Behavior-preserving burn-down of the SonarCloud csharpsquid smell cluster (orbit-api, key thomasluizon_orbit-api). Every fix is at source and preserves behavior — no #pragma/NOSONAR gaming.

Rules fixed (all open instances → 0)

Rule Count Fix
S1192 7 Extract repeated string literals to private const (or reuse existing const).
S3267 10 Simplify filtering loops to LINQ Where/Any/OfType/FirstOrDefault.
S3358 7 Extract nested ternaries to statements / locals.
S108 3 Document intentionally-empty catch blocks with a URL-linked WHY note.
S1144 3 Delete unused private setters (Report.ReviewedAtUtc, PendingClarification.ResolvedAtUtc) + unused field (GoogleCalendarReconnectMessage).
S2589 3 Drop always-true/false sub-conditions.
S2325 2 Mark TrySaveBatchAsync / ApplyEntityMutationAsync static.
S1172 1 Remove unused cancellationToken param from HandlePostLogin + caller.
S1075 1 Move the Google OAuth token URL to Google:TokenUrl config (matches the existing Google:ClientId/ClientSecret pattern).
S3260 1 Seal the private UserDatePreferences record.
S4581 1 Use Guid.Empty instead of default(Guid).

Total: 39 issues across 11 rules.

Notes

  • Get-only entity props (ReviewedAtUtc, ResolvedAtUtc) remain EF-materializable via their backing fields — the removed setters were never called from C#.
  • S1075 config move adds Google:TokenUrl to appsettings.json (base, always loaded) + both GoogleTokenService test configs; the value is unchanged (https://oauth2.googleapis.com/token).
  • These are server-side csharpsquid rules, so the authoritative confirmation is the SonarCloud PR scan (they do not surface in the local build).

Verification

  • dotnet build Orbit.slnx — 0 errors, no new warnings.
  • dotnet test — 5284 passed, 0 failed (Domain 512, Application 2773, Infrastructure 1992, Analyzers 7).

Refs thomasluizon/orbit-ui-mobile#243

Behavior-preserving fixes for 11 SonarCloud csharpsquid rules:

- S1192 (7): extract repeated string literals to private consts
  (ResendEmailService heading/intro/footer, ProfileController
  enabled/disabled via ToggleLabel, NotificationTools update_notifications,
  OrbitDbContext '[]'::jsonb reuse the existing const).
- S3267 (10): simplify filtering loops to LINQ Where/Any/OfType/FirstOrDefault.
- S3358 (7): extract nested ternaries to statements/locals.
- S108 (3): document intentionally-empty catch blocks (URL-linked WHY).
- S1144 (3): delete unused private setters/field.
- S2589 (3): drop always-true/false conditions.
- S2325 (2): mark helper methods static.
- S1172 (1): remove unused cancellationToken param + caller.
- S1075 (1): move the Google OAuth token URL to Google:TokenUrl config.
- S3260 (1): seal the private UserDatePreferences record.
- S4581 (1): use Guid.Empty instead of default(Guid).

Refs thomasluizon/orbit-ui-mobile#243

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

PR Review — #389 chore(sonar): burn down csharpsquid smell cluster across API

Recommendation: APPROVE

Severity counts — Critical (incl. old-client breaks): 0, High: 0, Medium: 0, Low/Info: 1

Summary:
Behavior-preserving SonarCloud csharpsquid smell burndown across 35 files (+105/-118): loop-to-LINQ refactors, extracting repeated string literals to consts, de-nesting ternaries, documenting previously-empty catch blocks with WHY/URL comments, removing dead unused members, marking two methods static, removing unused parameters, moving the Google OAuth token URL from a hardcoded const to IConfiguration, sealing a private record, and replacing default(Guid) with Guid.Empty. All 35 changed files were traced line-by-line against their diffs; every fix checks out as genuinely behavior-preserving with no functional regression.

Security pass:

  • GoogleTokenService.cs: _googleTokenUrl is bound from configuration["Google:TokenUrl"]! at construction time, sourced only from appsettings.json/env vars, never from request input. No SSRF/config-injection risk. The null-forgiving ! is consistent with the existing Google:ClientId/Google:ClientSecret reads in the same file.
  • Three newly-documented empty catches, none swallow anything security-relevant: ConcurrencyRetry.cs SaveWithRetryAsync retries only when (attempt less than maxAttempts), final-attempt failure still propagates to ConcurrencyExceptionHandler resulting in HTTP 409; GoogleTokenService.cs ParseErrorResponse catches malformed JSON from Google's error body and falls through to the generic TransientFailure/logged-warning path (the auth-relevant invalid_grant/unauthorized_client classification happens before this catch is reached, so nothing is masked); ChatController.cs OperationCanceledException swallow is benign (client disconnect).
  • LINQ refactors: SyncControllerMutations.cs ApplyEntityMutationAsync still builds the ownedById predicate per call-site with x.Id == mutation.Id and x.UserId == userId; ownership scoping is preserved, the static extraction didn't drop userId from the predicate.
  • NotificationTools.cs: the extracted UpdateNotificationsTool const is just a string label; ClaimsPrincipal user (and thus per-request user-id scoping via McpExecutorBridge/IAgentOperationExecutor) is still forwarded on every call site. No scoping regression.

No Critical/High/Medium findings.

Cross-repo contract:
No endpoint/DTO shape changes in this diff (internal refactor only), the orbit-ui-mobile consumer-parity contract is not implicated. The sibling repo is not checked out in this CI job, so this is asserted from diff content rather than independently cross-checked; noted for transparency, not as a blocking concern.

Verdict:
Clean, low-risk mechanical cleanup. No blocking issues found, approving.

@thomasluizon
thomasluizon merged commit 56c75b9 into main Jul 13, 2026
19 checks passed
@thomasluizon
thomasluizon deleted the chore/sonar-csharpsquid-smell-burndown branch July 13, 2026 21:11
thomasluizon added a commit that referenced this pull request Jul 13, 2026
…tartup PendingModelChangesWarning crash) (#390)

#389's S1144 smell burn-down removed the `private set` from two EF-mapped
entity properties -- Report.ReviewedAtUtc and PendingClarification.ResolvedAtUtc --
turning them into read-only auto-properties. EF Core's property-discovery
convention does not map read-only auto-properties, so the runtime model
dropped both columns while OrbitDbContextModelSnapshot.cs still declared them.
The startup DB-migrate step is configured to throw on PendingModelChangesWarning,
so every orbit-api deploy since #389 crashes (exit 139) at
WebApplicationExtensions.ConfigureOrbitPipeline.

Restoring the private setters re-maps both columns with ZERO schema change
(no new migration, no snapshot edit): `dotnet ef migrations
has-pending-model-changes` returns false. Adding a migration instead would
DropColumn both live production columns (data loss), which #389 never intended.

The setters are unused from C# (EF sets them via reflection), so a URL-linked
WHY note keeps a future S1144 pass from re-removing them and re-breaking the deploy.

Refs #389, thomasluizon/orbit-ui-mobile#243

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
thomasluizon added a commit that referenced this pull request Jul 14, 2026
…p ReminderTimes in domain (#396)

Resolve two csharpsquid:S1144 "unused private setter" findings that were true
false positives (the setters were invoked only by EF via reflection):

- Report.ReviewedAtUtc and PendingClarification.ResolvedAtUtc are now read-only
  properties (=> _backingField) over private backing fields, so no private setter
  exists for Sonar to flag.
- Each column is mapped EXPLICITLY in its IEntityTypeConfiguration block via
  Property(x => x.Prop).HasField("_field"), so EF convention cannot drop it the
  way it dropped read-only auto-properties in #389. The relational model is
  byte-identical: `dotnet ef migrations has-pending-model-changes` reports no
  pending changes. PendingClarification's one-shot resolve still flips the column
  atomically at the SQL layer via ExecuteUpdate (unchanged).
- The backing fields are written only by EF (materialization / ExecuteUpdate), so
  CS0649 is suppressed locally with a URL-linked WHY note.

Hardening (same PR): the AI-tool path calls Habit.Create/Habit.Update directly,
bypassing the FluentValidation MaxReminderTimes (=10) cap. Added
HabitInvariants.ValidateReminderTimes (count <= DomainConstants.MaxReminderTimes,
already the single source of truth referenced by AppConstants.MaxReminderTimes)
and call it from both Habit.Create and Habit.Update, mirroring
ValidateScheduledReminders. Added domain tests: ==max passes, >max fails, for
both Create and Update. No existing caller legitimately exceeds the cap (HTTP,
onboarding, and bulk paths are already validator-capped).

Refs thomasluizon/orbit-ui-mobile#243, thomasluizon/orbit-ui-mobile#447

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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