From 90b889f56297c44a82ac9bfb03d92615c0042a31 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 26 May 2026 05:16:56 +0000
Subject: [PATCH 1/3] Fix crash in LinqEnumerableCodeFixProvider with
conditional access expressions
When the code fix wraps an expression containing a MemberBindingExpression
(from conditional access ?.) in parentheses with Simplifier.Annotation,
Roslyn's speculative semantic model crashes with NullReferenceException
in FindConditionalAccessNodeForBinding.
The fix skips parenthesization when the expression contains a
MemberBindingExpression, which avoids triggering the Roslyn bug.
Fixes #216
Agent-Logs-Url: https://github.com/WiseTechGlobal/WTG.Analyzers/sessions/d974a4ba-43c4-4b13-acca-e9ccb83f1f8f
Co-authored-by: brian-reichle <18721383+brian-reichle@users.noreply.github.com>
---
.../ConditionalAccess/Diagnostics.xml | 6 +++
.../ConditionalAccess/Result.cs | 13 +++++
.../ConditionalAccess/Source.cs | 13 +++++
.../LinqEnumerableCodeFixProvider.cs | 50 ++++++++++++++++---
4 files changed, 76 insertions(+), 6 deletions(-)
create mode 100644 src/WTG.Analyzers.Test/TestData/LinqEnumerableAnalyzer/ConditionalAccess/Diagnostics.xml
create mode 100644 src/WTG.Analyzers.Test/TestData/LinqEnumerableAnalyzer/ConditionalAccess/Result.cs
create mode 100644 src/WTG.Analyzers.Test/TestData/LinqEnumerableAnalyzer/ConditionalAccess/Source.cs
diff --git a/src/WTG.Analyzers.Test/TestData/LinqEnumerableAnalyzer/ConditionalAccess/Diagnostics.xml b/src/WTG.Analyzers.Test/TestData/LinqEnumerableAnalyzer/ConditionalAccess/Diagnostics.xml
new file mode 100644
index 0000000..328f7a0
--- /dev/null
+++ b/src/WTG.Analyzers.Test/TestData/LinqEnumerableAnalyzer/ConditionalAccess/Diagnostics.xml
@@ -0,0 +1,6 @@
+
+
+
+ Test0.cs: (8, 32-57)
+
+
diff --git a/src/WTG.Analyzers.Test/TestData/LinqEnumerableAnalyzer/ConditionalAccess/Result.cs b/src/WTG.Analyzers.Test/TestData/LinqEnumerableAnalyzer/ConditionalAccess/Result.cs
new file mode 100644
index 0000000..cdefe23
--- /dev/null
+++ b/src/WTG.Analyzers.Test/TestData/LinqEnumerableAnalyzer/ConditionalAccess/Result.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using System.Linq;
+
+public class Bob
+{
+ public int[] Method(Bob other)
+ {
+ var items = other.GetItems()?.Where(x => x > 0).Append(42).ToArray();
+ return items;
+ }
+
+ public IEnumerable GetItems() => new[] { 1, 2, 3 };
+}
diff --git a/src/WTG.Analyzers.Test/TestData/LinqEnumerableAnalyzer/ConditionalAccess/Source.cs b/src/WTG.Analyzers.Test/TestData/LinqEnumerableAnalyzer/ConditionalAccess/Source.cs
new file mode 100644
index 0000000..52f6dde
--- /dev/null
+++ b/src/WTG.Analyzers.Test/TestData/LinqEnumerableAnalyzer/ConditionalAccess/Source.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using System.Linq;
+
+public class Bob
+{
+ public int[] Method(Bob other)
+ {
+ var items = other.GetItems()?.Where(x => x > 0).Concat(new[] { 42 }).ToArray();
+ return items;
+ }
+
+ public IEnumerable GetItems() => new[] { 1, 2, 3 };
+}
diff --git a/src/WTG.Analyzers/Analyzers/LinqEnumerable/LinqEnumerableCodeFixProvider.cs b/src/WTG.Analyzers/Analyzers/LinqEnumerable/LinqEnumerableCodeFixProvider.cs
index 18ea77f..95a79a9 100644
--- a/src/WTG.Analyzers/Analyzers/LinqEnumerable/LinqEnumerableCodeFixProvider.cs
+++ b/src/WTG.Analyzers/Analyzers/LinqEnumerable/LinqEnumerableCodeFixProvider.cs
@@ -121,9 +121,7 @@ public static SyntaxNode FixConcatWithAppendMethod(MemberAccessExpressionSyntax
return InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
- ParenthesizedExpression(m.Expression.WithoutTrivia())
- .WithTriviaFrom(m.Expression)
- .WithAdditionalAnnotations(Simplifier.Annotation),
+ WrapExpressionIfNeeded(m.Expression),
m.OperatorToken,
IdentifierName(nameof(Enumerable.Append))
.WithTriviaFrom(m.Name)))
@@ -146,9 +144,20 @@ public static SyntaxNode FixConcatWithAppendMethod(MemberAccessExpressionSyntax
{
case 1:
listOfArgumentsAndSeparators.Add(Argument(LinqEnumerableUtils.GetFirstValue(m.Expression.TryGetExpressionFromParenthesizedExpression())!));
- member = ParenthesizedExpression(invocation.ArgumentList.Arguments[0].Expression.WithoutTrivia())
- .WithTriviaFrom(m.Expression)
- .WithAdditionalAnnotations(Simplifier.Annotation);
+ {
+ var argExpression = invocation.ArgumentList.Arguments[0].Expression;
+ if (ContainsMemberBinding(argExpression))
+ {
+ member = argExpression.WithTriviaFrom(m.Expression);
+ }
+ else
+ {
+ member = ParenthesizedExpression(argExpression.WithoutTrivia())
+ .WithTriviaFrom(m.Expression)
+ .WithAdditionalAnnotations(Simplifier.Annotation);
+ }
+ }
+
break;
case 2:
listOfArgumentsAndSeparators.Add(invocation.ArgumentList.Arguments[1]);
@@ -213,5 +222,34 @@ public static SyntaxNode FixConcatWithNewCollection(MemberAccessExpressionSyntax
.WithTriviaFrom(invocation)
.WithAdditionalAnnotations(Simplifier.Annotation);
}
+
+ static ExpressionSyntax WrapExpressionIfNeeded(ExpressionSyntax expression)
+ {
+ // If the expression contains a MemberBindingExpression (from conditional access ?.),
+ // we must not wrap it in parentheses with a Simplifier.Annotation because doing so
+ // disconnects the binding from its ConditionalAccessExpression, causing Roslyn's
+ // speculative semantic model to crash with a NullReferenceException.
+ if (ContainsMemberBinding(expression))
+ {
+ return expression;
+ }
+
+ return ParenthesizedExpression(expression.WithoutTrivia())
+ .WithTriviaFrom(expression)
+ .WithAdditionalAnnotations(Simplifier.Annotation);
+ }
+
+ static bool ContainsMemberBinding(SyntaxNode node)
+ {
+ foreach (var descendant in node.DescendantNodesAndSelf())
+ {
+ if (descendant.IsKind(SyntaxKind.MemberBindingExpression))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
}
From b16213b99ef699b9ecbbe35f3e9112129e875a32 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 26 May 2026 05:52:06 +0000
Subject: [PATCH 2/3] Simplify conditional access check to use
IsKind(InvocationExpression)
Replace ContainsMemberBinding descendant traversal with a simple
IsKind(SyntaxKind.InvocationExpression) check, since invocation
expressions never need parenthesization regardless of their receiver.
Agent-Logs-Url: https://github.com/WiseTechGlobal/WTG.Analyzers/sessions/fc763f58-f56a-4811-a94b-2fe440b3eb00
Co-authored-by: brian-reichle <18721383+brian-reichle@users.noreply.github.com>
---
.../LinqEnumerableCodeFixProvider.cs | 26 +++++--------------
1 file changed, 7 insertions(+), 19 deletions(-)
diff --git a/src/WTG.Analyzers/Analyzers/LinqEnumerable/LinqEnumerableCodeFixProvider.cs b/src/WTG.Analyzers/Analyzers/LinqEnumerable/LinqEnumerableCodeFixProvider.cs
index 95a79a9..605f45e 100644
--- a/src/WTG.Analyzers/Analyzers/LinqEnumerable/LinqEnumerableCodeFixProvider.cs
+++ b/src/WTG.Analyzers/Analyzers/LinqEnumerable/LinqEnumerableCodeFixProvider.cs
@@ -146,7 +146,7 @@ public static SyntaxNode FixConcatWithAppendMethod(MemberAccessExpressionSyntax
listOfArgumentsAndSeparators.Add(Argument(LinqEnumerableUtils.GetFirstValue(m.Expression.TryGetExpressionFromParenthesizedExpression())!));
{
var argExpression = invocation.ArgumentList.Arguments[0].Expression;
- if (ContainsMemberBinding(argExpression))
+ if (argExpression.IsKind(SyntaxKind.InvocationExpression))
{
member = argExpression.WithTriviaFrom(m.Expression);
}
@@ -225,11 +225,12 @@ public static SyntaxNode FixConcatWithNewCollection(MemberAccessExpressionSyntax
static ExpressionSyntax WrapExpressionIfNeeded(ExpressionSyntax expression)
{
- // If the expression contains a MemberBindingExpression (from conditional access ?.),
- // we must not wrap it in parentheses with a Simplifier.Annotation because doing so
- // disconnects the binding from its ConditionalAccessExpression, causing Roslyn's
- // speculative semantic model to crash with a NullReferenceException.
- if (ContainsMemberBinding(expression))
+ // Invocation expressions never need parenthesization as they already have
+ // clear binding. Wrapping them can cause issues when the expression is part
+ // of a conditional access chain (?.), as it disconnects MemberBindingExpressions
+ // from their ConditionalAccessExpression, causing Roslyn's speculative semantic
+ // model to crash with a NullReferenceException.
+ if (expression.IsKind(SyntaxKind.InvocationExpression))
{
return expression;
}
@@ -238,18 +239,5 @@ static ExpressionSyntax WrapExpressionIfNeeded(ExpressionSyntax expression)
.WithTriviaFrom(expression)
.WithAdditionalAnnotations(Simplifier.Annotation);
}
-
- static bool ContainsMemberBinding(SyntaxNode node)
- {
- foreach (var descendant in node.DescendantNodesAndSelf())
- {
- if (descendant.IsKind(SyntaxKind.MemberBindingExpression))
- {
- return true;
- }
- }
-
- return false;
- }
}
}
From 4259e834634fd6a88bdea443d75db29a51a02c01 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 26 May 2026 06:02:01 +0000
Subject: [PATCH 3/3] Refactor FixConcatWithPrependMethod to use
WrapExpressionIfNeeded helper
Agent-Logs-Url: https://github.com/WiseTechGlobal/WTG.Analyzers/sessions/efaaa8ee-40eb-44f9-a086-cd09f2840505
Co-authored-by: brian-reichle <18721383+brian-reichle@users.noreply.github.com>
---
.../LinqEnumerableCodeFixProvider.cs | 15 +--------------
1 file changed, 1 insertion(+), 14 deletions(-)
diff --git a/src/WTG.Analyzers/Analyzers/LinqEnumerable/LinqEnumerableCodeFixProvider.cs b/src/WTG.Analyzers/Analyzers/LinqEnumerable/LinqEnumerableCodeFixProvider.cs
index 605f45e..ab12fe0 100644
--- a/src/WTG.Analyzers/Analyzers/LinqEnumerable/LinqEnumerableCodeFixProvider.cs
+++ b/src/WTG.Analyzers/Analyzers/LinqEnumerable/LinqEnumerableCodeFixProvider.cs
@@ -144,20 +144,7 @@ public static SyntaxNode FixConcatWithAppendMethod(MemberAccessExpressionSyntax
{
case 1:
listOfArgumentsAndSeparators.Add(Argument(LinqEnumerableUtils.GetFirstValue(m.Expression.TryGetExpressionFromParenthesizedExpression())!));
- {
- var argExpression = invocation.ArgumentList.Arguments[0].Expression;
- if (argExpression.IsKind(SyntaxKind.InvocationExpression))
- {
- member = argExpression.WithTriviaFrom(m.Expression);
- }
- else
- {
- member = ParenthesizedExpression(argExpression.WithoutTrivia())
- .WithTriviaFrom(m.Expression)
- .WithAdditionalAnnotations(Simplifier.Annotation);
- }
- }
-
+ member = WrapExpressionIfNeeded(invocation.ArgumentList.Arguments[0].Expression.WithTriviaFrom(m.Expression));
break;
case 2:
listOfArgumentsAndSeparators.Add(invocation.ArgumentList.Arguments[1]);