Fix #3702: Don't rewrite GetValueOrDefault() to == true in ?. chains

Turning bool?.GetValueOrDefault() into `== true` ends an enclosing
null-conditional chain, so `c?.Flag.GetValueOrDefault()` became
`(c?.Flag == true)` of type bool instead of bool?. A following `?? false`
then fails to compile (CS0019), and a following `.ToString()` yields
"False" instead of null when c is null. Keep the call when its target is
part of a ?. chain.
This commit is contained in:
dualfroz committed 2026-10-16 10:07:54 +02:00
1 parent 8e3e41fedb
commit ffe7e7b3f1
4 files changed
+37 -2

No files matched your search

@@ -7,6 +7,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness
static void Main() static void Main()
{ {
new NullPropagation().TestNotCoalescing(); new NullPropagation().TestNotCoalescing();
new NullPropagation().TestNullableBoolInChain();
#if CS72 #if CS72
new NullPropagation().TestRefStructNotCoalescing(); new NullPropagation().TestRefStructNotCoalescing();
#endif #endif
@@ -15,6 +16,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness
class MyClass class MyClass
{ {
public string Text; public string Text;
public bool? Flag;
} }
void TestNotCoalescing() void TestNotCoalescing()
@@ -29,6 +31,26 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness
return c != null ? c.Text : "Hello"; return c != null ? c.Text : "Hello";
} }
void TestNullableBoolInChain()
{
Console.WriteLine("TestNullableBoolInChain:");
foreach (MyClass c in new[] { null, new MyClass(), new MyClass { Flag = false }, new MyClass { Flag = true } })
{
Console.WriteLine(EqualsTrue(c));
Console.WriteLine(GetValueOrDefaultToString(c) ?? "<null>");
}
}
bool EqualsTrue(MyClass c)
{
return c?.Flag == true;
}
string GetValueOrDefaultToString(MyClass c)
{
return c?.Flag.GetValueOrDefault().ToString();
}
#if CS72 #if CS72
ref struct ByRefLikeStruct ref struct ByRefLikeStruct
{ {
@@ -32,6 +32,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
public readonly MyStruct ReadonlyStructField; public readonly MyStruct ReadonlyStructField;
public string Text; public string Text;
public MyClass Field; public MyClass Field;
public bool? NullableBool;
public MyClass Property { get; set; } public MyClass Property { get; set; }
public MyClass this[int index] => null; public MyClass this[int index] => null;
public MyClass Method(int arg) public MyClass Method(int arg)
@@ -380,5 +381,15 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
return x?.ToString(); return x?.ToString();
#endif #endif
} }
private static bool Issue3702(MyClass c)
{
return c?.NullableBool.GetValueOrDefault() ?? false;
}
private static string Issue3702ToString(MyClass c)
{
return c?.NullableBool.GetValueOrDefault().ToString();
}
} }
} }
+3 -1
View File
@@ -26,6 +26,7 @@ using System.Text;
using ICSharpCode.Decompiler.CSharp.Resolver; using ICSharpCode.Decompiler.CSharp.Resolver;
using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.CSharp.Syntax;
using ICSharpCode.Decompiler.CSharp.Transforms;
using ICSharpCode.Decompiler.IL; using ICSharpCode.Decompiler.IL;
using ICSharpCode.Decompiler.IL.Transforms; using ICSharpCode.Decompiler.IL.Transforms;
using ICSharpCode.Decompiler.Semantics; using ICSharpCode.Decompiler.Semantics;
@@ -594,7 +595,8 @@ namespace ICSharpCode.Decompiler.CSharp
if (settings.LiftNullables && method.Name == "GetValueOrDefault" if (settings.LiftNullables && method.Name == "GetValueOrDefault"
&& method.DeclaringType.IsKnownType(KnownTypeCode.NullableOfT) && method.DeclaringType.IsKnownType(KnownTypeCode.NullableOfT)
&& method.DeclaringType.TypeArguments[0].IsKnownType(KnownTypeCode.Boolean) && method.DeclaringType.TypeArguments[0].IsKnownType(KnownTypeCode.Boolean)
&& argumentList.Length == 0) && argumentList.Length == 0
&& !IntroduceQueryExpressions.IsNullConditional(target))
{ {
argumentList.CheckNoNamedOrOptionalArguments(); argumentList.CheckNoNamedOrOptionalArguments();
return new BinaryOperatorExpression( return new BinaryOperatorExpression(
@@ -380,7 +380,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
} }
} }
bool IsNullConditional(Expression target) => target switch { internal static bool IsNullConditional(Expression target) => target switch {
UnaryOperatorExpression { Operator: UnaryOperatorType.NullConditional } => true, UnaryOperatorExpression { Operator: UnaryOperatorType.NullConditional } => true,
MemberReferenceExpression member => IsNullConditional(member.Target), MemberReferenceExpression member => IsNullConditional(member.Target),
InvocationExpression invocation => IsNullConditional(invocation.Target), InvocationExpression invocation => IsNullConditional(invocation.Target),