Repository navigation
Remove the null-forgiving operators - #413
Merged
Merged
Conversation
Map tags using a loop, to prevent duplicate keys in a dictionary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An operator hides the reason a value cannot be null. A pattern or a parameter now carries that reason, and Debug.Assert states it where neither can. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An operator hides the reason a value cannot be null. PinMove carries the non-null target of a moving pin, AppDirectiveEditor.RewriteVersions passes the version text to its callback, and Debug.Assert states what OverrideSelector guarantees. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Where a pattern match can prove the value non-null, the pattern match replaces the operator. Elsewhere, Debug.Assert names the guarantee the code relies on, and checks it in Debug builds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chain message checks onto Throws<T>(). Elsewhere, use methods with non-null returns, and Debug.Assert for asynchronous acts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assert intermediate nodes with IsNotNull(), use ?. for value comparisons. Synchronous helpers pass the null on, or throw. Use Debug.Assert as a last resort. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assert nullable results with IsNotNull(), use ?. for value comparisons, and filter lambdas with OfType<T>(). AssemblyExtensions.GetMetadata replaces AssemblyMetadataAttribute lookups. A null passed on purpose to test null detection keeps the operator, under a justification comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lines were pre-existing, but we touched the files in the same PR so they get the fixes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Common.props turned the analyzer off while the null-forgiving operators lacked a fix. Now we can enable it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Five Act local functions returned a value that the lambda passed to Assert.Throws discarded. FileBasedAppHelperTests compared two string? values with IsNotEqualTo, which ReSharper reports as CS8620. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #413 +/- ##
=======================================
Coverage 87.28% 87.28%
=======================================
Files 321 323 +2
Lines 9514 9555 +41
Branches 1793 1805 +12
=======================================
+ Hits 8304 8340 +36
- Misses 911 913 +2
- Partials 299 302 +3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
rdeago
commented
Oct 28, 2026
rdeago
left a comment
Member
Author
There was a problem hiding this comment.
Review by Claude. Self-review.
One finding, on the tests of GitService.GetLatestVersions.
GetLatestVersions_WithTwoVersionTagsOnOneCommit_ReadsBoth had one stable tag and two candidates for Latest, sorted alike by name and by version, so a pick by list order passed it. GetLatestVersions_WithSeveralVersionTagsOnOneCommit_ReadsTheHighest replaces it, and puts three stable and three prerelease tags on one commit. The highest version of each kind is neither first nor last, by name or by creation order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rdeago
commented
Oct 28, 2026
rdeago
left a comment
Member
Author
There was a problem hiding this comment.
Review by Claude. Self-review.
👍 Approved.
No blocking findings. GetLatestVersions_WithSeveralVersionTagsOnOneCommit_ReadsTheHighest addresses the previous finding.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Checklist of related issues / discussions
Proposed changes
A null-forgiving operator (
!) tells the compiler that a value is not null, and the compiler stops checking it. The fact behind the operator then lives in no type. #408 asks to remove the operators undersrc/, and to add a rule against them tocsharp-style-guide.md. #412 addedNullable.Extended.Analyzerto Buildvana SDK, andCommon.propsturned it off until the operators were gone.This PR removes every operator, under
src/,tests/, and.buildvana/hooks/. It then removesUseNullableExtendedAnalyzer=falsefromCommon.props, so the analyzer reports any new operator.src/and the hookThe fixes follow the order the new style guide section prescribes:
PinWriting.Movingreturns each moving pin as aPinMove, with its non-null target.AppDirectiveEditor.RewriteVersionspasses the version text to its callback as a non-null argument.PackagePinDumpsyields each pin dump with its non-nullProjectAssetsFile, forOrphanDetectorandOverrideProject.TryGetKeyedElementTypereturns theJsonKeyedObjectAttributewith the element type.boollocals, inSelfVersionService,GitignorePattern,ConstantValueParser,SidecarReader, and the JSON code.Debug.Assert, as a last resort. 22 assertions insrc/, and one inpost-release.cs. Each message says why the value cannot be null. Seven of them, and the one in the hook, assert thatPath.GetDirectoryNameof a full file path is not null.No operator is left under
src/. #408 admitted operators with a comment stating the invariant.Debug.Assertstates the invariant instead, and checks it in every Debug build.tests/#408 left open whether
tests/follows the rule. It does, with one exception:await Assert.That(value).IsNotNull()returns the value, non-null, where a test usedvalue!.?.fails on a null, as innode["a"]?["b"]?.GetValue<string>().WithMessageorWithMessageContainingchained ontoThrows<T>()replacesexception!.Message.AssemblyExtensions.GetMetadata, inBuildvana.Core.Testing, replaces 22 lookups ofAssemblyMetadataAttribute.Value!.InvalidOperationExceptionwhen the null comes from the code under test.The exception is a
null!ordefault!that a test passes on purpose, as the input under test. 29 operators keep the!, each under the comment// ! Null on purpose: testing null detection.OrphanDetectorTestsandOverrideLifecycleTestspassednew Lazy<SolutionContext>(static () => null!).OrphanDetectorreads the value, so the lazy now returns an emptySolutionContext.The style guide
csharp-style-guide.mdhas a new section, "The null-forgiving operator". It bans the operator, and lists the fixes above in order of preference. It does not allow the// !comment, except for the test inputs above.Additional changes
GitService.GetLatestVersionsthrewArgumentExceptionwhen two version tags named the same commit.ToDictionarykeyed the tags by commit. The fix keeps every version tag of a commit.Latestis the highest version on the first tagged commit, walking fromHEAD.LatestStableis the highest stable version on the first commit with a stable tag.GitServiceTestsreproduces the throw.IGitServiceanddocs/tool-commands/version.mddescribe the rule, andCHANGELOG.mdhas a bullet.Types of changes
This pull request introduces the following types of changes:
.gitattributes,.gitignore)Breaking changes
This pull request introduces breaking changes:
Checklist
🤖 Generated with Claude Code