diff --git a/analyzers/src/SonarAnalyzer.CSharp/Extensions/ExpressionSyntaxExtensions.Roslyn.cs b/analyzers/src/SonarAnalyzer.CSharp/Extensions/ExpressionSyntaxExtensions.Roslyn.cs
new file mode 100644
index 00000000000..8541757cd3c
--- /dev/null
+++ b/analyzers/src/SonarAnalyzer.CSharp/Extensions/ExpressionSyntaxExtensions.Roslyn.cs
@@ -0,0 +1,278 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.CodeAnalysis.CSharp.Extensions;
+
+// Copied from Roslyn
+// https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs
+[ExcludeFromCodeCoverage]
+public static class ExpressionSyntaxExtensions
+{
+ ///
+ /// Returns if represents a node where a value is written to, like on the left side of an assignment expression. This method
+ /// also returns for potentially writeable expressions, like parameters.
+ /// See also .
+ ///
+ ///
+ /// Copied from
+ ///
+ public static bool IsWrittenTo(
+ this ExpressionSyntax expression,
+ SemanticModel semanticModel,
+ CancellationToken cancellationToken)
+ {
+ if (expression == null)
+ return false;
+
+ expression = GetExpressionToAnalyzeForWrites(expression);
+
+ if (expression.IsOnlyWrittenTo())
+ return true;
+
+ if (expression.IsInRefContext(out var refParent))
+ {
+ // most cases of `ref x` will count as a potential write of `x`. An important exception is:
+ // `ref readonly y = ref x`. In that case, because 'y' can't be written to, this would not
+ // be a write of 'x'.
+ if (refParent.Parent is EqualsValueClauseSyntax { Parent: VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Type: { } variableDeclarationType } } })
+ {
+ if (ScopedTypeSyntaxWrapper.IsInstance(variableDeclarationType) && (ScopedTypeSyntaxWrapper)variableDeclarationType is { } scopedType)
+ {
+ variableDeclarationType = scopedType.Type;
+ }
+
+ if (RefTypeSyntaxWrapper.IsInstance(variableDeclarationType) && ((RefTypeSyntaxWrapper)variableDeclarationType).ReadOnlyKeyword != default)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ // Similar to `ref x`, `&x` allows reads and write of the value, meaning `x` may be (but is not definitely)
+ // written to.
+ if (expression.Parent.IsKind(SyntaxKind.AddressOfExpression))
+ return true;
+
+ // We're written if we're used in a ++, or -- expression.
+ if (expression.IsOperandOfIncrementOrDecrementExpression())
+ return true;
+
+ if (expression.IsLeftSideOfAnyAssignExpression())
+ return true;
+
+ // An extension method invocation with a ref-this parameter can write to an expression.
+ if (expression.Parent is MemberAccessExpressionSyntax memberAccess &&
+ expression == memberAccess.Expression)
+ {
+ var symbol = semanticModel.GetSymbolInfo(memberAccess, cancellationToken).Symbol;
+ if (symbol is IMethodSymbol
+ {
+ MethodKind: MethodKind.ReducedExtension,
+ ReducedFrom.Parameters: { Length: > 0 } parameters,
+ } && parameters[0].RefKind == RefKind.Ref)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // Copy of
+ // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L221
+ private static ExpressionSyntax GetExpressionToAnalyzeForWrites(ExpressionSyntax? expression)
+ {
+ if (expression.IsRightSideOfDotOrArrow())
+ {
+ expression = (ExpressionSyntax)expression.Parent;
+ }
+
+ expression = (ExpressionSyntax)expression.WalkUpParentheses();
+
+ return expression;
+ }
+
+ // Copy of
+ // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L63
+ public static bool IsRightSideOfDotOrArrow(this ExpressionSyntax name)
+ => IsAnyMemberAccessExpressionName(name) || IsRightSideOfQualifiedName(name);
+
+ // Copy of
+ // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L41
+ public static bool IsAnyMemberAccessExpressionName(this ExpressionSyntax expression)
+ {
+ if (expression == null)
+ return false;
+
+ return expression == (expression.Parent as MemberAccessExpressionSyntax)?.Name ||
+ expression.IsMemberBindingExpressionName();
+ }
+
+ // Copy of
+ // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L50
+ public static bool IsMemberBindingExpressionName(this ExpressionSyntax expression)
+ => expression?.Parent is MemberBindingExpressionSyntax memberBinding &&
+ memberBinding.Name == expression;
+
+ // Copy of
+ // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L54
+ public static bool IsRightSideOfQualifiedName(this ExpressionSyntax expression)
+ => expression?.Parent is QualifiedNameSyntax qualifiedName && qualifiedName.Right == expression;
+
+ // Copy of
+ // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L233
+ public static bool IsOnlyWrittenTo(this ExpressionSyntax expression)
+ {
+ expression = GetExpressionToAnalyzeForWrites(expression);
+
+ if (expression != null)
+ {
+ if (expression.IsInOutContext())
+ {
+ return true;
+ }
+
+ if (expression.Parent != null)
+ {
+ if (expression.IsLeftSideOfAssignExpression())
+ {
+ return true;
+ }
+
+ if (expression.IsAttributeNamedArgumentIdentifier())
+ {
+ return true;
+ }
+ }
+
+ if (IsExpressionOfArgumentInDeconstruction(expression))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// If this declaration or identifier is part of a deconstruction, find the deconstruction.
+ /// If found, returns either an assignment expression or a foreach variable statement.
+ /// Returns null otherwise.
+ ///
+ /// copied from SyntaxExtensions.GetContainingDeconstruction.
+ ///
+ ///
+ /// Copied from
+ ///
+ private static bool IsExpressionOfArgumentInDeconstruction(ExpressionSyntax expr)
+ {
+ if (!expr.IsParentKind(SyntaxKind.Argument))
+ {
+ return false;
+ }
+
+ while (true)
+ {
+ var parent = expr.Parent;
+ if (parent == null)
+ {
+ return false;
+ }
+
+ switch (parent.Kind())
+ {
+ case SyntaxKind.Argument:
+ if (parent.Parent?.Kind() == SyntaxKindEx.TupleExpression)
+ {
+ expr = (ExpressionSyntax)parent.Parent;
+ continue;
+ }
+
+ return false;
+ case SyntaxKind.SimpleAssignmentExpression:
+ if (((AssignmentExpressionSyntax)parent).Left == expr)
+ {
+ return true;
+ }
+
+ return false;
+ case SyntaxKindEx.ForEachVariableStatement:
+ if (((ForEachVariableStatementSyntaxWrapper)parent).Variable == expr)
+ {
+ return true;
+ }
+
+ return false;
+
+ default:
+ return false;
+ }
+ }
+ }
+
+ // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L190
+ public static bool IsInOutContext(this ExpressionSyntax expression)
+ => expression?.Parent is ArgumentSyntax { RefOrOutKeyword: SyntaxToken { RawKind: (int)SyntaxKind.OutKeyword } } argument &&
+ argument.Expression == expression;
+
+ // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L383
+ public static bool IsAttributeNamedArgumentIdentifier(this ExpressionSyntax expression)
+ {
+ var nameEquals = expression?.Parent as NameEqualsSyntax;
+ return nameEquals.IsParentKind(SyntaxKind.AttributeArgument);
+ }
+
+ // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L194
+ public static bool IsInRefContext(this ExpressionSyntax expression)
+ => IsInRefContext(expression, out _);
+
+ ///
+ /// Returns true if this expression is in some ref keyword context. If then
+ /// will be the node containing the keyword.
+ ///
+ ///
+ /// Copied from
+ ///
+ public static bool IsInRefContext(this ExpressionSyntax expression, out SyntaxNode refParent)
+ {
+ while (expression?.Parent is ParenthesizedExpressionSyntax or PostfixUnaryExpressionSyntax { RawKind: (int)SyntaxKindEx.SuppressNullableWarningExpression })
+ expression = (ExpressionSyntax)expression.Parent;
+
+ if (expression?.Parent switch
+ {
+ ArgumentSyntax { RefOrOutKeyword.RawKind: (int)SyntaxKind.RefKeyword } => true,
+ var x when RefExpressionSyntaxWrapper.IsInstance(x) => true,
+ _ => false,
+ })
+ {
+ refParent = expression.Parent;
+ return true;
+ }
+
+ refParent = null;
+ return false;
+ }
+
+ // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L389
+ public static bool IsOperandOfIncrementOrDecrementExpression(this ExpressionSyntax expression)
+ {
+ if (expression?.Parent is SyntaxNode parent)
+ {
+ switch (parent.Kind())
+ {
+ case SyntaxKind.PostIncrementExpression:
+ case SyntaxKind.PreIncrementExpression:
+ case SyntaxKind.PostDecrementExpression:
+ case SyntaxKind.PreDecrementExpression:
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.Roslyn.cs b/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.Roslyn.cs
new file mode 100644
index 00000000000..195f7b7a54c
--- /dev/null
+++ b/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.Roslyn.cs
@@ -0,0 +1,153 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.CodeAnalysis.CSharp.Extensions;
+
+[ExcludeFromCodeCoverage]
+internal static class SyntaxNodeExtensions
+{
+ ///
+ /// Returns the left hand side of a conditional access expression. Returns c in case like a?.b?[0].c?.d.e?.f if d is passed.
+ ///
+ /// Copied from
+ /// Roslyn SyntaxNodeExtensions
+ public static ConditionalAccessExpressionSyntax GetParentConditionalAccessExpression(this SyntaxNode node)
+ {
+ // Walk upwards based on the grammar/parser rules around ?. expressions (can be seen in
+ // LanguageParser.ParseConsequenceSyntax).
+
+ // These are the parts of the expression that the ?... expression can end with. Specifically:
+ //
+ // 1. x?.y.M() // invocation
+ // 2. x?.y[...]; // element access
+ // 3. x?.y.z // member access
+ // 4. x?.y // member binding
+ // 5. x?[y] // element binding
+ var current = node;
+
+ if ((current.IsParentKind(SyntaxKind.SimpleMemberAccessExpression, out MemberAccessExpressionSyntax memberAccess) && memberAccess.Name == current) ||
+ (current.IsParentKind(SyntaxKind.MemberBindingExpression, out MemberBindingExpressionSyntax memberBinding) && memberBinding.Name == current))
+ {
+ current = current.Parent;
+ }
+
+ // Effectively, if we're on the RHS of the ? we have to walk up the RHS spine first until we hit the first
+ // conditional access.
+ while ((current.Kind() is SyntaxKind.InvocationExpression
+ or SyntaxKind.ElementAccessExpression
+ or SyntaxKind.SimpleMemberAccessExpression
+ or SyntaxKind.MemberBindingExpression
+ or SyntaxKind.ElementBindingExpression
+ // Optional exclamations might follow the conditional operation. For example: a.b?.$$c!!!!()
+ or SyntaxKindEx.SuppressNullableWarningExpression) &&
+ current.Parent is not ConditionalAccessExpressionSyntax)
+ {
+ current = current.Parent;
+ }
+
+ // Two cases we have to care about:
+ //
+ // 1. a?.b.$$c.d and
+ // 2. a?.b.$$c.d?.e...
+ //
+ // Note that `a?.b.$$c.d?.e.f?.g.h.i` falls into the same bucket as two. i.e. the parts after `.e` are
+ // lower in the tree and are not seen as we walk upwards.
+ //
+ //
+ // To get the root ?. (the one after the `a`) we have to potentially consume the first ?. on the RHS of the
+ // right spine (i.e. the one after `d`). Once we do this, we then see if that itself is on the RHS of a
+ // another conditional, and if so we hten return the one on the left. i.e. for '2' this goes in this direction:
+ //
+ // a?.b.$$c.d?.e // it will do:
+ // ----->
+ // <---------
+ //
+ // Note that this only one CAE consumption on both sides. GetRootConditionalAccessExpression can be used to
+ // get the root parent in a case like:
+ //
+ // x?.y?.z?.a?.b.$$c.d?.e.f?.g.h.i // it will do:
+ // ----->
+ // <---------
+ // <---
+ // <---
+ // <---
+ if (current.IsParentKind(SyntaxKind.ConditionalAccessExpression, out ConditionalAccessExpressionSyntax conditional) &&
+ conditional.Expression == current)
+ {
+ current = conditional;
+ }
+
+ if (current.IsParentKind(SyntaxKind.ConditionalAccessExpression, out conditional) &&
+ conditional.WhenNotNull == current)
+ {
+ current = conditional;
+ }
+
+ return current as ConditionalAccessExpressionSyntax;
+ }
+
+ ///
+ /// Call on the `.y` part of a `x?.y` to get the entire `x?.y` conditional access expression. This also works
+ /// when there are multiple chained conditional accesses. For example, calling this on '.y' or '.z' in
+ /// `x?.y?.z` will both return the full `x?.y?.z` node. This can be used to effectively get 'out' of the RHS of
+ /// a conditional access, and commonly represents the full standalone expression that can be operated on
+ /// atomically.
+ ///
+ /// Copied from Roslyn SyntaxNodeExtensions.
+ public static ConditionalAccessExpressionSyntax GetRootConditionalAccessExpression(this SyntaxNode node)
+ {
+ // Once we've walked up the entire RHS, now we continually walk up the conditional accesses until we're at
+ // the root. For example, if we have `a?.b` and we're on the `.b`, this will give `a?.b`. Similarly with
+ // `a?.b?.c` if we're on either `.b` or `.c` this will result in `a?.b?.c` (i.e. the root of this CAE
+ // sequence).
+ var current = node.GetParentConditionalAccessExpression();
+ while (current.IsParentKind(SyntaxKind.ConditionalAccessExpression, out ConditionalAccessExpressionSyntax conditional) &&
+ conditional.WhenNotNull == current)
+ {
+ current = conditional;
+ }
+
+ return current;
+ }
+
+ // Copy of
+ // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs#L347
+ public static bool IsLeftSideOfAssignExpression(this SyntaxNode node)
+ => node?.Parent is AssignmentExpressionSyntax { RawKind: (int)SyntaxKind.SimpleAssignmentExpression } assignment &&
+ assignment.Left == node;
+
+ // Copy of
+ // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs#L43C1-L45C1
+ public static bool IsParentKind(this SyntaxNode node, SyntaxKind kind)
+ => Microsoft.CodeAnalysis.CSharpExtensions.IsKind(node?.Parent, kind);
+
+ // Copy of
+ // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs#L46
+ public static bool IsParentKind(this SyntaxNode node, SyntaxKind kind, out T result) where T : SyntaxNode
+ {
+ if (node?.Parent?.IsKind(kind) is true && node.Parent is T t)
+ {
+ result = t;
+ return true;
+ }
+ result = null;
+ return false;
+ }
+
+ // Copy of
+ // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs#L351
+ public static bool IsLeftSideOfAnyAssignExpression(this SyntaxNode node)
+ {
+ return node?.Parent != null &&
+ node.Parent.IsAnyAssignExpression() &&
+ ((AssignmentExpressionSyntax)node.Parent).Left == node;
+ }
+
+ // Copy of
+ // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs#L323
+ public static bool IsAnyAssignExpression(this SyntaxNode node)
+ => SyntaxFacts.IsAssignmentExpression(node.Kind());
+}
diff --git a/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs b/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs
index 4dfe05a601d..0d0c1a2c4ec 100644
--- a/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs
+++ b/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs
@@ -363,17 +363,6 @@ static bool TakesExpressionTree(SymbolInfo info)
}
}
- public static bool IsParentKind(this SyntaxNode node, SyntaxKind kind, out T result) where T : SyntaxNode
- {
- if (node?.Parent?.IsKind(kind) is true && node.Parent is T t)
- {
- result = t;
- return true;
- }
- result = null;
- return false;
- }
-
// based on Type="ArgumentListSyntax" in https://github.com/dotnet/roslyn/blob/main/src/Compilers/CSharp/Portable/Syntax/Syntax.xml
public static ArgumentListSyntax ArgumentList(this SyntaxNode node) =>
node switch
@@ -399,110 +388,6 @@ node switch
_ => default,
};
- ///
- /// Returns the left hand side of a conditional access expression. Returns c in case like a?.b?[0].c?.d.e?.f if d is passed.
- ///
- /// Copied from
- /// Roslyn SyntaxNodeExtensions
- public static ConditionalAccessExpressionSyntax GetParentConditionalAccessExpression(this SyntaxNode node)
- {
- // Walk upwards based on the grammar/parser rules around ?. expressions (can be seen in
- // LanguageParser.ParseConsequenceSyntax).
-
- // These are the parts of the expression that the ?... expression can end with. Specifically:
- //
- // 1. x?.y.M() // invocation
- // 2. x?.y[...]; // element access
- // 3. x?.y.z // member access
- // 4. x?.y // member binding
- // 5. x?[y] // element binding
- var current = node;
-
- if ((current.IsParentKind(SyntaxKind.SimpleMemberAccessExpression, out MemberAccessExpressionSyntax memberAccess) && memberAccess.Name == current) ||
- (current.IsParentKind(SyntaxKind.MemberBindingExpression, out MemberBindingExpressionSyntax memberBinding) && memberBinding.Name == current))
- {
- current = current.Parent;
- }
-
- // Effectively, if we're on the RHS of the ? we have to walk up the RHS spine first until we hit the first
- // conditional access.
- while ((current.Kind() is SyntaxKind.InvocationExpression
- or SyntaxKind.ElementAccessExpression
- or SyntaxKind.SimpleMemberAccessExpression
- or SyntaxKind.MemberBindingExpression
- or SyntaxKind.ElementBindingExpression
- // Optional exclamations might follow the conditional operation. For example: a.b?.$$c!!!!()
- or SyntaxKindEx.SuppressNullableWarningExpression) &&
- current.Parent is not ConditionalAccessExpressionSyntax)
- {
- current = current.Parent;
- }
-
- // Two cases we have to care about:
- //
- // 1. a?.b.$$c.d and
- // 2. a?.b.$$c.d?.e...
- //
- // Note that `a?.b.$$c.d?.e.f?.g.h.i` falls into the same bucket as two. i.e. the parts after `.e` are
- // lower in the tree and are not seen as we walk upwards.
- //
- //
- // To get the root ?. (the one after the `a`) we have to potentially consume the first ?. on the RHS of the
- // right spine (i.e. the one after `d`). Once we do this, we then see if that itself is on the RHS of a
- // another conditional, and if so we hten return the one on the left. i.e. for '2' this goes in this direction:
- //
- // a?.b.$$c.d?.e // it will do:
- // ----->
- // <---------
- //
- // Note that this only one CAE consumption on both sides. GetRootConditionalAccessExpression can be used to
- // get the root parent in a case like:
- //
- // x?.y?.z?.a?.b.$$c.d?.e.f?.g.h.i // it will do:
- // ----->
- // <---------
- // <---
- // <---
- // <---
- if (current.IsParentKind(SyntaxKind.ConditionalAccessExpression, out ConditionalAccessExpressionSyntax conditional) &&
- conditional.Expression == current)
- {
- current = conditional;
- }
-
- if (current.IsParentKind(SyntaxKind.ConditionalAccessExpression, out conditional) &&
- conditional.WhenNotNull == current)
- {
- current = conditional;
- }
-
- return current as ConditionalAccessExpressionSyntax;
- }
-
- ///
- /// Call on the `.y` part of a `x?.y` to get the entire `x?.y` conditional access expression. This also works
- /// when there are multiple chained conditional accesses. For example, calling this on '.y' or '.z' in
- /// `x?.y?.z` will both return the full `x?.y?.z` node. This can be used to effectively get 'out' of the RHS of
- /// a conditional access, and commonly represents the full standalone expression that can be operated on
- /// atomically.
- ///
- /// Copied from Roslyn SyntaxNodeExtensions.
- public static ConditionalAccessExpressionSyntax GetRootConditionalAccessExpression(this SyntaxNode node)
- {
- // Once we've walked up the entire RHS, now we continually walk up the conditional accesses until we're at
- // the root. For example, if we have `a?.b` and we're on the `.b`, this will give `a?.b`. Similarly with
- // `a?.b?.c` if we're on either `.b` or `.c` this will result in `a?.b?.c` (i.e. the root of this CAE
- // sequence).
- var current = node.GetParentConditionalAccessExpression();
- while (current.IsParentKind(SyntaxKind.ConditionalAccessExpression, out ConditionalAccessExpressionSyntax conditional) &&
- conditional.WhenNotNull == current)
- {
- current = conditional;
- }
-
- return current;
- }
-
private static string GetUnknownType(SyntaxKind kind) =>
#if DEBUG
diff --git a/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs
index 25023b750ba..64f45bbfe8e 100644
--- a/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs
+++ b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs
@@ -108,6 +108,10 @@ internal sealed class CSharpSyntaxFacade : SyntaxFacade
public override bool IsStatic(SyntaxNode node) =>
Cast(node).IsStatic();
+ ///
+ public override bool IsWrittenTo(SyntaxNode expression, SemanticModel semanticModel, CancellationToken cancellationToken) =>
+ Cast(expression).IsWrittenTo(semanticModel, cancellationToken);
+
public override SyntaxKind Kind(SyntaxNode node) => node.Kind();
public override string LiteralText(SyntaxNode literal) =>
diff --git a/analyzers/src/SonarAnalyzer.CSharp/SonarAnalyzer.CSharp.csproj b/analyzers/src/SonarAnalyzer.CSharp/SonarAnalyzer.CSharp.csproj
index bbe8145a243..cdae8be47bc 100644
--- a/analyzers/src/SonarAnalyzer.CSharp/SonarAnalyzer.CSharp.csproj
+++ b/analyzers/src/SonarAnalyzer.CSharp/SonarAnalyzer.CSharp.csproj
@@ -34,6 +34,7 @@
+
diff --git a/analyzers/src/SonarAnalyzer.Common/Facade/SyntaxFacade.cs b/analyzers/src/SonarAnalyzer.Common/Facade/SyntaxFacade.cs
index 48efed1287d..b95fbbae1d0 100644
--- a/analyzers/src/SonarAnalyzer.Common/Facade/SyntaxFacade.cs
+++ b/analyzers/src/SonarAnalyzer.Common/Facade/SyntaxFacade.cs
@@ -51,6 +51,7 @@ public abstract class SyntaxFacade
public abstract bool IsMemberAccessOnKnownType(SyntaxNode memberAccess, string name, KnownType knownType, SemanticModel semanticModel);
public abstract bool IsNullLiteral(SyntaxNode node);
public abstract bool IsStatic(SyntaxNode node);
+ public abstract bool IsWrittenTo(SyntaxNode expression, SemanticModel semanticModel, CancellationToken cancellationToken);
public abstract TSyntaxKind Kind(SyntaxNode node);
public abstract string LiteralText(SyntaxNode literal);
public abstract ImmutableArray LocalDeclarationIdentifiers(SyntaxNode node);
diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/SyntaxNodeExtensions.Roslyn.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/SyntaxNodeExtensions.Roslyn.cs
new file mode 100644
index 00000000000..dbad0012a3b
--- /dev/null
+++ b/analyzers/src/SonarAnalyzer.Common/Helpers/SyntaxNodeExtensions.Roslyn.cs
@@ -0,0 +1,67 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.CodeAnalysis.Shared.Extensions;
+
+[ExcludeFromCodeCoverage]
+internal static class SyntaxNodeExtensions
+{
+ ///
+ /// Returns true if is a given token is a child token of a certain type of parent node.
+ ///
+ /// The type of the parent node.
+ /// The node that we are testing.
+ /// A function that, when given the parent node, returns the child token we are interested in.
+ ///
+ /// Copied from
+ ///
+ public static bool IsChildNode(this SyntaxNode node, Func childGetter) where TParent : SyntaxNode
+ {
+ var ancestor = node.GetAncestor();
+ if (ancestor == null)
+ {
+ return false;
+ }
+
+ var ancestorNode = childGetter(ancestor);
+
+ return node == ancestorNode;
+ }
+
+ // Copy of
+ // https://github.com/dotnet/roslyn/blob/5a1cc5f83e4baba57f0355a685a5d1f487bfac66/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/SyntaxNodeExtensions.cs#L56
+ public static TNode GetAncestor(this SyntaxNode node) where TNode : SyntaxNode
+ {
+ var current = node.Parent;
+ while (current != null)
+ {
+ if (current is TNode tNode)
+ {
+ return tNode;
+ }
+
+ current = current.GetParent(ascendOutOfTrivia: true);
+ }
+
+ return null;
+ }
+
+ // Copy of
+ // https://github.com/dotnet/roslyn/blob/5a1cc5f83e4baba57f0355a685a5d1f487bfac66/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/SyntaxNodeExtensions.cs#L811
+ public static SyntaxNode GetParent(this SyntaxNode node, bool ascendOutOfTrivia)
+ {
+ var parent = node.Parent;
+ if (parent == null && ascendOutOfTrivia)
+ {
+ if (node is IStructuredTriviaSyntax structuredTrivia)
+ {
+ parent = structuredTrivia.ParentTrivia.Token.Parent;
+ }
+ }
+
+ return parent;
+ }
+}
diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/ExpressionSyntaxExtensions.Roslyn.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/ExpressionSyntaxExtensions.Roslyn.cs
new file mode 100644
index 00000000000..9762bbfe3c4
--- /dev/null
+++ b/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/ExpressionSyntaxExtensions.Roslyn.cs
@@ -0,0 +1,164 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.CodeAnalysis.VisualBasic.Extensions;
+
+[ExcludeFromCodeCoverage]
+internal static class ExpressionSyntaxExtensions
+{
+ ///
+ /// Returns if represents a node where a value is written to, like on the left side of an assignment expression. This method
+ /// also returns for potentially writeable expressions, like parameters.
+ /// See also .
+ ///
+ ///
+ /// Copied from
+ ///
+ public static bool IsWrittenTo(this ExpressionSyntax expression, SemanticModel semanticModel, CancellationToken cancellationToken)
+ {
+ if (expression == null)
+ return false;
+
+ if (expression.IsOnlyWrittenTo())
+ return true;
+
+ if (expression.IsRightSideOfDot())
+ expression = expression.Parent as ExpressionSyntax;
+
+ if (expression != null)
+ {
+ if (expression.IsInRefContext(semanticModel, cancellationToken))
+ return true;
+
+ if (expression.Parent is AssignmentStatementSyntax)
+ {
+ var assignmentStatement = (AssignmentStatementSyntax)expression.Parent;
+ if (expression == assignmentStatement.Left)
+ return true;
+ }
+
+ if (expression.IsChildNode(n => n.Name))
+ return true;
+
+ // Extension method with a 'ref' parameter can write to the value it is called on.
+ if (expression.Parent is MemberAccessExpressionSyntax)
+ {
+ var memberAccess = (MemberAccessExpressionSyntax)expression.Parent;
+ if (memberAccess.Expression == expression)
+ {
+ var method = semanticModel.GetSymbolInfo(memberAccess, cancellationToken).Symbol as IMethodSymbol;
+ if (method != null)
+ {
+ if (method.MethodKind == MethodKind.ReducedExtension && method.ReducedFrom.Parameters.Length > 0 && method.ReducedFrom.Parameters.First().RefKind == RefKind.Ref)
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ return false;
+ }
+
+ // Copy of
+ // https://github.com/dotnet/roslyn/blob/5a1cc5f83e4baba57f0355a685a5d1f487bfac66/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb#L325
+ public static bool IsOnlyWrittenTo(this ExpressionSyntax expression)
+ {
+ if (expression.IsRightSideOfDot())
+ expression = expression.Parent as ExpressionSyntax;
+
+ if (expression != null)
+ {
+ // Sonar: IsInOutContext deleted because not relevant for VB
+ if (expression.IsParentKind(SyntaxKind.SimpleAssignmentStatement))
+ {
+ var assignmentStatement = (AssignmentStatementSyntax)expression.Parent;
+ if (expression == assignmentStatement.Left)
+ return true;
+ }
+
+ if (expression.IsParentKind(SyntaxKind.NameColonEquals) && expression.Parent.IsParentKind(SyntaxKind.SimpleArgument))
+
+ //
+ // this is only a write to Prop
+ return true;
+
+ if (expression.IsChildNode(n => n.Name))
+ return true;
+
+ return false;
+ }
+
+ return false;
+ }
+
+ // Copy of
+ // https://github.com/dotnet/roslyn/blob/5a1cc5f83e4baba57f0355a685a5d1f487bfac66/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb#L73
+ public static bool IsRightSideOfDot(this ExpressionSyntax expression)
+ {
+ return expression.IsSimpleMemberAccessExpressionName() || expression.IsRightSideOfQualifiedName();
+ }
+
+ // Copy of
+ // https://github.com/dotnet/roslyn/blob/5a1cc5f83e4baba57f0355a685a5d1f487bfac66/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb#L56
+ public static bool IsSimpleMemberAccessExpressionName(this ExpressionSyntax expression)
+ {
+ return expression.IsParentKind(SyntaxKind.SimpleMemberAccessExpression) && ((MemberAccessExpressionSyntax)expression.Parent).Name == expression;
+ }
+
+ // Copy of
+ // https://github.com/dotnet/roslyn/blob/5a1cc5f83e4baba57f0355a685a5d1f487bfac66/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb#L78
+ public static bool IsRightSideOfQualifiedName(this ExpressionSyntax expression)
+ {
+ return expression.IsParentKind(SyntaxKind.QualifiedName) && ((QualifiedNameSyntax)expression.Parent).Right == expression;
+ }
+
+ // Copy of
+ // https://github.com/dotnet/roslyn/blob/5a1cc5f83e4baba57f0355a685a5d1f487bfac66/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb#L277
+ public static bool IsInRefContext(this ExpressionSyntax expression, SemanticModel semanticModel, CancellationToken cancellationToken)
+ {
+ var simpleArgument = expression?.Parent as SimpleArgumentSyntax;
+
+ if (simpleArgument == null)
+ return false;
+ else if (simpleArgument.IsNamed)
+ {
+ var info = semanticModel.GetSymbolInfo(simpleArgument.NameColonEquals.Name, cancellationToken);
+
+ var parameter = info.Symbol as IParameterSymbol;
+ return parameter != null && parameter.RefKind != RefKind.None;
+ }
+ else
+ {
+ var argumentList = simpleArgument.Parent as ArgumentListSyntax;
+
+ if (argumentList != null)
+ {
+ var parent = argumentList.Parent;
+ var index = argumentList.Arguments.IndexOf(simpleArgument);
+
+ var info = semanticModel.GetSymbolInfo(parent, cancellationToken);
+ var symbol = info.Symbol;
+
+ if (symbol is IMethodSymbol)
+ {
+ var method = (IMethodSymbol)symbol;
+ if (index < method.Parameters.Length)
+ return method.Parameters[index].RefKind != RefKind.None;
+ }
+ else if (symbol is IPropertySymbol)
+ {
+ var prop = (IPropertySymbol)symbol;
+ if (index < prop.Parameters.Length)
+ return prop.Parameters[index].RefKind != RefKind.None;
+ }
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/SyntaxNodeExtensions.Roslyn.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/SyntaxNodeExtensions.Roslyn.cs
new file mode 100644
index 00000000000..1c333b3ea3d
--- /dev/null
+++ b/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/SyntaxNodeExtensions.Roslyn.cs
@@ -0,0 +1,34 @@
+/*
+* SonarAnalyzer for .NET
+* Copyright (C) 2015-2023 SonarSource SA
+* mailto: contact AT sonarsource DOT com
+*
+* This program is free software; you can redistribute it and/or
+* modify it under the terms of the GNU Lesser General Public
+* License as published by the Free Software Foundation; either
+* version 3 of the License, or (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this program; if not, write to the Free Software Foundation,
+* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.CodeAnalysis.VisualBasic.Extensions;
+
+[ExcludeFromCodeCoverage]
+internal static class SyntaxNodeExtensions
+{
+ // Copied and converted from
+ // https://github.com/dotnet/roslyn/blob/5a1cc5f83e4baba57f0355a685a5d1f487bfac66/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/SyntaxNodeExtensions.vb#L16
+ public static bool IsParentKind(this SyntaxNode node, SyntaxKind kind)
+ {
+ return node != null && node.Parent.IsKind(kind);
+ }
+}
diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxFacade.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxFacade.cs
index 0cb42fd1178..a027e706ae5 100644
--- a/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxFacade.cs
+++ b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxFacade.cs
@@ -108,6 +108,10 @@ internal sealed class VisualBasicSyntaxFacade : SyntaxFacade
public override bool IsStatic(SyntaxNode node) =>
Cast(node).IsShared();
+ ///
+ public override bool IsWrittenTo(SyntaxNode expression, SemanticModel semanticModel, CancellationToken cancellationToken) =>
+ Cast(expression).IsWrittenTo(semanticModel, cancellationToken);
+
public override SyntaxKind Kind(SyntaxNode node) => node.Kind();
public override string LiteralText(SyntaxNode literal) =>
diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/SonarAnalyzer.VisualBasic.csproj b/analyzers/src/SonarAnalyzer.VisualBasic/SonarAnalyzer.VisualBasic.csproj
index a875815cccd..dd751dbc175 100644
--- a/analyzers/src/SonarAnalyzer.VisualBasic/SonarAnalyzer.VisualBasic.csproj
+++ b/analyzers/src/SonarAnalyzer.VisualBasic/SonarAnalyzer.VisualBasic.csproj
@@ -30,6 +30,8 @@
+
+
diff --git a/analyzers/tests/SonarAnalyzer.Test/Extensions/SyntaxNodeExtensionsTest.cs b/analyzers/tests/SonarAnalyzer.Test/Extensions/SyntaxNodeExtensionsTest.cs
index 27f024af618..67613ad668d 100644
--- a/analyzers/tests/SonarAnalyzer.Test/Extensions/SyntaxNodeExtensionsTest.cs
+++ b/analyzers/tests/SonarAnalyzer.Test/Extensions/SyntaxNodeExtensionsTest.cs
@@ -30,6 +30,7 @@
using static csharp::SonarAnalyzer.Extensions.SyntaxTokenExtensions;
using ExtensionsCS = csharp::SonarAnalyzer.Extensions.SyntaxNodeExtensions;
using ExtensionsVB = vbnet::SonarAnalyzer.Extensions.SyntaxNodeExtensions;
+using MicrosoftExtensionsCS = csharp::Microsoft.CodeAnalysis.CSharp.Extensions.SyntaxNodeExtensions;
using SyntaxCS = Microsoft.CodeAnalysis.CSharp.Syntax;
using SyntaxVB = Microsoft.CodeAnalysis.VisualBasic.Syntax;
@@ -691,7 +692,7 @@ public X M()
}
""";
var node = NodeBetweenMarkers(code, LanguageNames.CSharp);
- var parentConditional = ExtensionsCS.GetParentConditionalAccessExpression(node);
+ var parentConditional = MicrosoftExtensionsCS.GetParentConditionalAccessExpression(node);
parentConditional.ToString().Should().Be(parent);
parentConditional.Expression.ToString().Should().Be(parentExpression);
}
@@ -759,7 +760,7 @@ public X M()
}}
";
var node = NodeBetweenMarkers(code, LanguageNames.CSharp);
- var parentConditional = ExtensionsCS.GetRootConditionalAccessExpression(node);
+ var parentConditional = MicrosoftExtensionsCS.GetRootConditionalAccessExpression(node);
parentConditional.ToString().Should().Be(expression.Replace("$$", string.Empty));
}