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 637364b97f..160ef4a65a 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/QueryExpressions.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/QueryExpressions.cs @@ -44,6 +44,16 @@ public static Maybe Where(this Maybe a, Func predicate) 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 @@ private List Issue2545(List arglist) { 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 2b07fc7e87..d33e41884a 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs @@ -377,10 +377,12 @@ public override AstNode VisitIdentifier(Identifier identifier) } } - 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 160ef4a65a..0870735b1d 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/QueryExpressions.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/QueryExpressions.cs @@ -48,6 +48,10 @@ public class MaybeHolder { public Maybe Value; +#if CS60 + public Maybe this[int index] => default(Maybe); +#endif + public Func> Factory() { return () => default(Maybe); @@ -237,6 +241,11 @@ private List Issue2545(List arglist) { 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 d33e41884a..4745061e3b 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs @@ -380,7 +380,8 @@ public override AstNode VisitIdentifier(Identifier identifier) 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 };