fix: avoid ambiguous null check (CS9342) for types with multiple equality operators#2324
Merged
latonz merged 2 commits intoJun 19, 2026
Merged
Conversation
fdipuma
force-pushed
the
fix/2316-ambiguous-null-check-operators
branch
from
June 16, 2026 14:13
5a3c714 to
5fea3dd
Compare
latonz
requested changes
Jun 19, 2026
latonz
approved these changes
Jun 19, 2026
latonz
left a comment
Contributor
There was a problem hiding this comment.
Thanks for this contribution! 💪🏻
Contributor
|
Can you rebase onto main? Then I'll merge it 😊 |
fdipuma
force-pushed
the
fix/2316-ambiguous-null-check-operators
branch
from
June 19, 2026 16:21
018b1f3 to
63177a2
Compare
Contributor
Author
|
Rebased! thank you @latonz :) |
…ty operators When a (nullable) source type defines multiple user-defined equality/inequality operator overloads (the strongly-typed value object pattern, e.g. both `==(Code?, Code?)` and `==(Code?, string?)`), the generated `source.X != null` null check could not be resolved by the compiler and failed with CS9342 "Operator resolution is ambiguous". The null literal is now cast to the nullable source type (e.g. `(Code?)null`) whenever the type declares more than one equality/inequality operator, which disambiguates the comparison while preserving behaviour. Types with a single operator overload (such as `string`) are unaffected. Fixes riok#2316
- Check op_Equality only for `==` (IsNull) and op_Inequality only for `!=` (IsNotNull) so the cast is emitted more precisely. - Walk the type hierarchy when looking for the operators, so operators declared on a base type (e.g. a shared value object base class) are detected as well. - Thread the operand type into the remaining null-check call sites that emit a bare null literal: the unsafe property/field accessors, the generic existing-target null guard and the IfNull/IfNullReturnOrThrow helpers (NullDelegateMethodMapping, MethodMemberNullAssignmentInitializerMapping). - Add tests for inherited operators and for the unsafe accessor receiver.
latonz
force-pushed
the
fix/2316-ambiguous-null-check-operators
branch
from
June 19, 2026 19:44
63177a2 to
624af9c
Compare
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.
Description
Fixes #2316.
When a (nullable) source type defines multiple user-defined equality/inequality operator overloads - the strongly-typed value object pattern, e.g. both
==(Code?, Code?)and==(Code?, string?)- Mapperly's generated null checksource.X != nullcould not be resolved by the compiler and failed with:because the bare
nullliteral is convertible to more than one operand type.Fix
The
nullliteral in the generated null checks is now cast to the nullable source type (e.g.(global::Code?)null) when the operand type declares more than one==/!=overload. This disambiguates the comparison while preserving behaviour, and works for both object mappings and queryable projection expression trees.Types with a single operator overload (such as
string) are left untouched, so there is no change to existing generated code (no snapshot churn).The cast is threaded through the existing null-check generation:
MemberNullDelegateAssignmentMapping(object mappingif (source.X != null))MemberPathGetter(queryable projectionsource.X != null ? ... : default)NullDelegateMapping(nested null handling)IfNoneNull/IfAnyNullDetection lives in a new
ITypeSymbol.HasAmbiguousEqualityOperators()helper.Tests
CastTest.NullableSourceWithMultipleUserDefinedEqualityOperatorsShouldCastNullLiteral: object mapping body.CastTest.NullableSourceWithMultipleUserDefinedEqualityOperatorsShouldCastNullLiteralInProjection: queryable projection snapshot.The full unit test suite passes with no other snapshot changes. Confirmed in a standalone compilation that the previous output fails with CS9342 while the new
(global::Code?)nulloutput compiles.Checklist