Skip to content

Commit a23efdf

Browse files
authored
Merge pull request #212 from EFNext/copilot/fix-null-coalesce-issue
Preserve nullable typing for ignored null-conditional access in coalesce expressions
2 parents 68a4599 + 8d81f5d commit a23efdf

3 files changed

Lines changed: 81 additions & 1 deletion

File tree

src/EntityFrameworkCore.Projectables.Generator/SyntaxRewriters/ExpressionSyntaxRewriter.NullConditionalRewrite.cs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,29 @@ internal partial class ExpressionSyntaxRewriter
2626
else if (_nullConditionalRewriteSupport is NullConditionalRewriteSupport.Ignore)
2727
{
2828
// Ignore the conditional access and simply visit the WhenNotNull expression
29-
return Visit(node.WhenNotNull);
29+
var rewrittenExpression = (ExpressionSyntax?)Visit(node.WhenNotNull);
30+
if (rewrittenExpression is null)
31+
{
32+
return null;
33+
}
34+
35+
var typeInfo = _semanticModel.GetTypeInfo(node);
36+
var whenNotNullType = _semanticModel.GetTypeInfo(node.WhenNotNull).Type;
37+
if (IsCoalesceLeftOperand(node) &&
38+
typeInfo.ConvertedType is INamedTypeSymbol { IsValueType: true, OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } nullableType &&
39+
whenNotNullType is { IsValueType: true } rewrittenType &&
40+
(
41+
SymbolEqualityComparer.Default.Equals(nullableType.TypeArguments[0], rewrittenType) ||
42+
SymbolEqualityComparer.Default.Equals(nullableType, rewrittenType)
43+
))
44+
{
45+
return SyntaxFactory.CastExpression(
46+
SyntaxFactory.ParseTypeName(nullableType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)),
47+
SyntaxFactory.ParenthesizedExpression(rewrittenExpression)
48+
);
49+
}
50+
51+
return rewrittenExpression;
3052
}
3153

3254
else if (_nullConditionalRewriteSupport is NullConditionalRewriteSupport.Rewrite)
@@ -105,4 +127,19 @@ private static bool IsNullableValueType(ITypeSymbol? type)
105127
return type is { IsValueType: true } &&
106128
type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T;
107129
}
130+
131+
private static bool IsCoalesceLeftOperand(ConditionalAccessExpressionSyntax node)
132+
{
133+
ExpressionSyntax current = node;
134+
var parent = node.Parent;
135+
136+
while (parent is ParenthesizedExpressionSyntax parenthesizedExpression)
137+
{
138+
current = parenthesizedExpression;
139+
parent = parenthesizedExpression.Parent;
140+
}
141+
142+
return parent is BinaryExpressionSyntax { RawKind: (int)SyntaxKind.CoalesceExpression } coalesceExpression &&
143+
coalesceExpression.Left == current;
144+
}
108145
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// <auto-generated/>
2+
#nullable disable
3+
using EntityFrameworkCore.Projectables;
4+
5+
namespace EntityFrameworkCore.Projectables.Generated
6+
{
7+
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
8+
static class _Product_GetCost
9+
{
10+
static global::System.Linq.Expressions.Expression<global::System.Func<global::Product, decimal>> Expression()
11+
{
12+
return (global::Product @this) => (decimal? )(@this.Price.Amount) ?? 0m;
13+
}
14+
}
15+
}

tests/EntityFrameworkCore.Projectables.Generator.Tests/NullableTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,34 @@ class Foo {
488488
return Verifier.Verify(result.GeneratedTrees[0].ToString());
489489
}
490490

491+
[Fact]
492+
public Task NullConditionalNullCoalesceTypeConversion_WithIgnoreSupport()
493+
{
494+
var compilation = CreateCompilation(@"
495+
using EntityFrameworkCore.Projectables;
496+
497+
public class Price {
498+
public decimal Amount { get; set; }
499+
}
500+
501+
public class Product {
502+
public Price? Price { get; set; }
503+
504+
[Projectable(AllowBlockBody = true, NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)]
505+
public decimal GetCost() {
506+
return Price?.Amount ?? 0m;
507+
}
508+
}
509+
");
510+
511+
var result = RunGenerator(compilation);
512+
513+
Assert.Empty(result.Diagnostics);
514+
Assert.Single(result.GeneratedTrees);
515+
516+
return Verifier.Verify(result.GeneratedTrees[0].ToString());
517+
}
518+
491519
[Fact]
492520
public Task NullableValueType_MemberAccess_WithRewriteSupport_IsBeingRewritten()
493521
{

0 commit comments

Comments
 (0)