Skip to content

fix: Add null checks when mapping nullable sources to non-nullable targets with MayBeNull Attribute#2256

Draft
Abhijithtv wants to merge 12 commits intoriok:mainfrom
Abhijithtv:fix/target-ignore-maybenull
Draft

fix: Add null checks when mapping nullable sources to non-nullable targets with MayBeNull Attribute#2256
Abhijithtv wants to merge 12 commits intoriok:mainfrom
Abhijithtv:fix/target-ignore-maybenull

Conversation

@Abhijithtv
Copy link
Copy Markdown

@Abhijithtv Abhijithtv commented May 1, 2026

Add null checks when mapping nullable sources to non-nullable targets with MayBeNull Attribute

Description

For target members, [MaybeNull] should not be considered when determining nullability.
Otherwise, it may lead to incorrect nullable analysis and result in CS8601 ("Possible null reference assignment") warnings.

Fixes #2192

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)

@Abhijithtv Abhijithtv changed the title fix: Add null checks when mapping nullable sources to non-nullable targets with MayBeNull Attribute #2192 fix: Add null checks when mapping nullable sources to non-nullable targets with MayBeNull Attribute May 1, 2026
@latonz
Copy link
Copy Markdown
Contributor

latonz commented May 1, 2026

Thanks for the contribution! The fix turns out to be a bit more involved than it might initially appear, IMO also my original MayBeNull support PR was short-sighted. To handle this correctly, I think we need to support both MayBeNull (when reading values) and AllowNull (when writing values).

@Abhijithtv
Copy link
Copy Markdown
Author

Thanks, that makes sense!

I checked and it looks like AllowNull isn't currently considered in target nullability checks (I also couldn’t find any tests covering it).

Would you prefer this to be handled in a separate PR focusing on AllowNull support?

Also, regarding the implementation: would you recommend modeling read and write nullability as separate properties (e.g., sourceMayBeNull vs targetAcceptsNull), or passing a context flag like isRead/isWrite?

@latonz
Copy link
Copy Markdown
Contributor

latonz commented May 1, 2026

As these two strongly belong together and the changes shouldn't get too big, I'd do it in the same PR.
I'd prefer separate properties/methods.

todo - add test for allow Null
Copy link
Copy Markdown
Contributor

@latonz latonz left a comment

Choose a reason for hiding this comment

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

Thanks for the updates, I added my feedback.

Please also update the 5-0.md breaking changes docs.

);
.HaveDiagnostic(
DiagnosticDescriptors.MapValueMethodTypeMismatch,
"Cannot assign method return type string of BuildValue() to B.Value of type string"
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.

This error message is misleading (string mismatch vs string).

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.

The test name no longer matches the assertion — it used to allow the nullable return, it now diagnoses it. Please rename to something like MethodToMaybeNullTargetPropertyShouldDiagnoseNullableReturnType.

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.

Same as above, name says "should map", body now asserts a diagnostic. Please rename and consider whether this case (both sides annotated [MaybeNull]) is what users actually expect to break. Testing [AllowNull] as the way to opt into nullable writes would help here too.

@@ -92,6 +92,18 @@ private bool IsAccessible(ISymbol symbol, MemberVisibility visibility)
}

public bool IsNullable(ISymbol symbol)
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.

Three near-identical helpers (IsNullable, IsReadNullable, IsWriteNullable) is a foot-gun, IsNullable now ignores both [MaybeNull] and [AllowNull], so any future call site that picks it without thinking will reintroduce the bug this PR fixes. Could the IsNullable methods be made private?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I have removed the IsNullable and replaced all its reverences with IsReadNullable and IsWriteNullable

Comment thread src/Riok.Mapperly/Descriptors/SymbolAccessor.cs
Comment on lines +23 to +24
public bool IsReadNullable => parameter.Type.IsNullable();
public bool IsWriteNullable => parameter.Type.IsNullable();
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.

These ignore [MaybeNull] / [AllowNull] on the parameter symbol, while ConstructorParameterMember correctly delegates to SymbolAccessor.IsReadNullable/IsWriteNullable

@latonz latonz added bug Something isn't working enhancement New feature or request labels May 2, 2026
@Abhijithtv Abhijithtv marked this pull request as draft May 3, 2026 09:16
public ITypeSymbol Type => parameter.Type;
public INamedTypeSymbol? ContainingType => null;
public bool IsNullable => parameter.Type.IsNullable();
public bool IsReadNullable =>
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

For MethodMapping, is it sufficient to rely on the parameter type to determine nullability?

Currently, we do not store the parameter symbol within MethodMapping (I’m not sure whether this is feasible or intended). Could you provide some guidance on how nullability should be handled in this case?

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.

We can't only rely on the type as that would ignore these attributes.

I think storing the symbol within the method parameter should be okay. I'd add helpers though (IsWriteNullable and IsReadNullable).

@Abhijithtv Abhijithtv marked this pull request as ready for review May 4, 2026 19:58
@Abhijithtv
Copy link
Copy Markdown
Author

@latonz Thanks for the review.

I realized I forgot to mark the PR as a draft earlier. Your feedback was really helpful.
I believe the PR is ready for review now.

@latonz latonz self-requested a review May 5, 2026 16:44
Comment on lines +20 to +21
- `MaybeNull` attributes are now respected for fields, properties and constructor parameters in read context.
- `AllowNull` attributes are now respected for fields, properties and constructor parameters in write context.
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.

instead of in read/write context IMO when reading/writing values may be clearer?

.GetReturnTypeAttributes()
.Any(a => string.Equals(a.AttributeClass?.Name, nameof(MaybeNullAttribute)));

var effectiveType = hasNullableAttribute
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.

IMO SymbolAccessor.TryHasAttribute<MaybeNullAttribute>(methodSymbol.GetReturnTypeAttributes()) should be used.

Comment on lines +225 to +229
bool hasNullableAttribute = methodSymbol
.GetReturnTypeAttributes()
.Any(a => string.Equals(a.AttributeClass?.Name, nameof(MaybeNullAttribute)));

var effectiveType = hasNullableAttribute
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.

this logic is duplicated with the UserMethodMappingExtractor… extract into a new helper, e.g. SymbolAccessor.UpgradeReturnNullable(IMethodSymbol)

int Ordinal,
string Name,
ITypeSymbol Type,
IParameterSymbol? symbol = null,
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.

Why is this lower cased?

public ITypeSymbol Type => parameter.Type;
public INamedTypeSymbol? ContainingType => null;
public bool IsNullable => parameter.Type.IsNullable();
public bool IsReadNullable =>
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.

We can't only rely on the type as that would ignore these attributes.

I think storing the symbol within the method parameter should be okay. I'd add helpers though (IsWriteNullable and IsReadNullable).


private static string FormattedReturnType(IMethodSymbol methodSymbol)
{
bool hasNullableAttribute = methodSymbol
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.

We use var across this codebase unless the type really adds some value or is necessary for the compilation. IMO it doesn't add any value here…

public bool IsAnyNullable() => Path.Any(p => p.IsNullable);
public bool IsAnyReadNullable() => Path.Any(x => x.IsReadNullable);

public bool IsWriteNullable() => Path[^1].IsWriteNullable;
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.

Shouldn't this be moved to NonEmptyMemberPath?

@Abhijithtv Abhijithtv marked this pull request as draft May 6, 2026 05:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CS8601 while mapping properties annotated with MaybeNullAttribute

2 participants