From 3a8e7dee230aecfd76806dcde2b96a18999580e4 Mon Sep 17 00:00:00 2001 From: Sebastien Lebreton Date: Sat, 8 Aug 2026 19:03:34 +0200 Subject: [PATCH 1/2] Keep null-conditional LINQ chains in method syntax Query syntax cannot preserve a null-conditional receiver that lifts a value type. Detect null conditionals through the LINQ receiver chain before introducing query syntax. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0dd407b6-9410-48df-add5-761ca4a8dec0 --- .../TestCases/Pretty/QueryExpressions.cs | 20 +++++++++++++++++++ .../Transforms/IntroduceQueryExpressions.cs | 10 ++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/QueryExpressions.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/QueryExpressions.cs index 637364b97..160ef4a65 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/QueryExpressions.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/QueryExpressions.cs @@ -44,6 +44,16 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty public class QueryExpressions { + public class MaybeHolder + { + public Maybe Value; + + public Func> Factory() + { + return () => default(Maybe); + } + } + public class HbmParam { public string Name { get; set; } @@ -217,6 +227,16 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { return arglist?.OrderByDescending((string f) => f.Length).ThenBy((string f) => f.ToLower()).ToList(); } + + public Maybe? NullConditionalValueTypeQuery(MaybeHolder holder) + { + return holder?.Value.Where((int value) => value > 0).Select((int value) => value.ToString()); + } + + public Maybe? NullConditionalNestedInvocationQuery(MaybeHolder holder) + { + return holder?.Factory()().Where((int value) => value > 0).Select((int value) => value.ToString()); + } #endif public static IEnumerable Issue1310a(bool test) diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs index 2b07fc7e8..d33e41884 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs @@ -377,10 +377,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms } } - bool IsNullConditional(Expression target) - { - return target is UnaryOperatorExpression uoe && uoe.Operator == UnaryOperatorType.NullConditional; - } + bool IsNullConditional(Expression target) => target switch { + UnaryOperatorExpression { Operator: UnaryOperatorType.NullConditional } => true, + MemberReferenceExpression member => IsNullConditional(member.Target), + InvocationExpression { Target: { } invocationTarget } => IsNullConditional(invocationTarget), + _ => false + }; /// /// This fixes #437: Decompilation of query expression loses material parentheses From 4a2afbb7236f8c061a953562650d222edaaa09db Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Mon, 10 Aug 2026 18:56:00 +0200 Subject: [PATCH 2/2] Walk indexer accesses when looking for a null-conditional source A query source can be reached through an indexer as well as through a member access or a call: `holder?[0].Where(...).Select(...)` puts an IndexerExpression between the LINQ call and the `?.`. The receiver walk stopped there, so query syntax was still introduced over a source the conditional access had lifted to a nullable value type, and the output failed to compile with CS1936 - the same way as the case that was reported, one node kind further along. IndexerExpression.Target is nullable where MemberReferenceExpression's and InvocationExpression's are not, so only that arm needs to match on the target. Assisted-by: Claude:claude-opus-5[1m]:Claude Code --- .../TestCases/Pretty/QueryExpressions.cs | 9 +++++++++ .../CSharp/Transforms/IntroduceQueryExpressions.cs | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/QueryExpressions.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/QueryExpressions.cs index 160ef4a65..0870735b1 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/QueryExpressions.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/QueryExpressions.cs @@ -48,6 +48,10 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { public Maybe Value; +#if CS60 + public Maybe this[int index] => default(Maybe); +#endif + public Func> Factory() { return () => default(Maybe); @@ -237,6 +241,11 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { return holder?.Factory()().Where((int value) => value > 0).Select((int value) => value.ToString()); } + + public Maybe? NullConditionalIndexerQuery(MaybeHolder holder) + { + return holder?[0].Where((int value) => value > 0).Select((int value) => value.ToString()); + } #endif public static IEnumerable Issue1310a(bool test) diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs index d33e41884..4745061e3 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs @@ -380,7 +380,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms bool IsNullConditional(Expression target) => target switch { UnaryOperatorExpression { Operator: UnaryOperatorType.NullConditional } => true, MemberReferenceExpression member => IsNullConditional(member.Target), - InvocationExpression { Target: { } invocationTarget } => IsNullConditional(invocationTarget), + InvocationExpression invocation => IsNullConditional(invocation.Target), + IndexerExpression { Target: { } indexerTarget } => IsNullConditional(indexerTarget), _ => false };