Skip to content

chore: enable the expanded StyleSharp analyzer rule set and resolve all violations#4385

Merged
glennawatson merged 35 commits into
mainfrom
feature/analyzer-cleanup-sst313
Jun 24, 2026
Merged

chore: enable the expanded StyleSharp analyzer rule set and resolve all violations#4385
glennawatson merged 35 commits into
mainfrom
feature/analyzer-cleanup-sst313

Conversation

@glennawatson

@glennawatson glennawatson commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

Build / chore + behaviour-preserving refactor. Enables the expanded StyleSharp 3.13.x rule set across the repo, fixes every resulting violation (no #pragma/SuppressMessage suppressions), and stops the ReactiveUI.Primitives analyzers from flowing to downstream consumers.

What is the new behavior?

  • The expanded SST rule set is enabled and the entire reactiveui.slnx builds cleandotnet build reactiveui.slnx -c Release → 0 errors across all libraries, test projects, examples and benchmarks. Apple TFMs (ios/maccatalyst/tvos/macos) verified separately on a Windows guest.
  • Code brought up to the new rules: production System.Linq converted to explicit iteration (fusing passes / pre-sizing, not a 1:1 translation), the WhenAny Arity11/12 readiness &&-chains replaced with an O(1) _ready counter, return-only branches collapsed, discards, nameof, target-typed new, expression-bodied accessors, etc.
  • Analyzers are now opt-in for consumers: every ReactiveUI.Primitives* PackageReference carries ExcludeAssets="analyzers", so installing ReactiveUI no longer drags the Primitives analyzers into a downstream project. The README documents how to opt in.
  • Dependency bumps: StyleSharp.Analyzers 3.11.2 → 3.13.4, ReactiveUI.Primitives → 5.6.0, TUnit → 1.56.25.
  • Canonical de-duplication: CA1040, S2357, SST2205 disabled in .editorconfig as duplicates of the canonical SST/IDE rules already enforced.

What is the current behavior?

A narrower analyzer rule set, with a backlog of violations the new rules surface. The Primitives analyzers were excluded on only ReactiveUI / ReactiveUI.Core / ReactiveUI.Reactive, so the eight platform packages (Wpf, Winforms, WinUI, Maui + their .Reactive variants) still flowed those analyzers to downstream users.

What might this PR break?

  • Behaviour: none intended — the refactors are behaviour-preserving and verified by the existing suites (ReactiveUI.Tests 1872, Builder 217, AOT 49, Blazor 22, Splat 6, Routing 10, plus the WhenAny arity tests that exercise the rewritten sinks).
  • Consumer-facing: the only change is that the Primitives analyzers no longer run automatically in downstream projects that install ReactiveUI's platform packages. This is intentional (opt-in); no API or runtime change.

Checklist

  • I have read the Contribute guide
  • Tests have been added or updated (for bug fixes / features) — existing suites cover the behaviour-preserving refactors; no new behaviour added
  • Docs have been added or updated (for bug fixes / features) — README "Analyzers are opt-in" section
  • Changes target the main branch
  • PR title follows Conventional Commits

Additional information

Several of the new rules had false positives, each reported and fixed in a StyleSharp patch release rather than worked around:

FP Shipped fix
SST1117 — classic extension methods (this.Log()) 3.13.1
SST1117 — C# 14 extension-block members (this.WhenAnyValue) 3.13.2
SST2218 — overload-disambiguating lambda parameters 3.13.3
SST1440 — Blazor @onclick handlers bound in markup 3.13.4

Verification: full reactiveui.slnx build is green on Linux (Windows TFMs via EnableWindowsTargeting); Apple TFMs of ReactiveUI, ReactiveUI.Reactive and ReactiveUI.Maui build 0/0 on a real Windows guest across net10.0-ios/-maccatalyst/-tvos/-macos.

- Bump StyleSharp.Analyzers 3.11.2 -> 3.13.2, Primitives 5.6.0, TUnit 1.56.25
- Enable the new SST rule set and resolve every violation across the core,
  shared, and platform libraries and the test projects, without suppressions
- Convert production LINQ to explicit iteration (SST2233), fusing passes and
  pre-sizing buffers rather than translating one-to-one
- Replace the WhenAny Arity11/12 readiness AND-chains with an O(1) ready
  counter to satisfy the branch-complexity limit (SST1442)
- Collapse return-only branches, assign ignored results to discards, prefer
  nameof, and apply the remaining modern-usage rules
- Disable CA1040, S2357, and SST2205 as duplicates of canonical SST/IDE rules
- Resolve multi-TFM name-ambiguity from name shortening via using aliases
  (System.Linq.Expressions.Expression in WPF)

Verified: ReactiveUI.Tests (1872), Builder (217), AOT (49), Blazor (22),
Splat (6), Routing (10) all green; libraries and Windows test projects
compile clean.
CI surfaced violations in platform TFMs not buildable in the prior Linux
pass (net8.0 / net8.0-windows only):

- android FlexibleCommandBinder: LINQ affinity lookups -> explicit loops (SST2233)
- Maui: discards, nameof, target-typed new, collapsed branch, const message;
  resolve the multi-TFM Application cref ambiguity (Android.App.Application)
  with a using alias
- benchmarks: target-typed new (SST2202)
- fix a net462/net481 nullable regression in ValidationBindingWpf by narrowing
  with a property pattern (CS8604)

Verified: ReactiveUI android (net11.0-android37), ReactiveUI.Maui
(net9.0 + android), benchmarks (net9.0), Wpf (net8.0-windows) all clean.
Verified on the dockur Windows guest across net10.0-ios, -maccatalyst, -tvos,
and -macos (ReactiveUI, ReactiveUI.Reactive, ReactiveUI.Maui) — all 0/0.

- apple-common / uikit-common: target-typed new, discards, null-guard throws
  folded into ?? throw, and Enumerable.Range/Select chains (CommonReactiveSource,
  IndexNormalizer, KVOObservableForProperty) rewritten as explicit loops
- KVOObservableForProperty: extract reflection lookups into helpers to stay
  under the nested-flow complexity limit
- CommonReactiveSource: concrete List<Update> return types (CA1859)
- ios/tvos LinkerOverrides: rewrite the self-referential get+set linker stubs as
  initializer (setter) + discard read (getter), preserving both accessors
- mac: discards and unbound nameof generic argument
- RoutedViewHost: rename a lambda parameter so the discard does not collide with
  the '_' parameter name
- UIViewControllerMixins: internal (not private) extension member used across
  sibling extension blocks
3.13.3 fixes the SST2218 false positive on overload-disambiguating lambda
parameters. ReactiveUI.Wpf.Tests and ReactiveUI.WinForms.Tests now build clean
without the temporary NoWarn — no analyzer suppressions remain.
ReactiveUI.Primitives ships analyzers; without ExcludeAssets="analyzers" they
flow transitively to anyone installing ReactiveUI's platform packages, forcing
our analyzers on consumers who didn't opt in. Core/ReactiveUI/Reactive already
excluded them — add the same to the 8 platform projects (Wpf, Winforms, WinUI,
Maui and their .Reactive variants) so it's consistent.

ExcludeAssets="analyzers" drops only the analyzer asset; compile/runtime stay.
README documents that analyzers are opt-in.
- ReactiveUI.AndroidX (+ .Reactive) and the shared ChangeSetBinder: discards
- ReactiveUI.Maui.Tests / Builder.Maui.Tests: discards, shorten names, move
  assignment to initializer, drop redundant parens
- Example apps (BlazorServer, WpfApp, Wpf, Winforms, Samples.Maui): discards and
  LINQ FirstOrDefault -> explicit loop

Verified clean: AndroidX (android), Maui test projects (net9.0), and four of the
five examples. BlazorServer still trips SST1440 on Razor @OnClick handlers that
the analyzer can't see in markup (pending analyzer fix).
A full `dotnet build reactiveui.slnx` surfaced TFM-specific files the
per-project net8.0 builds missed:

- Samples.Maui (LoginPage.xaml.cs, MauiProgram.cs) and Testing.Tests: discards
- benchmarks ObservableBenchmarkViewModel: add `using ReactiveUI.Primitives.Signals`
  so `Signal<int>` resolves by arity (the non-generic Signal factory is in scope
  globally), keeping the shortened name
- bump StyleSharp.Analyzers to 3.13.4 (fixes the SST1440 Blazor @OnClick FP)

Full slnx now builds clean (0 errors); Apple TFMs verified separately in dockur.
@glennawatson glennawatson changed the title chore: enable expanded StyleSharp analyzers and fix all violations chore: enable the expanded StyleSharp analyzer rule set and resolve all violations Jun 24, 2026
@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.02131% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.07%. Comparing base (30dcf2f) to head (b316c35).

Files with missing lines Patch % Lines
...ctiveUI.Wpf.Shared/Binding/ValidationBindingWpf.cs 72.72% 2 Missing and 4 partials ⚠️
...c/ReactiveUI.Core/Expression/ExpressionRewriter.cs 80.00% 0 Missing and 1 partial ⚠️
src/ReactiveUI.Shared/Activation/ViewForMixins.cs 88.88% 0 Missing and 1 partial ⚠️
...erty/Internal/PropertyBindingExpressionCompiler.cs 0.00% 0 Missing and 1 partial ⚠️
...operty/PropertyBinderImplementation.Conversions.cs 90.90% 0 Missing and 1 partial ⚠️
src/ReactiveUI.Shared/Routing/MessageBus.cs 66.66% 1 Missing ⚠️
...activeUI.Winforms/CreatesWinformsCommandBinding.cs 92.85% 0 Missing and 1 partial ⚠️
...veUI.Wpf.Shared/WpfPropertyBinderImplementation.cs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4385      +/-   ##
==========================================
+ Coverage   89.69%   93.07%   +3.37%     
==========================================
  Files         341      343       +2     
  Lines       14948    14924      -24     
  Branches     1502     1570      +68     
==========================================
+ Hits        13408    13890     +482     
+ Misses       1208      770     -438     
+ Partials      332      264      -68     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

- Remove ReactiveUI.Core/Mixins/CompatMixins.cs: ForEach/SkipLast had no
  production callers and SkipLast was shadowed by BCL's Enumerable.SkipLast
- Add tests for BindingFallbackConverterRegistry and
  SetMethodBindingConverterRegistry (GetAllConverters empty/registered,
  TryGetConverter on an empty registry)
…to the default

The switch listed every ExpressionType explicitly only to throw the same
NotSupportedException the default arm already produces. Collapse the ~80
redundant arms into a single default that interpolates the node type, keeping
the supported Index/MemberAccess cases and the message wording.
…ctly

Lift MessageBus's nested SkipFirstObserver into a top-level internal class so
its skip-first/error/completion forwarding can be unit-tested directly.
…n file

The shared UIKit control-preservation code was duplicated between the ios and
tvos LinkerOverrides (flagged as duplicated new code by Sonar). Move the common
controls into a single Platforms/uikit-common/LinkerOverrides.cs (already
compiled into ios, tvos and maccatalyst) and keep the iOS-only controls
(UISlider, UIRefreshControl, UISwitch) in a separate IosLinkerOverrides class.
Verified ios, tvos and maccatalyst compile 0/0 in the dockur guest.
…visual tree

The existing binding tests construct the binding against an unrealized Window
whose visual tree has no children, so FindControlByName fails and the
constructor throws (swallowed by try/catch), leaving Bind() and the binding
setup uncovered. Realize a UserControl's visual tree with Measure/Arrange/
UpdateLayout so the control is found and the two-way binding is exercised
end-to-end. Verified passing on real Windows in the dockur guest.
…Hook branches)

Verified passing on real Windows in the dockur guest.
…andler overloads

The existing tests only exercised the default-event overload; add coverage for
the named-event, non-generic add/remove, and generic add/remove overloads.
Verified passing on real Windows in the dockur guest.
Verified passing on real Windows in the dockur guest.
…er success path

The previous test wrongly assumed a read-only property yields a null setter and
returns false; GetValueSetterForProperty returns PropertyInfo.SetValue (non-null)
for read-only properties, which throws on invoke. Test the writable-member
success path instead, which exercises the same reflection API.
view is null-checked before line 259 in ViewModelViewHost; section is
dereferenced at line 163 in CommonReactiveSource, so the ?. on it is redundant.
…ndRemove

The three AutoPersistCollection overloads each repeated the same
dispose-and-remove cleanup callback (flagged as duplicated by Sonar). Extract a
shared DisposeAndRemove helper.
ReactiveAppCompatActivity and ReactiveFragmentActivity each nested an identical
ActivityResultAwaiter (flagged as duplicated by Sonar), differing only in the
Intent nullability of the result tuple. Lift it into a single shared
ActivityResultAwaiter and unify ReactiveFragmentActivity's ActivityResult /
StartActivityForResultAsync to Intent? (matching ReactiveAppCompatActivity) —
a correctness fix, since activity results can legitimately carry a null intent
(its OnActivityResult no longer throws on null data). PublicAPI baselines
updated for the net10/net11 android TFMs; both build clean.
Add IViewFor<TViewModel> wrappers for two MAUI host types that previously had
no reactive equivalent:

- ReactiveWindow<TViewModel> — mirrors the existing WPF/WinUI/Mac ReactiveWindow
  and aligns with .NET 9's CreateWindow direction (Window is now the primary-page
  host after Application.MainPage was deprecated).
- ReactiveTitleBar<TViewModel> — wraps the .NET 9 TitleBar control for VM-bound
  custom title bars on Windows / Mac Catalyst.

Both follow the established Reactive* pattern (ViewModel bindable property,
two-way ViewModel<->BindingContext sync). PublicAPI baselines updated (Shipped)
across all MAUI TFMs; net9.0 + net10.0-android36.0 build clean. Adds full unit
coverage for both controls (12 tests).
…nd new MAUI controls

The ReactiveUI.AndroidX.Reactive and ReactiveUI.Maui.Reactive shim projects
compile the same sources under the ReactiveUI.Reactive.* namespace and carry
their own PublicAPI baselines, which were missed:

- AndroidX.Reactive: apply the ReactiveFragmentActivity Intent! -> Intent?
  result-tuple change (net10/net11 android).
- Maui.Reactive: add ReactiveWindow/ReactiveTitleBar entries (shipped).

Both projects now build clean.
- ReactiveProperty.Dispose: drop the redundant null-conditional on _disposables
  (the guard above already proves it non-null).
- RoutedViewHost (MAUI): extract page resolution into ResolveCurrentPage so the
  null-check operates on a method result — Sonar can't see the page being set
  inside the inline Subscribe callback and wrongly flagged the branch unreachable.
- InteractionHandleObservable: move the "_stepRanInline" pending check into an
  AdoptIfPending helper. The field is set by a callback that ScheduleOrInline may
  run inline or defer; Sonar assumes scheduler.Schedule runs synchronously and
  flagged the condition constant. Behaviour is identical.
- Adds coverage for Activation, Builder, ChangeSet, Interaction, RoutingState, DependencyResolver, and other core mixin/extensions.
- Validates correct functionality for corner cases such as fallback conversions, lifecycle activation, event handling, and state navigation.
- Introduces mock components and specialized stubs to exercise and verify framework behavior across platforms (WinForms, WPF, etc.).
glennawatson and others added 7 commits June 24, 2026 19:33
…l assertions

- Add validation for the `Initialized` property of `ParameterlessType` alongside type checks.
- Update `ParameterlessType` to use `Initialized` instead of `Created`, reflecting its construction state.
…overage

- mark ValueChangeRelay and AsyncProjectObservable.Sink OnError/OnCompleted
  with [ExcludeFromCodeCoverage]: these interface-required callbacks are never
  invoked because the subscription is always disposed before the source can
  complete or error, so they were permanently-uncovered defensive code
…NetworkService

- `CHAT_MULTICAST_ADDRESS` allows overriding the default hardcoded address.
- Updated `MulticastAddress` to dynamically configure from the environment variable if set.
- Retains fallback to the default address.
…minal

- remove the UDP/multicast chat sample, including its hard-coded multicast
  address (resolves the SonarCloud S1313 hard-coded-IP finding by deletion)
- add a self-contained mock EFTPOS terminal that exercises a lot of the
  ReactiveUI surface with no networking: the builder, routing between a
  terminal and a journal screen, a typed suspension host that persists the
  transaction journal to JSON, an async ReactiveCommand payment flow with
  IsExecuting/canExecute, ObservableAsPropertyHelper-derived outputs and the
  message bus
Push the page before mutating the router stack so the headless navigation
stack sync never PushAsync-es into an empty navigation stack. net10 MAUI
rejected that with an out-of-range List.Insert (net9 tolerated it), failing
InvalidateCurrentViewModel_NullPageViewModel_DoesNotUpdate in CI.
- Added a `Flush` method to drain the dispatcher queue and ensure all marshalled work is processed before assertions.
- Updated `WpfPropertyBinderImplementationTests` to invoke `Flush` for consistent synchronization in tests
Ensure PayCommand notifications are observed on the main UI scheduler by using PayCommand.WitnessOn(RxSchedulers.MainThreadScheduler) so UI-bound state (Journal, LastTransaction) is updated safely. Also call ApplicationConfiguration.Initialize() before building the WinForms app (and remove the duplicate call) so application configuration is applied early.
@sonarqubecloud

Copy link
Copy Markdown

@glennawatson glennawatson merged commit 6dc538e into main Jun 24, 2026
13 checks passed
@glennawatson glennawatson deleted the feature/analyzer-cleanup-sst313 branch June 24, 2026 23:10
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