Skip to content

Enable nullable reference types across FEx (convention change)#19

Open
GioAbq wants to merge 7 commits into
developfrom
bugfix/nullable-enable
Open

Enable nullable reference types across FEx (convention change)#19
GioAbq wants to merge 7 commits into
developfrom
bugfix/nullable-enable

Conversation

@GioAbq

@GioAbq GioAbq commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What

Enables <Nullable>enable</Nullable> globally across the FEx library (convention change). Every project now compiles under nullable reference types with the zero-warning policy (TreatWarningsAsErrors=true), so all CS8xxx were build errors that had to be resolved.

Why

Nullable reference types catch null-dereference bugs at compile time and make the public API's nullability explicit for consumers. Deliberate convention change (previously "nullable disabled by default").

Scope

  • 55 src projects + 4 test projects annotated, migrated layer by layer (L0 foundational abstractions through L10 leaf projects, then tests).
  • Adapted to genuinely-nullable signatures: nullable params/returns/fields, widened override/interface members, where T : notnull, [NotNullWhen]/[return: MaybeNull], and Guard/GuardProperty (which carry [NotNull]) to re-narrow.
  • null!/default! used only for documented invariants and null-object no-ops; no blanket suppression, no #nullable directives (nullability stays props-driven).
  • Down-level TFMs (netstandard2.0 / net481): PolySharp added to DevConfigs to polyfill the System.Diagnostics.CodeAnalysis nullable attributes so annotations compile on every TFM.

Breaking changes

Some public API signatures were widened to nullable where the value is genuinely nullable (honest annotations over = null! suppression), acceptable per FEx strategy (internal lib; consumers migrate at their own pace). A few behavior deltas are documented in commit messages (some debug-view / paginated-list constructors now throw ArgumentNullException on null instead of a deferred NRE; a couple of already-null-tolerant methods gained nullable params).

Verification

  • dotnet build FEx.slnx green on all TFMs incl. net481 (0 errors, 0 warnings under TreatWarningsAsErrors).
  • 94/94 tests pass (Debug + Release).
  • ReSharper inspection gate: ERROR = 0 (redundancies removed; a handful of ReSharper-vs-nullable / multi-TFM false positives suppressed inline with justification).

Depends on

DevConfigs main @ c3568ac (Enabled Nullable + PolySharp), pinned via the submodule pointer.

Follow-ups (separate)

  • FuncProcessor.environmentId is validated but never stored - pre-existing smell, flagged for review.
  • IsNotNullOrEmptyReadOnlyCollection in Agnostics.Abstractions lacks [NotNullWhen(true)] (worked around locally).
  • Consumer projects (e.g. Automaton) bump their FEx / DevConfigs pointer when ready.

GioAbq added 7 commits July 10, 2026 00:07
…rors)

Enable nullable reference types across Layer 0 of the FEx nullable migration
(convention change: nullable now ON globally). Agnostics.Abstractions defines
the shared patterns: Guard helper annotated [NotNull]/[return: NotNull] (via a
SysNotNull alias to avoid CS0104 with JetBrains.Annotations); Error/Result
domain model made nullable where absence is valid; generic dictionary/set keys
constrained 'notnull'; no blanket null-forgiving. Bumps DevConfigs to the
PolySharp-enabled commit. Verified: all 4 L0 projects build green on every TFM.
L1: Agnostics, Agnostics.TestMocks, DependencyInjection.Abstractions, CLI, WebScraping, OneDrv.Abstractions
L2: DependencyInjection, MVVM.Abstractions, Logging.Abstractions, PersistentStorage.Abstractions, Json
L3: Core.Abstractions, Logging, PersistentStorage, OneDrv

Idiomatic nullable annotations: nullable params/returns/fields, widened
override/interface members, where T : notnull, [NotNullWhen]/[return: MaybeNull],
L0 Guard/GuardProperty for [NotNull] propagation. null!/default! only for
documented invariants and null-object no-ops. No #nullable directives, no blanket
suppression. Verified green build (0 err/0 warn) on all TFMs per project.
L4: Core, Asyncx, Sqlx.Abstractions, Maui, RESXx, Platforms, Platforms.Windows, Telemetry, Common.Abstractions, PersistentStorage.Rx, Encryption, Logging.Web
L5: MVVM, FileSystem, NuGetx, Telemetry.Sentry, Common, MVVM.Rx.Legacy, MVVM.Rx, Sqlx, EFCore, SecureStorage, AppSettings, Flurlx

Idiomatic nullable annotations adapting to now-nullable L0-L3 APIs: widened
module RegisterServices(TContainer?) + Guard re-narrow, where T : notnull on
concurrent dictionaries, nullable returns/params, override/interface signature
matches. null!/default! only for documented invariants and sibling-interface
contracts (each commented). No #nullable directives, no blanket suppression.
Verified green build (0 err/0 warn) on all TFMs per project incl. Maui/Windows.
L6: Webx, Flurlx.Newtonsoft, MSBuildx, Telemetry.Sentry.Web, Avaloniax
L7: Downloader, Legacy, AzureStorage
L8: FTPx, WPFx
L9: Imaging.Windows
L10: AzureDevOpsx

Completes the src nullable migration. Honest nullable signatures over
suppression, module RegisterServices(TContainer?)+Guard pattern, override/
interface matches. null!/default! only for documented invariants (WPF
DependencyProperty statics, framework-init fields), each commented. No #nullable
directives, no blanket suppression. Verified green (0 err/0 warn) all TFMs.

Follow-up (flagged by Webx): IsNotNullOrEmptyReadOnlyCollection in
Agnostics.Abstractions lacks [NotNullWhen(true)]; worked around locally.
…llable

test: DependencyInjection.Tests, MVVM.Tests, FileSystem.Tests, Flurlx.Tests.
Adapted test models/helpers to nullable (Child?/Name?/TestData.Name?), widened
TestInitializeModule override to match now-nullable RegisterServices/GetModule,
Task<TestData?> for the bulkhead lambda that returns null on rejection.

FileSystem root-cause (revealed by null-tolerance tests): GetGitWorktreeKind(this
DirectoryInfo?) and WhoIsLocking(string?) - both already null-check internally, so
the honest param is nullable; L4-L5 pass had left them non-null.

IDISP001 on GetModule() peek suppressed locally (default container owned by
FExServiceProvider, not the test) - repo-standard #pragma pattern.

Completes nullable migration: full `dotnet build FEx.slnx` green, 0 err / 0 warn.
…ct <Nullable>

CLAUDE.md: nullable is now enabled globally via DevConfigs (PolySharp polyfills
attributes on down-level TFMs); do not re-declare <Nullable> per project.
Removed the now-redundant <Nullable>enable</Nullable> from the 3 samples and
FEx.Building (inherited from DevConfigs/Directory.Build.props).
Clean fixes: removed redundant usings/base-ctor/casts/assignment, removed a
redundant optional-param default (explicit overload exists), !b -> !b.Value.

Justified suppressions (each commented; build of all TFMs incl. net481 stays
green, so none hides a real error):
- IDISP004 x4: Guard returns the same instance, does not create a disposable.
- RedundantCast x3: cast required on down-level TFMs (oblivious object/string)
  to keep the IDictionary<,object?>/<,string?> contract - verified CS8619 without it.
- SuggestVarOrType x2: var would infer non-null Type and break BaseType reassign.
- ArrangeRedundantParentheses x2: parens needed so ! applies to the ?. result.
- RedundantUsingDirective x1: CodeAnalysis using is used on net10.
- ParameterOnlyUsedForPreconditionCheck x1: pre-existing (environmentId validated,
  not stored) - flagged for separate review.
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