From ffe7e7b3f1e697fe0e6930fe61f2e961b98fa5ea Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 10 Oct 2026 13:03:54 +0200 Subject: [PATCH] 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. --- .../TestCases/Correctness/NullPropagation.cs | 22 +++++++++++++++++++ .../TestCases/Pretty/NullPropagation.cs | 11 ++++++++++ ICSharpCode.Decompiler/CSharp/CallBuilder.cs | 4 +++- .../Transforms/IntroduceQueryExpressions.cs | 2 +- 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/NullPropagation.cs b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/NullPropagation.cs index 90a83bd..14e31dd 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/NullPropagation.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/NullPropagation.cs @@ -7,6 +7,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness static void Main() { new NullPropagation().TestNotCoalescing(); + new NullPropagation().TestNullableBoolInChain(); #if CS72 new NullPropagation().TestRefStructNotCoalescing(); #endif @@ -15,6 +16,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness class MyClass { public string Text; + public bool? Flag; } void TestNotCoalescing() @@ -29,6 +31,26 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness 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) ?? ""); + } + } + + bool EqualsTrue(MyClass c) + { + return c?.Flag == true; + } + + string GetValueOrDefaultToString(MyClass c) + { + return c?.Flag.GetValueOrDefault().ToString(); + } + #if CS72 ref struct ByRefLikeStruct { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullPropagation.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullPropagation.cs index 063dab6..c2f9201 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullPropagation.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullPropagation.cs @@ -32,6 +32,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty public readonly MyStruct ReadonlyStructField; public string Text; public MyClass Field; + public bool? NullableBool; public MyClass Property { get; set; } public MyClass this[int index] => null; public MyClass Method(int arg) @@ -380,5 +381,15 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty return x?.ToString(); #endif } + + private static bool Issue3702(MyClass c) + { + return c?.NullableBool.GetValueOrDefault() ?? false; + } + + private static string Issue3702ToString(MyClass c) + { + return c?.NullableBool.GetValueOrDefault().ToString(); + } } } diff --git a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs index bad908d..0371d7a 100644 --- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs @@ -26,6 +26,7 @@ using System.Text; using ICSharpCode.Decompiler.CSharp.Resolver; using ICSharpCode.Decompiler.CSharp.Syntax; +using ICSharpCode.Decompiler.CSharp.Transforms; using ICSharpCode.Decompiler.IL; using ICSharpCode.Decompiler.IL.Transforms; using ICSharpCode.Decompiler.Semantics; @@ -594,7 +595,8 @@ namespace ICSharpCode.Decompiler.CSharp if (settings.LiftNullables && method.Name == "GetValueOrDefault" && method.DeclaringType.IsKnownType(KnownTypeCode.NullableOfT) && method.DeclaringType.TypeArguments[0].IsKnownType(KnownTypeCode.Boolean) - && argumentList.Length == 0) + && argumentList.Length == 0 + && !IntroduceQueryExpressions.IsNullConditional(target)) { argumentList.CheckNoNamedOrOptionalArguments(); return new BinaryOperatorExpression( diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs index c9c5064..57580b1 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs @@ -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, MemberReferenceExpression member => IsNullConditional(member.Target), InvocationExpression invocation => IsNullConditional(invocation.Target),