Skip to content

fix: avoid ambiguous null check (CS9342) for types with multiple equality operators#2324

Merged
latonz merged 2 commits into
riok:mainfrom
fdipuma:fix/2316-ambiguous-null-check-operators
Jun 19, 2026
Merged

fix: avoid ambiguous null check (CS9342) for types with multiple equality operators#2324
latonz merged 2 commits into
riok:mainfrom
fdipuma:fix/2316-ambiguous-null-check-operators

Conversation

@fdipuma

@fdipuma fdipuma commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

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 check source.X != null could not be resolved by the compiler and failed with:

error CS9342: Operator resolution is ambiguous between the following members:
'Code.operator !=(Code?, Code?)' and 'Code.operator !=(Code?, string?)'

because the bare null literal is convertible to more than one operand type.

Fix

The null literal 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 mapping if (source.X != null))
  • MemberPathGetter (queryable projection source.X != null ? ... : default)
  • NullDelegateMapping (nested null handling)
  • IfNoneNull / IfAnyNull

Detection 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?)null output compiles.

Checklist

  • I did not use AI tools to generate this PR, or I have manually verified that the code is correct, optimal, and follows the project guidelines and architecture
  • I understand that low-quality, AI-generated PRs will be closed immediately without further explanation
  • The existing code style is followed
  • The commit message follows our guidelines
  • Performed a self-review of my code
  • Hard-to-understand areas of my code are commented
  • The documentation is updated (as applicable)
  • Unit tests are added/updated
  • Integration tests are added/updated (as applicable, especially if feature/bug depends on roslyn or framework version in use)

@fdipuma
fdipuma force-pushed the fix/2316-ambiguous-null-check-operators branch from 5a3c714 to 5fea3dd Compare June 16, 2026 14:13

@latonz latonz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the high-quality report and this contribution! I added my feedback 😊

Comment thread src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Condition.cs Outdated
Comment thread src/Riok.Mapperly/Helpers/SymbolExtensions.cs Outdated
@latonz latonz added the bug Something isn't working label Jun 19, 2026

@latonz latonz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this contribution! 💪🏻

@latonz

latonz commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Can you rebase onto main? Then I'll merge it 😊

@fdipuma
fdipuma force-pushed the fix/2316-ambiguous-null-check-operators branch from 018b1f3 to 63177a2 Compare June 19, 2026 16:21
@fdipuma

fdipuma commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Rebased! thank you @latonz :)

fdipuma added 2 commits June 19, 2026 21:44
…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
latonz force-pushed the fix/2316-ambiguous-null-check-operators branch from 63177a2 to 624af9c Compare June 19, 2026 19:44
@latonz
latonz merged commit fc42cd9 into riok:main Jun 19, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generated null checks fail to compile (CS9342) when the source type defines equality operators against its underlying primitive

2 participants