Skip to content

feat: suppress nullability warnings after an IsNotNull expectation - #1021

Merged
vbreuss merged 4 commits into
mainfrom
feat/is-not-null-suppressor
Sep 5, 2026
Merged

feat: suppress nullability warnings after an IsNotNull expectation#1021
vbreuss merged 4 commits into
mainfrom
feat/is-not-null-suppressor

Conversation

@vbreuss

@vbreuss vbreuss commented Sep 5, 2026

Copy link
Copy Markdown
Member

Add IsNotNullSuppressor, a DiagnosticSuppressor that silences CS8600, CS8602, CS8604 and CS8629 for a subject that a preceding Expect.That(subject).IsNotNull() verified to be not null.

The suppression only applies when the expectation is guaranteed to have been evaluated for the same subject: it must be a sequential statement preceding the warning in an enclosing block, no branching may separate the two and the subject must not be written to in between.

The analyzer testing packages are bumped to 1.1.4, because 1.1.2 throws on any suppressed diagnostic against Roslyn 4.11 and lacks WithIsSuppressed.

@vbreuss vbreuss self-assigned this Sep 5, 2026
Copilot AI lite review requested due to automatic review settings September 5, 2026 11:27
@vbreuss vbreuss added the enhancement New feature or request label Sep 5, 2026

Copilot AI 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.

🟢 Approval recommended

The implementation is well-scoped and backed by extensive suppressor-focused tests, with only a minor test naming clarity nit outstanding.

Pull request overview

Adds a new Roslyn DiagnosticSuppressor (IsNotNullSuppressor) to suppress specific C# nullability warnings (CS8600/CS8602/CS8604/CS8629) when a preceding Expect.That(subject).IsNotNull()-style expectation guarantees the same local/parameter subject was evaluated and not written to in between. This extends the aweXpect.Analyzers package to better align compiler nullability diagnostics with runtime-verified expectations, and introduces dedicated suppressor test infrastructure.

Changes:

  • Add IsNotNullSuppressor and suppression descriptors/justification resources for nullability warnings after not-null expectations.
  • Add a new C# suppressor verifier and a comprehensive suppressor test suite covering control-flow, scoping, and mutation cases.
  • Bump Microsoft.CodeAnalysis.*.Testing packages to 1.1.4 to support suppressed diagnostic assertions and avoid Roslyn 4.11 suppression issues.
File summaries
File Description
Tests/aweXpect.Analyzers.Tests/Verifiers/CSharpSuppressorVerifier.cs New verifier tailored to validating compiler warnings and suppressor behavior under nullable-enabled compilation.
Tests/aweXpect.Analyzers.Tests/IsNotNullSuppressorTests.cs New test suite validating when nullability warnings should/shouldn’t be suppressed.
Source/aweXpect.Analyzers/Rules.cs Adds suppression descriptors for the supported nullability warning IDs.
Source/aweXpect.Analyzers/Resources.resx Adds localized justification text for suppressions.
Source/aweXpect.Analyzers/Resources.Designer.cs Regenerated designer to expose the new resource string.
Source/aweXpect.Analyzers/IsNotNullSuppressor.cs Implements the suppressor logic and matching rules for safe suppression.
Directory.Packages.props Updates Roslyn analyzer testing package versions to 1.1.4.
Review details

Files not reviewed (1)

  • Source/aweXpect.Analyzers/Resources.Designer.cs: Generated file
  • Files reviewed: 6/7 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Tests/aweXpect.Analyzers.Tests/IsNotNullSuppressorTests.cs Outdated
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Test Results

     23 files   - 27       23 suites   - 27   9m 42s ⏱️ + 1m 46s
 19 938 tests  -  9   19 936 ✅  -  9  2 💤 ±0  0 ❌ ±0 
102 857 runs   -  3  102 855 ✅  -  3  2 💤 ±0  0 ❌ ±0 

Results for commit 739b154. ± Comparison against base commit f37799d.

This pull request removes 3245 and adds 3236 tests. Note that renamed tests count towards both.
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "message1a5cfa06-a733-4390-a2ba-78b47595c808")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "message24c610f8-822f-440b-b73a-e8bf0b2a7549")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "message37bf5be4-b0ae-408e-9441-e97a835034f1")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "message4db0a6d6-021d-4d7b-b101-f94c7b266e5f")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "message5d153227-77c7-4374-bb08-d625485b58a7")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ InnerException_ShouldBeSet(message: "messagef37808fd-12f0-4536-9b5c-eba70901ca97")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message1545f7c9-7886-4271-b1d0-73b17ac9809a")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message1ad004d6-0da1-4968-843d-3cac297f7c08")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message3decde00-8606-4159-a1c6-fb5153746b6d")
aweXpect.Core.Tests.Core.Exceptions.FailExceptionTests ‑ Message_ShouldBeSet(message: "message6761316f-a62c-4234-82c4-bf3deb5264b2")
…
aweXpect.Analyzers.Tests.IsNotNullSuppressorTests ‑ WhenControlFlowIsBetweenExpectationAndUsage_ShouldNotSuppressWarning
aweXpect.Analyzers.Tests.IsNotNullSuppressorTests ‑ WhenExpectationAllowsNullCollection_ShouldNotSuppressWarning
aweXpect.Analyzers.Tests.IsNotNullSuppressorTests ‑ WhenExpectationAllowsNullString_ShouldNotSuppressWarning
aweXpect.Analyzers.Tests.IsNotNullSuppressorTests ‑ WhenExpectationIsCombinedWithAnd_ShouldSuppressWarning
aweXpect.Analyzers.Tests.IsNotNullSuppressorTests ‑ WhenExpectationIsCombinedWithThatAll_ShouldSuppressWarning
aweXpect.Analyzers.Tests.IsNotNullSuppressorTests ‑ WhenExpectationIsCombinedWithThatAny_ShouldNotSuppressWarning
aweXpect.Analyzers.Tests.IsNotNullSuppressorTests ‑ WhenExpectationIsFollowedByOr_ShouldNotSuppressWarning
aweXpect.Analyzers.Tests.IsNotNullSuppressorTests ‑ WhenExpectationIsInConditionalBlock_ShouldNotSuppressWarning
aweXpect.Analyzers.Tests.IsNotNullSuppressorTests ‑ WhenExpectationIsInConditionalExpression_ShouldNotSuppressWarning
aweXpect.Analyzers.Tests.IsNotNullSuppressorTests ‑ WhenExpectationIsInsideLambda_ShouldNotSuppressWarning
…

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

🚀 Benchmark Results

Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
INTEL XEON PLATINUM 8573C 2.30GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.303
[Host] : .NET 8.0.30 (8.0.30, 8.0.3026.36720), X64 RyuJIT x86-64-v4

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Method Mean Error StdDev Gen0 Gen1 Allocated
Bool_aweXpect 268.7 ns 2.40 ns 2.24 ns 0.0081 - 696 B
Bool_FluentAssertions 291.9 ns 3.17 ns 2.97 ns 0.0110 - 952 B
Equivalency_aweXpect 319,628.8 ns 804.93 ns 752.93 ns 3.9063 - 335444 B
Equivalency_FluentAssertions 2,140,838.7 ns 20,320.44 ns 19,007.75 ns 54.6875 7.8125 4841613 B
Int_GreaterThan_aweXpect 278.9 ns 3.98 ns 3.72 ns 0.0100 - 864 B
Int_GreaterThan_FluentAssertions 333.3 ns 2.68 ns 2.38 ns 0.0143 - 1224 B
ItemsCount_AtLeast_aweXpect 520.9 ns 3.61 ns 3.38 ns 0.0162 - 1360 B
ItemsCount_AtLeast_FluentAssertions 548.1 ns 5.80 ns 5.14 ns 0.0238 - 2008 B
String_aweXpect 454.3 ns 2.83 ns 2.36 ns 0.0134 - 1128 B
String_FluentAssertions 1,252.1 ns 6.64 ns 6.21 ns 0.0458 - 3944 B
StringArray_aweXpect 1,435.9 ns 5.09 ns 4.51 ns 0.0305 - 2624 B
StringArray_FluentAssertions 1,433.1 ns 10.28 ns 9.61 ns 0.0496 - 4152 B
StringArrayInAnyOrder_aweXpect 1,877.6 ns 4.56 ns 4.04 ns 0.0324 - 2816 B
StringArrayInAnyOrder_FluentAssertions 17,601.3 ns 100.85 ns 94.33 ns 0.3967 - 33465 B

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

👽 Mutation Results

Mutation testing badge

aweXpect

Details
File Score Killed Survived Timeout No Coverage Ignored Compile Errors Runtime Errors Total Detected Total Undetected Total Mutants

The final mutation score is NaN%

Coverage Thresholds: high:80 low:60 break:0

aweXpect.Core

Details
File Score Killed Survived Timeout No Coverage Ignored Compile Errors Runtime Errors Total Detected Total Undetected Total Mutants

The final mutation score is NaN%

Coverage Thresholds: high:80 low:60 break:0

Copilot AI review requested due to automatic review settings September 5, 2026 11:47
@vbreuss
vbreuss force-pushed the feat/is-not-null-suppressor branch from 4940798 to e3ff5f6 Compare September 5, 2026 11:47
Add `IsNotNullSuppressor`, a `DiagnosticSuppressor` that silences CS8600,
CS8602, CS8604 and CS8629 for a subject that a preceding
`Expect.That(subject).IsNotNull()` verified to be not null.

The suppression only applies when the expectation is guaranteed to have been
evaluated for the same subject: it must be a sequential statement preceding
the warning in an enclosing block, no branching may separate the two and the
subject must not be written to in between.

The analyzer testing packages are bumped to 1.1.4, because 1.1.2 throws on any
suppressed diagnostic against Roslyn 4.11 and lacks `WithIsSuppressed`.
The suppressor silenced nullability warnings in cases where the subject
could still be null:

- a loop reaches the usage again, so a write at the end of an iteration
  invalidates an expectation from outside the loop
- `Expect.ThatAny` only requires any of its expectations to be met, just
  like the already excluded `Or`
- the warning for `subject.Member` was matched against an expectation for
  `subject`
- a write via `this._subject` or a deconstruction was not detected
- a property could return a different value on each access

Subjects are now restricted to local variables and parameters, which also
covers writes through a lambda that captures them.

Additionally, `IsNotNullOrEmpty` and `IsNotNullOrWhiteSpace` are accepted
as expectations that guarantee a not-null subject. The result type is not
used to detect them, because it also replaces the nullable subject type
for expectations that a null subject does fulfil, e.g. `IsNotEmpty` on a
string or `IsNotEqualTo` on a collection.

Copilot AI 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.

🟡 Changes recommended

The suppressor’s control-flow invalidation is stricter than described (treats non-branching statements as “branching”), and write-detection incorrectly treats in arguments as writes, leading to incorrect/overly-restrictive behavior.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Files not reviewed (1)

  • Source/aweXpect.Analyzers/Resources.Designer.cs: Generated file
  • Files reviewed: 6/7 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread Source/aweXpect.Analyzers/IsNotNullSuppressor.cs Outdated
Comment thread Source/aweXpect.Analyzers/IsNotNullSuppressor.cs
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 5, 2026 11:53
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI 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.

🔵 Needs a closer look

The write-tracking logic misses ++/-- mutations, which can incorrectly allow suppression after the subject was modified between the expectation and the warned usage.

Review details

Files not reviewed (1)

  • Source/aweXpect.Analyzers/Resources.Designer.cs: Generated file

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

Source/aweXpect.Analyzers/IsNotNullSuppressor.cs:274

  • WritesTo(...) currently treats assignments and ref/out arguments as writes, but it misses ++/-- operations (prefix/postfix unary), which also mutate the subject and should invalidate a preceding IsNotNull expectation per the stated rules ("subject must not be written to in between"). This can lead to suppressing a nullability warning even though the subject was modified between expectation and usage.
  • Files reviewed: 6/7 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@sonarqubecloud

sonarqubecloud Bot commented Sep 5, 2026

Copy link
Copy Markdown

@vbreuss
vbreuss merged commit 99b6899 into main Sep 5, 2026
14 checks passed
@vbreuss
vbreuss deleted the feat/is-not-null-suppressor branch September 5, 2026 14:40
github-actions Bot added a commit that referenced this pull request Sep 5, 2026
…er an `IsNotNull` expectation (#1021) by Valentin Breuß
github-actions Bot added a commit that referenced this pull request Sep 5, 2026
…er an `IsNotNull` expectation (#1021) by Valentin Breuß
@github-actions github-actions Bot added the state: released The issue is released label Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

This is addressed in release v2.37.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request state: released The issue is released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants