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