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]);