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/SyntaxNodeExtensionsCSharp.cs b/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensionsCSharp.cs index e8194b224d6..80c57e432a8 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensionsCSharp.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensionsCSharp.cs @@ -210,6 +210,7 @@ node switch PointerTypeSyntax { ElementType: { } elementType } => GetIdentifier(elementType), PredefinedTypeSyntax { Keyword: var keyword } => keyword, QualifiedNameSyntax { Right.Identifier: var identifier } => identifier, + SimpleBaseTypeSyntax { Type: { } type } => GetIdentifier(type), SimpleNameSyntax { Identifier: var identifier } => identifier, TypeParameterConstraintClauseSyntax { Name.Identifier: var identifier } => identifier, TypeParameterSyntax { Identifier: var identifier } => identifier, @@ -375,24 +376,15 @@ 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) => + // based on Type="BaseArgumentListSyntax" in https://github.com/dotnet/roslyn/blob/main/src/Compilers/CSharp/Portable/Syntax/Syntax.xml + public static BaseArgumentListSyntax ArgumentList(this SyntaxNode node) => node switch { ObjectCreationExpressionSyntax creation => creation.ArgumentList, InvocationExpressionSyntax invocation => invocation.ArgumentList, ConstructorInitializerSyntax constructorInitializer => constructorInitializer.ArgumentList, + ElementAccessExpressionSyntax x => x.ArgumentList, + ElementBindingExpressionSyntax x => x.ArgumentList, null => null, _ when PrimaryConstructorBaseTypeSyntaxWrapper.IsInstance(node) => ((PrimaryConstructorBaseTypeSyntaxWrapper)node).ArgumentList, _ when ImplicitObjectCreationExpressionSyntaxWrapper.IsInstance(node) => ((ImplicitObjectCreationExpressionSyntaxWrapper)node).ArgumentList, @@ -411,110 +403,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; - } - public static BlockSyntax GetBody(this SyntaxNode node) => node switch { 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/Facade/CSharpTrackerFacade.cs b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpTrackerFacade.cs index 5355c39d22e..2fe0db1e731 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpTrackerFacade.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpTrackerFacade.cs @@ -24,6 +24,7 @@ namespace SonarAnalyzer.Helpers.Facade { internal sealed class CSharpTrackerFacade : ITrackerFacade { + public ArgumentTracker Argument => new CSharpArgumentTracker(); public BaseTypeTracker BaseType { get; } = new CSharpBaseTypeTracker(); public ElementAccessTracker ElementAccess { get; } = new CSharpElementAccessTracker(); public FieldAccessTracker FieldAccess { get; } = new CSharpFieldAccessTracker(); diff --git a/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpAttributeParameterLookup.cs b/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpAttributeParameterLookup.cs index f49481b7871..6e4adf5aa6f 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpAttributeParameterLookup.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpAttributeParameterLookup.cs @@ -20,14 +20,15 @@ namespace SonarAnalyzer.Helpers; -internal class CSharpAttributeParameterLookup : MethodParameterLookupBase +internal class CSharpAttributeParameterLookup(AttributeSyntax attribute, IMethodSymbol methodSymbol) : + MethodParameterLookupBase(attribute.ArgumentList?.Arguments ?? default, methodSymbol) { - public CSharpAttributeParameterLookup(AttributeSyntax attribute, IMethodSymbol methodSymbol) - : base(attribute.ArgumentList?.Arguments ?? default, methodSymbol) { } - protected override SyntaxNode Expression(AttributeArgumentSyntax argument) => argument.Expression; - protected override SyntaxToken? GetNameColonArgumentIdentifier(AttributeArgumentSyntax argument) => + protected override SyntaxToken? GetNameColonIdentifier(AttributeArgumentSyntax argument) => argument.NameColon?.Name.Identifier; + + protected override SyntaxToken? GetNameEqualsIdentifier(AttributeArgumentSyntax argument) => + argument.NameEquals?.Name.Identifier; } diff --git a/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpMethodParameterLookup.cs b/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpMethodParameterLookup.cs index 7c85b5a6e13..4db91a6f255 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpMethodParameterLookup.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpMethodParameterLookup.cs @@ -28,15 +28,18 @@ public CSharpMethodParameterLookup(InvocationExpressionSyntax invocation, Semant public CSharpMethodParameterLookup(InvocationExpressionSyntax invocation, IMethodSymbol methodSymbol) : this(invocation.ArgumentList, methodSymbol) { } - public CSharpMethodParameterLookup(ArgumentListSyntax argumentList, SemanticModel semanticModel) + public CSharpMethodParameterLookup(BaseArgumentListSyntax argumentList, SemanticModel semanticModel) : base(argumentList.Arguments, semanticModel.GetSymbolInfo(argumentList.Parent)) { } - public CSharpMethodParameterLookup(ArgumentListSyntax argumentList, IMethodSymbol methodSymbol) + public CSharpMethodParameterLookup(BaseArgumentListSyntax argumentList, IMethodSymbol methodSymbol) : base(argumentList.Arguments, methodSymbol) { } protected override SyntaxNode Expression(ArgumentSyntax argument) => argument.Expression; - protected override SyntaxToken? GetNameColonArgumentIdentifier(ArgumentSyntax argument) => + protected override SyntaxToken? GetNameColonIdentifier(ArgumentSyntax argument) => argument.NameColon?.Name.Identifier; + + protected override SyntaxToken? GetNameEqualsIdentifier(ArgumentSyntax argument) => + null; } diff --git a/analyzers/src/SonarAnalyzer.CSharp/SonarAnalyzer.CSharp.csproj b/analyzers/src/SonarAnalyzer.CSharp/SonarAnalyzer.CSharp.csproj index b51e6bd98f7..4216d9b92e6 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/SonarAnalyzer.CSharp.csproj +++ b/analyzers/src/SonarAnalyzer.CSharp/SonarAnalyzer.CSharp.csproj @@ -35,6 +35,7 @@ + diff --git a/analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs b/analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs new file mode 100644 index 00000000000..bdaf70d80dd --- /dev/null +++ b/analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs @@ -0,0 +1,105 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2024 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. + */ + +namespace SonarAnalyzer.Helpers.Trackers; + +internal sealed class CSharpArgumentTracker : ArgumentTracker +{ + protected override SyntaxKind[] TrackedSyntaxKinds => + [ + SyntaxKind.AttributeArgument, + SyntaxKind.Argument, + ]; + + protected override ILanguageFacade Language => CSharpFacade.Instance; + + protected override IReadOnlyCollection ArgumentList(SyntaxNode argumentNode) => + argumentNode switch + { + AttributeArgumentSyntax { Parent: AttributeArgumentListSyntax { Arguments: { } list } } => list, + ArgumentSyntax { Parent: BaseArgumentListSyntax { Arguments: { } list } } => list, + _ => null, + }; + + protected override int? Position(SyntaxNode argumentNode) => + argumentNode is ArgumentSyntax { NameColon: not null } + or AttributeArgumentSyntax { NameColon: not null } + or AttributeArgumentSyntax { NameEquals: not null } + ? null + : ArgumentList(argumentNode).IndexOf(x => x == argumentNode); + + protected override RefKind? ArgumentRefKind(SyntaxNode argumentNode) => + argumentNode switch + { + ArgumentSyntax { RefOrOutKeyword: { } refOrOut } => refOrOut.Kind() switch + { + SyntaxKind.OutKeyword => RefKind.Out, + SyntaxKind.RefKeyword => RefKind.Ref, + SyntaxKind.InKeyword => RefKindEx.In, + _ => RefKind.None + }, + AttributeArgumentSyntax => null, // RefKind is not supported for attributes and there is no way to specify such a constraint for Attributes in ArgumentDescriptor. + _ => null, + }; + + protected override bool InvocationMatchesMemberKind(SyntaxNode invokedExpression, MemberKind memberKind) => + memberKind switch + { + MemberKind.Method => invokedExpression is InvocationExpressionSyntax, + MemberKind.Constructor => invokedExpression is ObjectCreationExpressionSyntax + or ConstructorInitializerSyntax + || ImplicitObjectCreationExpressionSyntaxWrapper.IsInstance(invokedExpression), + MemberKind.Indexer => invokedExpression is ElementAccessExpressionSyntax or ElementBindingExpressionSyntax, + MemberKind.Attribute => invokedExpression is AttributeSyntax, + _ => false, + }; + + protected override bool InvokedMemberMatches(SemanticModel model, SyntaxNode invokedExpression, MemberKind memberKind, Func invokedMemberNameConstraint) => + memberKind switch + { + MemberKind.Method => invokedMemberNameConstraint(invokedExpression.GetName()), + MemberKind.Constructor => invokedExpression switch + { + ObjectCreationExpressionSyntax { Type: { } typeName } => invokedMemberNameConstraint(typeName.GetName()), + ConstructorInitializerSyntax x => FindClassNameFromConstructorInitializerSyntax(x) is not string name || invokedMemberNameConstraint(name), + { } x when ImplicitObjectCreationExpressionSyntaxWrapper.IsInstance(x) => invokedMemberNameConstraint(model.GetSymbolInfo(x).Symbol?.ContainingType?.Name), + _ => false, + }, + MemberKind.Indexer => invokedExpression switch + { + ElementAccessExpressionSyntax { Expression: { } accessedExpression } => invokedMemberNameConstraint(accessedExpression.GetName()), + ElementBindingExpressionSyntax binding => binding.GetParentConditionalAccessExpression() is { Expression: { } accessedExpression } + && invokedMemberNameConstraint(accessedExpression.GetName()), + _ => false, + }, + MemberKind.Attribute => invokedExpression is AttributeSyntax { Name: { } typeName } && invokedMemberNameConstraint(typeName.GetName()), + _ => false, + }; + + private static string FindClassNameFromConstructorInitializerSyntax(ConstructorInitializerSyntax initializerSyntax) => + initializerSyntax.ThisOrBaseKeyword.Kind() switch + { + SyntaxKind.ThisKeyword => initializerSyntax is { Parent: ConstructorDeclarationSyntax { Identifier.ValueText: { } typeName } } ? typeName : null, + SyntaxKind.BaseKeyword => initializerSyntax is { Parent: ConstructorDeclarationSyntax { Parent: BaseTypeDeclarationSyntax { BaseList.Types: { Count: > 0 } baseListTypes } } } + ? baseListTypes.First().GetName() // Get the class name of the called constructor from the base types list of the type declaration + : null, + _ => null, + }; +} diff --git a/analyzers/src/SonarAnalyzer.Common/Facade/ITrackerFacade.cs b/analyzers/src/SonarAnalyzer.Common/Facade/ITrackerFacade.cs index 226b7ae5ebe..869789f9aca 100644 --- a/analyzers/src/SonarAnalyzer.Common/Facade/ITrackerFacade.cs +++ b/analyzers/src/SonarAnalyzer.Common/Facade/ITrackerFacade.cs @@ -25,6 +25,7 @@ namespace SonarAnalyzer.Helpers.Facade public interface ITrackerFacade where TSyntaxKind : struct { + ArgumentTracker Argument { get; } BaseTypeTracker BaseType { get; } ElementAccessTracker ElementAccess { get; } FieldAccessTracker FieldAccess { get; } 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/ArgumentDescriptor.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/ArgumentDescriptor.cs new file mode 100644 index 00000000000..8cdde505812 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/ArgumentDescriptor.cs @@ -0,0 +1,189 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2024 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. + */ + +namespace SonarAnalyzer.Helpers; + +public enum MemberKind +{ + Method, + Constructor, + Indexer, + Attribute +} + +public class ArgumentDescriptor +{ + public MemberKind MemberKind { get; } + public Func, int?, bool> ArgumentListConstraint { get; } + public RefKind? RefKind { get; } + public Func ParameterConstraint { get; } + public Func InvokedMemberNameConstraint { get; } + public Func InvokedMemberNodeConstraint { get; } + public Func InvokedMemberConstraint { get; } + + private ArgumentDescriptor(MemberKind memberKind, + Func invokedMemberConstraint, + Func invokedMemberNameConstraint, + Func invokedMemberNodeConstraint, + Func, int?, bool> argumentListConstraint, + Func parameterConstraint, + RefKind? refKind) + { + MemberKind = memberKind; + ArgumentListConstraint = argumentListConstraint; + RefKind = refKind; + ParameterConstraint = parameterConstraint; + InvokedMemberNameConstraint = invokedMemberNameConstraint; + InvokedMemberNodeConstraint = invokedMemberNodeConstraint; + InvokedMemberConstraint = invokedMemberConstraint; + } + + public static ArgumentDescriptor MethodInvocation(KnownType invokedType, string methodName, string parameterName, int argumentPosition) => + MethodInvocation(invokedType, methodName, parameterName, x => x == argumentPosition); + + public static ArgumentDescriptor MethodInvocation(KnownType invokedType, string methodName, string parameterName, Func argumentPosition) => + MethodInvocation(invokedType, methodName, x => x.Name == parameterName, argumentPosition, null); + + public static ArgumentDescriptor MethodInvocation(KnownType invokedType, string methodName, string parameterName, Func argumentPosition, RefKind refKind) => + MethodInvocation(invokedType, methodName, x => x.Name == parameterName, argumentPosition, refKind); + + public static ArgumentDescriptor MethodInvocation(KnownType invokedType, + Func invokedMemberNameConstraint, + Func parameterConstraint, + Func argumentPosition, + RefKind? refKind) => + MethodInvocation(x => invokedType.Matches(x.ContainingType), invokedMemberNameConstraint, parameterConstraint, argumentPosition, refKind); + + public static ArgumentDescriptor MethodInvocation(Func invokedMemberConstraint, + Func invokedMemberNameConstraint, + Func parameterConstraint, + Func argumentPosition, + RefKind? refKind) => + MethodInvocation(invokedMemberConstraint, + invokedMemberNameConstraint, + (_, _, _) => true, + parameterConstraint, + (_, position) => position is null || argumentPosition is null || argumentPosition(position.Value), + refKind); + + public static ArgumentDescriptor MethodInvocation(Func invokedMemberConstraint, + Func invokedMemberNameConstraint, + Func invokedMemberNodeConstraint, + Func parameterConstraint, + Func, int?, bool> argumentListConstraint, + RefKind? refKind) => + new(MemberKind.Method, invokedMemberConstraint, invokedMemberNameConstraint, invokedMemberNodeConstraint, argumentListConstraint, parameterConstraint, refKind); + + public static ArgumentDescriptor ConstructorInvocation(KnownType constructedType, string parameterName, int argumentPosition) => + ConstructorInvocation( + x => constructedType.Matches(x.ContainingType), + (x, c) => x.Equals(constructedType.TypeName, c), + static (_, _, _) => true, + x => x.Name == parameterName, + (_, x) => x is null || x == argumentPosition, + null); + + public static ArgumentDescriptor ConstructorInvocation(Func invokedMethodSymbol, + Func invokedMemberNameConstraint, + Func invokedMemberNodeConstraint, + Func parameterConstraint, + Func, int?, bool> argumentListConstraint, + RefKind? refKind) => + new(MemberKind.Constructor, + invokedMemberConstraint: invokedMethodSymbol, + invokedMemberNameConstraint, + invokedMemberNodeConstraint, + argumentListConstraint, + parameterConstraint, + refKind); + + public static ArgumentDescriptor ElementAccess(KnownType invokedIndexerContainer, Func parameterConstraint, int argumentPosition) => + ElementAccess(invokedIndexerContainer, null, parameterConstraint, x => x == argumentPosition); + + public static ArgumentDescriptor ElementAccess(KnownType invokedIndexerContainer, string invokedIndexerExpression, Func parameterConstraint, int argumentPosition) => + ElementAccess(invokedIndexerContainer, invokedIndexerExpression, parameterConstraint, x => x == argumentPosition); + + public static ArgumentDescriptor ElementAccess(KnownType invokedIndexerContainer, Func parameterConstraint, Func argumentPositionConstraint) => + ElementAccess(invokedIndexerContainer, null, parameterConstraint, argumentPositionConstraint); + + public static ArgumentDescriptor ElementAccess(KnownType invokedIndexerContainer, + string invokedIndexerExpression, + Func parameterConstraint, + Func argumentPositionConstraint) => + + ElementAccess( + x => x is { ContainingSymbol: INamedTypeSymbol container } && invokedIndexerContainer.Matches(container), + (s, c) => invokedIndexerExpression is null || s.Equals(invokedIndexerExpression, c), + (_, _, _) => true, + parameterConstraint, + (_, p) => argumentPositionConstraint is null || p is null || argumentPositionConstraint(p.Value)); + + public static ArgumentDescriptor ElementAccess(Func invokedIndexerPropertyMethod, + Func invokedIndexerExpression, + Func invokedIndexerExpressionNodeConstraint, + Func parameterConstraint, + Func, int?, bool> argumentListConstraint) => + new(MemberKind.Indexer, + invokedMemberConstraint: invokedIndexerPropertyMethod, + invokedMemberNameConstraint: invokedIndexerExpression, + invokedMemberNodeConstraint: invokedIndexerExpressionNodeConstraint, + argumentListConstraint: argumentListConstraint, + parameterConstraint: parameterConstraint, + refKind: null); + + public static ArgumentDescriptor AttributeArgument(string attributeName, string parameterName, int argumentPosition) => + AttributeArgument( + x => x is { MethodKind: MethodKind.Constructor, ContainingType.Name: { } name } && AttributeClassNameConstraint(attributeName, name, StringComparison.Ordinal), + (x, c) => AttributeClassNameConstraint(attributeName, x, c), + (_, _, _) => true, + x => x.Name == parameterName, + (_, i) => i is null || i.Value == argumentPosition); + + public static ArgumentDescriptor AttributeArgument(Func attributeConstructorConstraint, + Func attributeNameConstraint, + Func attributeNodeConstraint, + Func parameterConstraint, + Func, int?, bool> argumentListConstraint) => + new(MemberKind.Attribute, + invokedMemberConstraint: attributeConstructorConstraint, + invokedMemberNameConstraint: attributeNameConstraint, + invokedMemberNodeConstraint: attributeNodeConstraint, + argumentListConstraint: argumentListConstraint, + parameterConstraint: parameterConstraint, + refKind: null); + + public static ArgumentDescriptor AttributeProperty(string attributeName, string propertyName) => + AttributeArgument( + attributeConstructorConstraint: x => x is { MethodKind: MethodKind.PropertySet, AssociatedSymbol.Name: { } name } && name == propertyName, + attributeNameConstraint: (s, c) => AttributeClassNameConstraint(attributeName, s, c), + (_, _, _) => true, + parameterConstraint: _ => true, + argumentListConstraint: (_, _) => true); + + private static bool AttributeClassNameConstraint(string expectedAttributeName, string nodeClassName, StringComparison c) => + nodeClassName.Equals(expectedAttributeName, c) || nodeClassName.Equals($"{expectedAttributeName}Attribute"); + + private static ArgumentDescriptor MethodInvocation(KnownType invokedType, + string methodName, + Func parameterConstraint, + Func argumentPosition, + RefKind? refKind) => + MethodInvocation(invokedType, (n, c) => n.Equals(methodName, c), parameterConstraint, argumentPosition, refKind); +} diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs index 18704e5f8c0..4e0e9834b1a 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs @@ -242,6 +242,7 @@ public sealed partial class KnownType public static readonly KnownType System_Collections_DictionaryBase = new("System.Collections.DictionaryBase"); public static readonly KnownType System_Collections_Frozen_FrozenDictionary_TKey_TValue = new("System.Collections.Frozen.FrozenDictionary", "TKey", "TValue"); public static readonly KnownType System_Collections_Frozen_FrozenSet_T = new("System.Collections.Frozen.FrozenSet", "T"); + public static readonly KnownType System_Collections_Generic_Comparer_T = new("System.Collections.Generic.Comparer", "T"); public static readonly KnownType System_Collections_Generic_Dictionary_TKey_TValue = new("System.Collections.Generic.Dictionary", "TKey", "TValue"); public static readonly KnownType System_Collections_Generic_HashSet_T = new("System.Collections.Generic.HashSet", "T"); public static readonly KnownType System_Collections_Generic_IAsyncEnumerable_T = new("System.Collections.Generic.IAsyncEnumerable", "T"); diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/MethodParameterLookupBase.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/MethodParameterLookupBase.cs index 43249717989..c146f3a1543 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/MethodParameterLookupBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/MethodParameterLookupBase.cs @@ -22,11 +22,11 @@ namespace SonarAnalyzer.Helpers; public interface IMethodParameterLookup { + IMethodSymbol MethodSymbol { get; } bool TryGetSymbol(SyntaxNode argument, out IParameterSymbol parameter); bool TryGetSyntax(IParameterSymbol parameter, out ImmutableArray expressions); bool TryGetSyntax(string parameterName, out ImmutableArray expressions); bool TryGetNonParamsSyntax(IParameterSymbol parameter, out SyntaxNode expression); - IMethodSymbol MethodSymbol { get; } } // This should come from the Roslyn API (https://github.com/dotnet/roslyn/issues/9) @@ -35,7 +35,8 @@ internal abstract class MethodParameterLookupBase : IMethodPara { private readonly SeparatedSyntaxList argumentList; - protected abstract SyntaxToken? GetNameColonArgumentIdentifier(TArgumentSyntax argument); + protected abstract SyntaxToken? GetNameColonIdentifier(TArgumentSyntax argument); + protected abstract SyntaxToken? GetNameEqualsIdentifier(TArgumentSyntax argument); protected abstract SyntaxNode Expression(TArgumentSyntax argument); public IMethodSymbol MethodSymbol { get; } @@ -63,16 +64,26 @@ private bool TryGetSymbol(SyntaxNode argument, IMethodSymbol methodSymbol, out I var arg = argument as TArgumentSyntax ?? throw new ArgumentException($"{nameof(argument)} must be of type {typeof(TArgumentSyntax)}", nameof(argument)); if (!argumentList.Contains(arg) - || methodSymbol == null + || methodSymbol is null || methodSymbol.IsVararg) { return false; } - if (GetNameColonArgumentIdentifier(arg) is { } nameColonArgumentIdentifier) + if (GetNameColonIdentifier(arg) is { } nameColonIdentifier) + { + parameter = methodSymbol.Parameters.FirstOrDefault(x => x.Name == nameColonIdentifier.ValueText); + return parameter is not null; + } + + if (GetNameEqualsIdentifier(arg) is { } nameEqualsIdentifier + && methodSymbol.ContainingType.GetMembers(nameEqualsIdentifier.ValueText) is { Length: 1 } properties + && properties[0] is IPropertySymbol { SetMethod: { } setter } property + && property.Name == nameEqualsIdentifier.ValueText + && setter.Parameters is { Length: 1 } parameters) { - parameter = methodSymbol.Parameters.FirstOrDefault(symbol => symbol.Name == nameColonArgumentIdentifier.ValueText); - return parameter != null; + parameter = parameters[0]; + return parameter is not null; } var index = argumentList.IndexOf(arg); @@ -80,7 +91,7 @@ private bool TryGetSymbol(SyntaxNode argument, IMethodSymbol methodSymbol, out I { var lastParameter = methodSymbol.Parameters.Last(); parameter = lastParameter.IsParams ? lastParameter : null; - return parameter != null; + return parameter is not null; } parameter = methodSymbol.Parameters[index]; return true; diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/RefKindEx.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/RefKindEx.cs index 72d54c21457..dd9d22876dd 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/RefKindEx.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/RefKindEx.cs @@ -20,8 +20,11 @@ namespace SonarAnalyzer.Helpers { +#pragma warning disable S2339 // Public constant members should not be used public static class RefKindEx { - public static readonly RefKind In = (RefKind)3; + public const RefKind In = (RefKind)3; + public const RefKind RefReadOnlyParameter = (RefKind)4; } +#pragma warning restore S2339 // Public constant members should not be used } 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.Common/Trackers/ArgumentContext.cs b/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentContext.cs new file mode 100644 index 00000000000..3befd88f6fd --- /dev/null +++ b/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentContext.cs @@ -0,0 +1,30 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2024 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. + */ + +namespace SonarAnalyzer.Helpers; + +public class ArgumentContext : SyntaxBaseContext +{ + public IParameterSymbol Parameter { get; internal set; } + + public ArgumentContext(SonarSyntaxNodeReportingContext context) : base(context) { } + + public ArgumentContext(SyntaxNode node, SemanticModel semanticModel) : base(node, semanticModel) { } +} diff --git a/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentTracker.cs b/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentTracker.cs new file mode 100644 index 00000000000..f3518bb0ed1 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentTracker.cs @@ -0,0 +1,96 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2024 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. + */ + +namespace SonarAnalyzer.Helpers.Trackers; + +public abstract class ArgumentTracker : SyntaxTrackerBase + where TSyntaxKind : struct +{ + protected abstract RefKind? ArgumentRefKind(SyntaxNode argumentNode); + protected abstract IReadOnlyCollection ArgumentList(SyntaxNode argumentNode); + protected abstract int? Position(SyntaxNode argumentNode); + protected abstract bool InvocationMatchesMemberKind(SyntaxNode invokedExpression, MemberKind memberKind); + protected abstract bool InvokedMemberMatches(SemanticModel model, SyntaxNode invokedExpression, MemberKind memberKind, Func invokedMemberNameConstraint); + + protected override ArgumentContext CreateContext(SonarSyntaxNodeReportingContext context) => + new(context); + + public Condition MatchArgument(ArgumentDescriptor descriptor) => + trackingContext => + { + if (trackingContext.Node is { } argumentNode + && argumentNode is { Parent.Parent: { } invoked } + && SyntacticChecks(trackingContext.SemanticModel, descriptor, argumentNode, invoked) + && (descriptor.InvokedMemberNodeConstraint?.Invoke(trackingContext.SemanticModel, Language, invoked) ?? true) + && MethodSymbol(trackingContext.SemanticModel, invoked) is { } methodSymbol + && Language.MethodParameterLookup(invoked, methodSymbol).TryGetSymbol(argumentNode, out var parameter) + && ParameterMatches(parameter, descriptor.ParameterConstraint, descriptor.InvokedMemberConstraint)) + { + trackingContext.Parameter = parameter; + return true; + } + return false; + }; + + private IMethodSymbol MethodSymbol(SemanticModel model, SyntaxNode invoked) => + model.GetSymbolInfo(invoked).Symbol switch + { + IMethodSymbol x => x, + IPropertySymbol propertySymbol => Language.Syntax.IsWrittenTo(invoked, model, CancellationToken.None) + ? propertySymbol.SetMethod + : propertySymbol.GetMethod, + _ => null, + }; + + // SemanticModel is needed for target-typed-new only. + private bool SyntacticChecks(SemanticModel model, ArgumentDescriptor descriptor, SyntaxNode argumentNode, SyntaxNode invokedExpression) => + InvocationMatchesMemberKind(invokedExpression, descriptor.MemberKind) + && RefKindMatches(descriptor, argumentNode) + && (descriptor.ArgumentListConstraint is null + || (ArgumentList(argumentNode) is { } argList && descriptor.ArgumentListConstraint(argList, Position(argumentNode)))) + && (descriptor.InvokedMemberNameConstraint is null + || InvokedMemberMatches(model, invokedExpression, descriptor.MemberKind, x => descriptor.InvokedMemberNameConstraint(x, Language.NameComparison))); + + private bool RefKindMatches(ArgumentDescriptor descriptor, SyntaxNode argumentNode) => + descriptor.RefKind is not { } expectedRefKind // When null: No RefKind constraint was specified + || ArgumentRefKind(argumentNode) is not { } actualRefKind // When null: In VB, the argument does not need ref/out keywords on the call side + || expectedRefKind == actualRefKind + // For parameter ref kind "in", on the call side "in" is optional or can be "ref" instead + // For "ref readonly" parameters, "none", "in" and "ref" are allowed on the call side + || (expectedRefKind is RefKindEx.In && actualRefKind is RefKind.None or RefKind.Ref) + || (expectedRefKind is RefKindEx.RefReadOnlyParameter && actualRefKind is RefKind.None or RefKind.Ref or RefKindEx.In); + + private static bool ParameterMatches(IParameterSymbol parameter, Func parameterConstraint, Func invokedMemberConstraint) + { + if (parameter.ContainingSymbol is IMethodSymbol method + && method.Parameters.IndexOf(parameter) is >= 0 and int position) + { + do + { + if (invokedMemberConstraint?.Invoke(method) is null or true && parameterConstraint?.Invoke(method.Parameters[position]) is null or true) + { + return true; + } + } + while ((method = method.OverriddenMethod) is not null); + } + return false; + } +} diff --git a/analyzers/src/SonarAnalyzer.Common/Trackers/TrackerBase.cs b/analyzers/src/SonarAnalyzer.Common/Trackers/TrackerBase.cs index 453ac4f70ab..089e33dfd6f 100644 --- a/analyzers/src/SonarAnalyzer.Common/Trackers/TrackerBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Trackers/TrackerBase.cs @@ -18,14 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -namespace SonarAnalyzer.Helpers +namespace SonarAnalyzer.Helpers; + +public abstract class TrackerBase + where TSyntaxKind : struct + where TContext : BaseContext { - public abstract class TrackerBase - where TSyntaxKind : struct - where TContext : BaseContext - { - public delegate bool Condition(TContext trackingContext); + protected abstract ILanguageFacade Language { get; } - protected abstract ILanguageFacade Language { get; } - } + public delegate bool Condition(TContext trackingContext); } 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..00e12f5a37f --- /dev/null +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/SyntaxNodeExtensions.Roslyn.cs @@ -0,0 +1,34 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2024 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/Extensions/SyntaxNodeExtensionsVisualBasic.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/SyntaxNodeExtensionsVisualBasic.cs index a14ac69f846..bb8453b8316 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/SyntaxNodeExtensionsVisualBasic.cs +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/SyntaxNodeExtensionsVisualBasic.cs @@ -89,6 +89,7 @@ static bool TakesExpressionTree(SymbolInfo info) EnumMemberDeclarationSyntax x => x.Identifier, InvocationExpressionSyntax x => x.Expression?.GetIdentifier(), ModifiedIdentifierSyntax x => x.Identifier, + ObjectCreationExpressionSyntax x => x.Type?.GetIdentifier(), PredefinedTypeSyntax x => x.Keyword, ParameterSyntax x => x.Identifier?.GetIdentifier(), PropertyStatementSyntax x => x.Identifier, @@ -104,6 +105,7 @@ static bool TakesExpressionTree(SymbolInfo info) public static ArgumentListSyntax ArgumentList(this SyntaxNode node) => node switch { + ArgumentListSyntax argumentList => argumentList, ArrayCreationExpressionSyntax arrayCreation => arrayCreation.ArrayBounds, AttributeSyntax attribute => attribute.ArgumentList, InvocationExpressionSyntax invocation => invocation.ArgumentList, diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicFacade.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicFacade.cs index b280347b9c8..3f7bb4d33d5 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicFacade.cs +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicFacade.cs @@ -51,9 +51,16 @@ internal sealed class VisualBasicFacade : ILanguageFacade node.FindConstantValue(model); public IMethodParameterLookup MethodParameterLookup(SyntaxNode invocation, IMethodSymbol methodSymbol) => - invocation?.ArgumentList() is { } argumentList - ? new VisualBasicMethodParameterLookup(argumentList, methodSymbol) - : null; + invocation switch + { + null => null, + AttributeSyntax x => new VisualBasicAttributeParameterLookup(x.ArgumentList.Arguments, methodSymbol), + IdentifierNameSyntax + { + Parent: NameColonEqualsSyntax { Parent: SimpleArgumentSyntax { IsNamed: true, Parent.Parent: AttributeSyntax attribute } } + } => new VisualBasicAttributeParameterLookup(attribute.ArgumentList.Arguments, methodSymbol), + _ => new VisualBasicMethodParameterLookup(invocation.ArgumentList(), methodSymbol), + }; public IMethodParameterLookup MethodParameterLookup(SyntaxNode invocation, SemanticModel semanticModel) => invocation?.ArgumentList() is { } argumentList 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/Facade/VisualBasicTrackerFacade.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicTrackerFacade.cs index d6912a40147..2700e8ec3fa 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicTrackerFacade.cs +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicTrackerFacade.cs @@ -24,6 +24,7 @@ namespace SonarAnalyzer.Helpers.Facade { internal sealed class VisualBasicTrackerFacade : ITrackerFacade { + public ArgumentTracker Argument => new VisualBasicArgumentTracker(); public BaseTypeTracker BaseType { get; } = new VisualBasicBaseTypeTracker(); public ElementAccessTracker ElementAccess { get; } = new VisualBasicElementAccessTracker(); public FieldAccessTracker FieldAccess { get; } = new VisualBasicFieldAccessTracker(); diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicAttributeParameterLookup.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicAttributeParameterLookup.cs new file mode 100644 index 00000000000..403b7b38164 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicAttributeParameterLookup.cs @@ -0,0 +1,35 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2024 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. + */ + +namespace SonarAnalyzer.Helpers; + +internal class VisualBasicAttributeParameterLookup(SeparatedSyntaxList argumentList, IMethodSymbol methodSymbol) : MethodParameterLookupBase(argumentList, methodSymbol) +{ + protected override SyntaxNode Expression(ArgumentSyntax argument) => + argument.GetExpression(); + + protected override SyntaxToken? GetNameColonIdentifier(ArgumentSyntax argument) => + null; + + protected override SyntaxToken? GetNameEqualsIdentifier(ArgumentSyntax argument) => + argument is SimpleArgumentSyntax { NameColonEquals.Name.Identifier: var identifier } + ? identifier + : null; +} diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicMethodParameterLookup.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicMethodParameterLookup.cs index 47be470b2fe..0314d51959a 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicMethodParameterLookup.cs +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicMethodParameterLookup.cs @@ -34,9 +34,12 @@ public VisualBasicMethodParameterLookup(ArgumentListSyntax argumentList, Semanti public VisualBasicMethodParameterLookup(ArgumentListSyntax argumentList, IMethodSymbol methodSymbol) : base(argumentList.Arguments, methodSymbol) { } - protected override SyntaxToken? GetNameColonArgumentIdentifier(ArgumentSyntax argument) => + protected override SyntaxToken? GetNameColonIdentifier(ArgumentSyntax argument) => (argument as SimpleArgumentSyntax)?.NameColonEquals?.Name.Identifier; + protected override SyntaxToken? GetNameEqualsIdentifier(ArgumentSyntax argument) => + null; + protected override SyntaxNode Expression(ArgumentSyntax argument) => argument.GetExpression(); } diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/SonarAnalyzer.VisualBasic.csproj b/analyzers/src/SonarAnalyzer.VisualBasic/SonarAnalyzer.VisualBasic.csproj index 07485cb2e53..5d354390340 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/SonarAnalyzer.VisualBasic.csproj +++ b/analyzers/src/SonarAnalyzer.VisualBasic/SonarAnalyzer.VisualBasic.csproj @@ -31,6 +31,8 @@ + + diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Trackers/VisualBasicArgumentTracker.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Trackers/VisualBasicArgumentTracker.cs new file mode 100644 index 00000000000..527643197af --- /dev/null +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Trackers/VisualBasicArgumentTracker.cs @@ -0,0 +1,54 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2024 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. + */ + +namespace SonarAnalyzer.Helpers.Trackers; + +public class VisualBasicArgumentTracker : ArgumentTracker +{ + protected override SyntaxKind[] TrackedSyntaxKinds => [SyntaxKind.SimpleArgument]; + + protected override ILanguageFacade Language => VisualBasicFacade.Instance; + + protected override IReadOnlyCollection ArgumentList(SyntaxNode argumentNode) => + argumentNode is ArgumentSyntax { Parent: ArgumentListSyntax { Arguments: { } list } } + ? list + : null; + + protected override int? Position(SyntaxNode argumentNode) => + argumentNode is ArgumentSyntax { IsNamed: true } + ? null + : ArgumentList(argumentNode)?.IndexOf(x => x == argumentNode); + + protected override RefKind? ArgumentRefKind(SyntaxNode argumentNode) => + null; + + protected override bool InvocationMatchesMemberKind(SyntaxNode invokedExpression, MemberKind memberKind) => + memberKind switch + { + MemberKind.Method => invokedExpression is InvocationExpressionSyntax, + MemberKind.Constructor => invokedExpression is ObjectCreationExpressionSyntax, + MemberKind.Indexer => invokedExpression is InvocationExpressionSyntax, + MemberKind.Attribute => invokedExpression is AttributeSyntax, + _ => false, + }; + + protected override bool InvokedMemberMatches(SemanticModel model, SyntaxNode invokedExpression, MemberKind memberKind, Func invokedMemberNameConstraint) => + invokedMemberNameConstraint(invokedExpression.GetName()); +} diff --git a/analyzers/tests/SonarAnalyzer.Test/Extensions/SyntaxNodeExtensionsTest.cs b/analyzers/tests/SonarAnalyzer.Test/Extensions/SyntaxNodeExtensionsTest.cs index d549eadcbd2..adbe3ce0a7d 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Extensions/SyntaxNodeExtensionsTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Extensions/SyntaxNodeExtensionsTest.cs @@ -32,6 +32,7 @@ using StyleCop.Analyzers.Lightup; using ExtensionsCommon = common::SonarAnalyzer.Extensions.SyntaxNodeExtensions; using ExtensionsShared = csharp::SonarAnalyzer.Extensions.SyntaxNodeExtensionsShared; +using MicrosoftExtensionsCS = csharp::Microsoft.CodeAnalysis.CSharp.Extensions.SyntaxNodeExtensions; using SyntaxCS = Microsoft.CodeAnalysis.CSharp.Syntax; using SyntaxTokenExtensions = csharp::SonarAnalyzer.Extensions.SyntaxTokenExtensions; using SyntaxVB = Microsoft.CodeAnalysis.VisualBasic.Syntax; @@ -691,7 +692,7 @@ public X M() } """; var node = NodeBetweenMarkers(code, AnalyzerLanguage.CSharp); - var parentConditional = SyntaxNodeExtensionsCSharp.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, AnalyzerLanguage.CSharp); - var parentConditional = SyntaxNodeExtensionsCSharp.GetRootConditionalAccessExpression(node); + var parentConditional = MicrosoftExtensionsCS.GetRootConditionalAccessExpression(node); parentConditional.ToString().Should().Be(expression.Replace("$$", string.Empty)); } diff --git a/analyzers/tests/SonarAnalyzer.Test/Facade/VisualBasicFacadeTest.cs b/analyzers/tests/SonarAnalyzer.Test/Facade/VisualBasicFacadeTest.cs index 7f765890eec..555296b2908 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Facade/VisualBasicFacadeTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Facade/VisualBasicFacadeTest.cs @@ -97,8 +97,8 @@ End Class var root = tree.GetRoot(); var argumentList = root.DescendantNodes().OfType().First(); var method = model.GetDeclaredSymbol(root.DescendantNodes().OfType().First()); - var actual = () => sut.MethodParameterLookup(argumentList, method); - actual.Should().Throw(); + var actual = sut.MethodParameterLookup(argumentList, method); + actual.Should().NotBeNull().And.BeOfType(); } [TestMethod] diff --git a/analyzers/tests/SonarAnalyzer.Test/Trackers/ArgumentTrackerTest.cs b/analyzers/tests/SonarAnalyzer.Test/Trackers/ArgumentTrackerTest.cs new file mode 100644 index 00000000000..2f1799fa4ec --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.Test/Trackers/ArgumentTrackerTest.cs @@ -0,0 +1,1454 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2024 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.ComponentModel; +using Microsoft.CodeAnalysis.Text; +using SonarAnalyzer.Extensions; +using SonarAnalyzer.Helpers.Trackers; +using CS = Microsoft.CodeAnalysis.CSharp.Syntax; +using VB = Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace SonarAnalyzer.UnitTest.Trackers; + +[TestClass] +public class ArgumentTrackerTest +{ + [TestMethod] + public void Method_SimpleArgument() + { + var snippet = """ + System.IFormatProvider provider = null; + 1.ToString($$provider); + """; + var context = ArgumentContextCS(WrapInMethodCS(snippet)); + + var descriptor = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "ToString", "provider", 0); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeTrue(); + context.Parameter.Name.Should().Be("provider"); + context.Parameter.Type.Name.Should().Be("IFormatProvider"); + } + + [TestMethod] + public void Method_SimpleArgument_VB() + { + var snippet = """ + Dim provider As System.IFormatProvider = Nothing + Dim i As Integer + i.ToString($$provider) + """; + var context = ArgumentContextVB(WrapInMethodVB(snippet)); + + var descriptor = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "ToString", "provider", 0); + var result = MatchArgumentVB(context, descriptor); + result.Should().BeTrue(); + context.Parameter.Name.Should().Be("provider"); + context.Parameter.Type.Name.Should().Be("IFormatProvider"); + } + + [DataTestMethod] + [DataRow("""M( $$ , , 1)""", "i", true)] + [DataRow("""M( $$ , , 1)""", "j", false)] + [DataRow("""M( , $$ , 1)""", "j", true)] + [DataRow("""M( , , $$1)""", "k", true)] + [DataRow("""M( , , $$1)""", "i", false)] + public void Method_OmittedArgument_VB(string invocation, string parameterName, bool expected) + { + var snippet = $$""" + Public Class C + Public Sub M(Optional i As Integer = 0, Optional j As Integer = 0, Optional k As Integer = 0) + {{invocation}} + End Sub + End Class + """; + var context = ArgumentContextVB(snippet); + + var descriptor = ArgumentDescriptor.MethodInvocation(x => x.Name == "M", (s, c) => s.Equals("M", c), x => x.Name == parameterName, _ => true, null); + var result = MatchArgumentVB(context, descriptor); + result.Should().Be(expected); + } + + [DataTestMethod] + [DataRow("""1.ToString($$provider);""", 0, true)] + [DataRow("""1.ToString($$provider);""", 1, false)] + [DataRow("""1.ToString("", $$provider);""", 1, true)] + [DataRow("""1.ToString("", $$provider);""", 0, false)] + [DataRow("""1.ToString("", $$provider: provider);""", 1, true)] + [DataRow("""1.ToString("", $$provider: provider);""", 0, true)] + [DataRow("""1.ToString($$provider: provider, format: "");""", 1, true)] + [DataRow("""1.ToString($$provider: provider, format: "");""", 0, true)] + public void Method_Position(string invocation, int position, bool expected) + { + var snippet = $$""" + System.IFormatProvider provider = null; + {{invocation}} + """; + var context = ArgumentContextCS(WrapInMethodCS(snippet)); + + var descriptor = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "ToString", "provider", position); + var result = MatchArgumentCS(context, descriptor); + result.Should().Be(expected); + } + + [DataTestMethod] + [DataRow("""i.ToString($$provider)""", 0, true)] + [DataRow("""i.ToString($$provider)""", 1, false)] + [DataRow("""i.ToString("", $$provider)""", 1, true)] + [DataRow("""i.ToString("", $$provider)""", 0, false)] + [DataRow("""i.ToString("", $$provider:= provider)""", 1, true)] + [DataRow("""i.ToString("", $$provider:= provider)""", 0, true)] + [DataRow("""i.ToString($$provider:= provider, format:= "")""", 1, true)] + [DataRow("""i.ToString($$provider:= provider, format:= "")""", 0, true)] + public void Method_Position_VB(string invocation, int position, bool expected) + { + var snippet = $$""" + Dim provider As System.IFormatProvider = Nothing + Dim i As Integer + {{invocation}} + """; + var context = ArgumentContextVB(WrapInMethodVB(snippet)); + + var descriptor = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "ToString", "provider", position); + var result = MatchArgumentVB(context, descriptor); + result.Should().Be(expected); + } + + [DataTestMethod] + [DataRow("""int.TryParse("", $$out var result);""")] + [DataRow("""int.TryParse("", System.Globalization.NumberStyles.HexNumber, null, $$out var result);""")] + public void Method_RefOut_True(string invocation) + { + var snippet = $$""" + System.IFormatProvider provider = null; + {{invocation}} + """; + var context = ArgumentContextCS(WrapInMethodCS(snippet)); + + var descriptor = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "TryParse", "result", _ => true, RefKind.Out); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeTrue(); + context.Parameter.RefKind.Should().Be(RefKind.Out); + } + + [DataTestMethod] + [DataRow("""Integer.TryParse("", $$result)""")] + [DataRow("""Integer.TryParse("", System.Globalization.NumberStyles.HexNumber, Nothing, $$result)""")] + public void Method_RefOut_True_VB(string invocation) + { + var snippet = $$""" + Dim provider As System.IFormatProvider = Nothing + Dim result As Integer + {{invocation}} + """; + var context = ArgumentContextVB(WrapInMethodVB(snippet)); + + var descriptor = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "TryParse", "result", _ => true, RefKind.Out); + var result = MatchArgumentVB(context, descriptor); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""int.TryParse("", $$out var result);""", RefKind.Ref)] + [DataRow("""int.TryParse("", $$out var result);""", RefKind.In)] + [DataRow("""int.TryParse("", $$out var result);""", RefKind.RefReadOnlyParameter)] + [DataRow("""int.TryParse("", System.Globalization.NumberStyles.HexNumber, null, $$out var result);""", RefKind.Ref)] + [DataRow("""int.TryParse("", System.Globalization.NumberStyles.HexNumber, null, $$out var result);""", RefKind.In)] + [DataRow("""int.TryParse("", System.Globalization.NumberStyles.HexNumber, null, $$out var result);""", RefKind.RefReadOnlyParameter)] + [DataRow("""int.TryParse($$"", out var result);""", RefKind.Out)] + [DataRow("""int.TryParse("", $$System.Globalization.NumberStyles.HexNumber, null, out var result);""", RefKind.Out)] + public void Method_RefOut_False(string invocation, RefKind refKind) + { + var snippet = $$""" + System.IFormatProvider provider = null; + {{invocation}} + """; + var context = ArgumentContextCS(WrapInMethodCS(snippet)); + + var descriptor = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "TryParse", "result", _ => true, refKind); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeFalse(); + context.Parameter.Should().BeNull(); + } + + [DataTestMethod] + [DataRow("""int.TryParse("", $$out var result);""")] + [DataRow("""int.TryParse("", System.Globalization.NumberStyles.HexNumber, null, $$out var result);""")] + public void Method_RefOut_Unspecified(string invocation) + { + var snippet = $$""" + System.IFormatProvider provider = null; + {{invocation}} + """; + var context = ArgumentContextCS(WrapInMethodCS(snippet)); + var descriptor = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "TryParse", "result", _ => true); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeTrue(); + context.Parameter.RefKind.Should().Be(RefKind.Out); + } + + [DataTestMethod] + [DataRow("in s")] + [DataRow("s")] // "in" is optional on the call side + [DataRow("ref s")] // Valid, but produces warning CS9191: The 'ref' modifier for argument 1 corresponding to 'in' parameter is equivalent to 'in'. Consider using 'in' instead. + public void Method_RefIn(string argument) + { + var snippet = $$""" + public readonly struct S { public readonly int I; } + + public class C + { + public void M(in S s) { } + + public void Test() + { + var s = new S(); + M($${{argument}}); + } + } + """; + var context = ArgumentContextCS(snippet); + var descriptor = ArgumentDescriptor.MethodInvocation(_ => true, (x, c) => string.Equals(x, "M", c), _ => true, _ => true, RefKind.In); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeTrue(); + context.Parameter.RefKind.Should().Be(RefKind.In); + } + + [DataTestMethod] + [DataRow("in s")] + [DataRow("s")] // Valid, produces warning CS9192: Argument 1 should be passed with 'ref' or 'in' keyword + [DataRow("ref s")] + public void Method_RefReadOnly(string argument) + { + var snippet = $$""" + public readonly struct S { public readonly int I; } + + public class C + { + public void M(ref readonly S s) { } + + public void Test() + { + var s = new S(); + M($${{argument}}); + } + } + """; + var context = ArgumentContextCS(snippet); + + var descriptor = ArgumentDescriptor.MethodInvocation(_ => true, (x, c) => string.Equals(x, "M", c), _ => true, _ => true, RefKind.RefReadOnlyParameter); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeTrue(); + context.Parameter.RefKind.Should().Be(RefKind.RefReadOnlyParameter); + } + + [DataTestMethod] + [DataRow("""new Direct().M($$1);""", true)] + [DataRow("""new DirectDifferentParameterName().M($$1);""", false)] // FN. This would require ExplicitOrImplicitInterfaceImplementations from the internal ISymbolExtensions in Roslyn. + [DataRow("""(new Explicit() as I).M($$1);""", true)] + [DataRow("""(new ExplicitDifferentParameterName() as I).M($$1);""", true)] + public void Method_Inheritance_Interface(string invocation, bool expected) + { + var snippet = $$""" + interface I + { + void M(int parameter); + } + public class Direct: I + { + public void M(int parameter) { } + } + public class DirectDifferentParameterName: I + { + public void M(int renamed) { } + } + public class Explicit: I + { + void I.M(int parameter) { } + } + public class ExplicitDifferentParameterName: I + { + void I.M(int renamed) { } + } + public class Test + { + void M() + { + {{invocation}} + } + } + """; + var context = ArgumentContextCS(snippet); + + var descriptor = ArgumentDescriptor.MethodInvocation(_ => true, (m, c) => m.Equals("M", c), x => x.Name == "parameter", _ => true, null); + var result = MatchArgumentCS(context, descriptor); + result.Should().Be(expected); + } + + [DataTestMethod] + [DataRow("""Dim a = New Direct().M($$1)""", true)] + [DataRow("""Dim a = New DirectDifferentParameterName().M($$1)""", false)] // FN. This would require ExplicitOrImplicitInterfaceImplementations from the internal ISymbolExtensions in Roslyn. + [DataRow(""" + Dim i As I = New Explicit() + i.M($$1) + """, true)] + [DataRow(""" + Dim i As I = New ExplicitDifferentParameterName() + i.M($$1) + """, true)] + public void Method_Inheritance_Interface_VB(string invocation, bool expected) + { + var snippet = $$""" + Interface I + Function M(ByVal parameter As Integer) As Boolean + End Interface + + Public Class Direct + Implements I + + Public Function M(parameter As Integer) As Boolean Implements I.M + End Function + End Class + + Public Class DirectDifferentParameterName + Implements I + + Public Function M(ByVal renamed As Integer) As Boolean Implements I.M + End Function + End Class + + Public Class Explicit + Implements I + + Private Function M(ByVal parameter As Integer) As Boolean Implements I.M + End Function + End Class + + Public Class ExplicitDifferentParameterName + Implements I + + Private Function M(ByVal renamed As Integer) As Boolean Implements I.M + End Function + End Class + + Public Class Test + Private Sub M() + {{invocation}} + End Sub + End Class + """; + var context = ArgumentContextVB(snippet); + + var descriptor = ArgumentDescriptor.MethodInvocation(_ => true, (m, c) => m.Equals("M", c), x => x.Name == "parameter", _ => true, null); + var result = MatchArgumentVB(context, descriptor); + result.Should().Be(expected); + } + + [DataTestMethod] + [DataRow("""comparer.Compare($$default, default);""")] + [DataRow("""new MyComparer().Compare($$1, 2);""")] + public void Method_Inheritance_BaseClasses_Generics(string invocation) + { + var snippet = $$""" + using System.Collections.Generic; + public class MyComparer : Comparer + { + public MyComparer() { } + public override int Compare(T a, T b) => 1; // The original definition uses x and y: int Compare(T? x, T? y) + } + public class Test + { + void M(MyComparer comparer) + { + {{invocation}} + } + } + """; + var context = ArgumentContextCS(snippet); + + var descriptor = ArgumentDescriptor.MethodInvocation(KnownType.System_Collections_Generic_Comparer_T, "Compare", "x", 0); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""comparer.Compare($$Nothing, Nothing)""", "x", true)] + [DataRow("""comparer.Compare($$Nothing, Nothing)""", "a", false)] + [DataRow("""Call New MyComparer(Of Integer)().Compare($$1, 2)""", "x", true)] + [DataRow("""Call New MyComparer(Of Integer)().Compare($$1, 2)""", "a", false)] + public void Method_Inheritance_BaseClasses_Generics_VB(string invocation, string parameterName, bool expected) + { + var snippet = $$""" + Imports System.Collections.Generic + + Public Class MyComparer(Of T) + Inherits Comparer(Of T) + + Public Overrides Function Compare(ByVal a As T, ByVal b As T) As Integer ' The original definition uses x and y + Return 1 + End Function + End Class + + Public Class Test + Private Sub M(Of T)(ByVal comparer As MyComparer(Of T)) + {{invocation}} + End Sub + End Class + """; + var context = ArgumentContextVB(snippet); + + var descriptor = ArgumentDescriptor.MethodInvocation(KnownType.System_Collections_Generic_Comparer_T, "Compare", parameterName, 0); + var result = MatchArgumentVB(context, descriptor); + result.Should().Be(expected); + } + + [DataTestMethod] + [DataRow("""OnInsert($$1, null);""")] + [DataRow("""OnInsert(position: $$1, null);""")] + public void Method_Inheritance_BaseClasses_Overrides(string invocation) + { + var snippet = $$""" + using System.Collections; + public class Collection : CollectionBase + { + protected override void OnInsert(int position, object value) { } + + void M(T arg) + { + {{invocation}} + } + } + """; + var context = ArgumentContextCS(snippet); + + var descriptor = ArgumentDescriptor.MethodInvocation(KnownType.System_Collections_CollectionBase, "OnInsert", "index", 0); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""OnInsert($$1, Nothing)""")] + [DataRow("""OnInsert(position:= $$1, Nothing)""")] + public void Method_Inheritance_BaseClasses_Overrides_VB(string invocation) + { + var snippet = $$""" + Imports System.Collections + + Public Class Collection(Of T) + Inherits CollectionBase + + Protected Overrides Sub OnInsert(ByVal position As Integer, ByVal value As Object) + End Sub + + Private Sub M(ByVal arg As T) + {{invocation}} + End Sub + End Class + """; + var context = ArgumentContextVB(snippet); + + var descriptor = ArgumentDescriptor.MethodInvocation(KnownType.System_Collections_CollectionBase, "OnInsert", "index", 0); + var result = MatchArgumentVB(context, descriptor); + result.Should().BeTrue(); + } + + [DataTestMethod] + // learn.microsoft.com/en-us/dotnet/api/system.string.format + [DataRow("""string.Format("format", $$0)""", "arg0")] + [DataRow("""string.Format("format", 0, $$1)""", "arg1")] + [DataRow("""string.Format("format", 0, 1, $$2)""", "arg2")] + [DataRow("""string.Format("format", 0, 1, 2, $$3)""", "args")] + [DataRow("""string.Format("format", 0, 1, $$2, 3)""", "args")] + [DataRow("""string.Format("format", 0, $$1, 2, 3)""", "args")] + [DataRow("""string.Format("format", $$0, 1, 2, 3)""", "args")] + [DataRow("""string.Format("format", arg2: 2, arg1: 1, $$arg0:0)""", "arg0")] + [DataRow("""string.Format("format", $$new object[0])""", "args")] + public void Method_ParamsArray(string invocation, string parameterName) + { + var snippet = $$""" + _ = {{invocation}}; + """; + var context = ArgumentContextCS(WrapInMethodCS(snippet)); + + var descriptor = ArgumentDescriptor.MethodInvocation(KnownType.System_String, "Format", parameterName, x => x >= 1); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeTrue(); + } + + [DataTestMethod] + // learn.microsoft.com/en-us/dotnet/api/system.string.format + [DataRow("""String.Format("format", $$0)""", "arg0")] + [DataRow("""String.Format("format", 0, $$1)""", "arg1")] + [DataRow("""String.Format("format", 0, 1, $$2)""", "arg2")] + [DataRow("""String.Format("format", 0, 1, 2, $$3)""", "args")] + [DataRow("""String.Format("format", arg2:=2, arg1:=1, $$arg0:=0)""", "arg0")] + [DataRow("""String.Format("format", $$New Object(){ })""", "args")] + public void Method_ParamsArray_VB(string invocation, string parameterName) + { + var snippet = $$""" + Dim a = {{invocation}} + """; + var context = ArgumentContextVB(WrapInMethodVB(snippet)); + + var descriptor = ArgumentDescriptor.MethodInvocation(KnownType.System_String, "Format", parameterName, x => x >= 1); + var result = MatchArgumentVB(context, descriptor); + result.Should().BeTrue(); + } + + [TestMethod] + public void Method_NamelessMethod() + { + var snippet = """ + using System; + class C + { + Action ActionReturning() => null; + + void M() + { + ActionReturning()($$1); + } + } + """; + var context = ArgumentContextCS(snippet); + + var descriptor = ArgumentDescriptor.MethodInvocation(KnownType.System_Action_T, methodName: string.Empty, "obj", 0); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeTrue(); + context.Parameter.Name.Should().Be("obj"); + context.Parameter.ContainingSymbol.Name.Should().Be("Invoke"); + context.Parameter.ContainingType.Name.Should().Be("Action"); + } + + [TestMethod] + public void Method_InvocationOnProperty() + { + var snippet = """ + using System.Collections.Generic; + class C + { + public IList List { get; } = new List(); + void M() + { + List.Add($$1); // Add is defined on ICollection while the List property is of type IList, invokedMemberNodeConstraint can figure this out + } + } + """; + var context = ArgumentContextCS(snippet); + + var descriptor = ArgumentDescriptor.MethodInvocation( + invokedMemberConstraint: x => x.Is(KnownType.System_Collections_Generic_ICollection_T, "Add"), + invokedMemberNameConstraint: (x, c) => string.Equals(x, "Add", c), + invokedMemberNodeConstraint: (model, language, node) => + node is CS.InvocationExpressionSyntax { Expression: CS.MemberAccessExpressionSyntax { Expression: CS.IdentifierNameSyntax { Identifier.ValueText: { } leftName } left } } + && language.NameComparer.Equals(leftName, "List") + && model.GetSymbolInfo(left).Symbol is IPropertySymbol property + && property.Type.Is(KnownType.System_Collections_Generic_IList_T), + parameterConstraint: _ => true, + argumentListConstraint: (_, _) => true, + refKind: null); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeTrue(); + context.Parameter.Name.Should().Be("item"); + context.Parameter.ContainingSymbol.Name.Should().Be("Add"); + context.Parameter.ContainingType.Name.Should().Be("ICollection"); + } + + [DataTestMethod] + [DataRow("""ProcessStartInfo($$"fileName")""", "fileName", 0, true)] + [DataRow("""ProcessStartInfo($$"fileName")""", "arguments", 1, false)] + [DataRow("""ProcessStartInfo("fileName", $$"arguments")""", "arguments", 1, true)] + [DataRow("""ProcessStartInfo("fileName", $$"arguments")""", "arguments", 0, false)] + [DataRow("""ProcessStartInfo($$"fileName", "arguments")""", "arguments", 1, false)] + [DataRow("""ProcessStartInfo(arguments: $$"arguments", fileName: "fileName")""", "arguments", 1, true)] + [DataRow("""ProcessStartInfo(arguments: $$"arguments", fileName: "fileName")""", "fileName", 0, false)] + [DataRow("""ProcessStartInfo(arguments: "arguments", $$fileName: "fileName")""", "fileName", 0, true)] + [DataRow("""ProcessStartInfo(arguments: "arguments", $$fileName: "fileName")""", "arguments", 1, false)] + public void Constructor_SimpleArgument(string constructor, string parameterName, int descriptorPosition, bool expected) + { + var snippet = $$""" + _ = new System.Diagnostics.{{constructor}}; + """; + var context = ArgumentContextCS(WrapInMethodCS(snippet)); + + var descriptor = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Diagnostics_ProcessStartInfo, parameterName, descriptorPosition); + var result = MatchArgumentCS(context, descriptor); + result.Should().Be(expected); + if (expected) + { + context.Parameter.Name.Should().Be(parameterName); + context.Parameter.ContainingSymbol.Name.Should().Be(".ctor"); + context.Parameter.ContainingType.Name.Should().Be("ProcessStartInfo"); + } + } + + [DataTestMethod] + [DataRow("""ProcessStartInfo($$"fileName")""", "fileName", 0, true)] + [DataRow("""ProcessStartInfo($$"fileName")""", "arguments", 1, false)] + [DataRow("""ProcessStartInfo("fileName", $$"arguments")""", "arguments", 1, true)] + [DataRow("""ProcessStartInfo("fileName", $$"arguments")""", "arguments", 0, false)] + [DataRow("""ProcessStartInfo($$"fileName", "arguments")""", "arguments", 1, false)] + [DataRow("""ProcessStartInfo(arguments:= $$"arguments", fileName:= "fileName")""", "arguments", 1, true)] + [DataRow("""ProcessStartInfo(arguments:= $$"arguments", fileName:= "fileName")""", "fileName", 0, false)] + [DataRow("""ProcessStartInfo(arguments:= "arguments", $$fileName:= "fileName")""", "fileName", 0, true)] + [DataRow("""ProcessStartInfo(arguments:= "arguments", $$fileName:= "fileName")""", "arguments" + + "", 1, false)] + public void Constructor_SimpleArgument_VB(string constructor, string parameterName, int descriptorPosition, bool expected) + { + var snippet = $$""" + Dim a = New System.Diagnostics.{{constructor}} + """; + var context = ArgumentContextVB(WrapInMethodVB(snippet)); + + var descriptor = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Diagnostics_ProcessStartInfo, parameterName, descriptorPosition); + var result = MatchArgumentVB(context, descriptor); + result.Should().Be(expected); + } + + [DataTestMethod] + [DataRow("""($$"fileName")""", "fileName", 0, true)] + [DataRow("""($$"fileName")""", "arguments", 1, false)] + [DataRow("""("fileName", $$"arguments")""", "arguments", 1, true)] + [DataRow("""("fileName", $$"arguments")""", "arguments", 0, false)] + [DataRow("""($$"fileName", "arguments")""", "arguments", 1, false)] + [DataRow("""(arguments: $$"arguments", fileName: "fileName")""", "arguments", 1, true)] + [DataRow("""(arguments: $$"arguments", fileName: "fileName")""", "fileName", 0, false)] + [DataRow("""(arguments: "arguments", $$fileName: "fileName")""", "fileName", 0, true)] + [DataRow("""(arguments: "arguments", $$fileName: "fileName")""", "arguments", 1, false)] + public void Constructor_TargetTyped(string constructor, string parameterName, int descriptorPosition, bool expected) + { + var snippet = $$""" + System.Diagnostics.ProcessStartInfo psi = new{{constructor}}; + """; + var context = ArgumentContextCS(WrapInMethodCS(snippet)); + + var descriptor = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Diagnostics_ProcessStartInfo, parameterName, descriptorPosition); + var result = MatchArgumentCS(context, descriptor); + result.Should().Be(expected); + } + + [DataTestMethod] + [DataRow("""new Dictionary($$1)""", "capacity", 0, true)] + [DataRow("""new Dictionary($$1)""", "capacity", 0, true)] + [DataRow("""new Dictionary($$1)""", "capacity", 0, true)] + [DataRow("""new Dictionary($$1)""", "comparer", 0, false)] + [DataRow("""new Dictionary($$1, EqualityComparer.Default)""", "capacity", 0, true)] + [DataRow("""new Dictionary(1, $$EqualityComparer.Default)""", "comparer", 1, true)] + public void Constructor_Generic(string constructor, string parameterName, int descriptorPosition, bool expected) + { + var snippet = $$""" + using System.Collections.Generic; + class C + { + public void M() where TKey : notnull + { + _ = {{constructor}}; + } + } + """; + var context = ArgumentContextCS(snippet); + + var descriptor = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Collections_Generic_Dictionary_TKey_TValue, parameterName, descriptorPosition); + var result = MatchArgumentCS(context, descriptor); + result.Should().Be(expected); + } + + [DataTestMethod] + [DataRow("""Dim a = new Dictionary(Of TKey, TValue)($$1)""", "capacity", 0, true)] + [DataRow("""Dim a = new Dictionary(Of Integer, TValue)($$1)""", "capacity", 0, true)] + [DataRow("""Dim a = new Dictionary(Of Integer, String)($$1)""", "capacity", 0, true)] + [DataRow("""Dim a = new Dictionary(Of TKey, TValue)($$1)""", "comparer", 0, false)] + [DataRow("""Dim a = New Dictionary(Of TKey, TValue)($$1, EqualityComparer(Of TKey).Default)""", "capacity", 0, true)] + [DataRow("""Dim a = new Dictionary(Of TKey, TValue)(1, $$EqualityComparer(Of TKey).Default)""", "comparer", 1, true)] + public void Constructor_Generic_VB(string constructor, string parameterName, int descriptorPosition, bool expected) + { + var snippet = $$""" + Imports System.Collections.Generic + + Class C + Public Sub M(Of TKey, TValue)() + {{constructor}} + End Sub + End Class + """; + var context = ArgumentContextVB(snippet); + + var descriptor = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Collections_Generic_Dictionary_TKey_TValue, parameterName, descriptorPosition); + var result = MatchArgumentVB(context, descriptor); + result.Should().Be(expected); + } + + [TestMethod] + public void Constructor_BaseCall() + { + var snippet = $$""" + using System.Collections.Generic; + class MyList: List + { + public MyList(int capacity) : base(capacity) // Forwarded constructor parameter are unsupported + { + } + } + public class Test + { + public void M() + { + _ = new MyList($$1); // Requires tracking of the parameter to the base constructor + } + } + """; + var context = ArgumentContextCS(snippet); + + var descriptor = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Collections_Generic_List_T, "capacity", 0); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeFalse(); + } + + [TestMethod] + public void Constructor_BaseCall_VB() + { + var snippet = $$""" + Imports System.Collections.Generic + + Class MyList + Inherits List(Of Integer) + + Public Sub New(ByVal capacity As Integer) + MyBase.New(capacity) ' Passing of the parameter to the base constructor is not followed + End Sub + End Class + + Public Class Test + Public Sub M() + Dim a = New MyList($$1) ' Requires tracking of the parameter to the base constructor + End Sub + End Class + """; + var context = ArgumentContextVB(snippet); + + var descriptor = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Collections_Generic_List_T, "capacity", 0); + var result = MatchArgumentVB(context, descriptor); + result.Should().BeFalse(); + } + + [DataTestMethod] + [DataRow("""new NumberList($$1)""", "capacity", 0, false)] // FN. Syntactic checks bail out before the semantic model can resolve the alias + [DataRow("""new($$1)""", "capacity", 0, true)] // Target typed new resolves the alias + public void Constructor_TypeAlias(string constructor, string parameterName, int descriptorPosition, bool expected) + { + var snippet = $$""" + using NumberList = System.Collections.Generic.List; + class C + { + public void M() + { + NumberList nl = {{constructor}}; + } + } + """; + var context = ArgumentContextCS(snippet); + + var descriptor = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Collections_Generic_List_T, parameterName, descriptorPosition); + var result = MatchArgumentCS(context, descriptor); + result.Should().Be(expected); + } + + [TestMethod] + public void Constructor_TypeAlias_VB() + { + var snippet = $$""" + Imports NumberList = System.Collections.Generic.List(Of Integer) + + Class C + Public Sub M() + Dim nl As NumberList = New NumberList($$1) + End Sub + End Class + """; + var context = ArgumentContextVB(snippet); + + var descriptor = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Collections_Generic_List_T, "capacity", 0); + var result = MatchArgumentVB(context, descriptor); + result.Should().BeFalse("FN. Syntactic check does not respect aliases."); + } + + [DataTestMethod] + [DataRow("""new($$1, 2)""", true)] + [DataRow("""new C(1, $$2)""", true)] + [DataRow("""new CAlias(1, $$2)""", true)] + [DataRow("""new C($$1)""", false)] // Count constraint fails + [DataRow("""new C(1, 2, $$3)""", false)] // Parameter name constraint fails + [DataRow("""new C($$k: 1, j:2, i:3)""", false)] // Parameter name constraint fails + public void Constructor_CustomLogic(string constructor, bool expected) + { + var snippet = $$""" + using CAlias = C; + class C + { + public C(int i) { } + public C(int j, int i) { } + public C(int j, int i, int k) { } + + public void M() + { + C c = {{constructor}}; + } + } + """; + var context = ArgumentContextCS(snippet); + + var descriptor = ArgumentDescriptor.ConstructorInvocation(invokedMethodSymbol: x => x is { MethodKind: MethodKind.Constructor, ContainingSymbol.Name: "C" }, + invokedMemberNameConstraint: (c, n) => c.Equals("C", n) || c.Equals("CAlias"), + invokedMemberNodeConstraint: (_, _, _) => true, + parameterConstraint: x => x.Name is "i" or "j", + argumentListConstraint: (n, i) => i is null or 0 or 1 && n.Count > 1, + refKind: null); + var result = MatchArgumentCS(context, descriptor); + result.Should().Be(expected); + } + + [TestMethod] + public void Constructor_InitializerCalls_This() + { + var snippet = $$""" + class Base + { + public Base(int i) : this($$i, 1) { } + public Base(int i, int j) { } + } + """; + var context = ArgumentContextCS(snippet); + + var descriptor = ArgumentDescriptor.ConstructorInvocation(invokedMethodSymbol: x => x is { MethodKind: MethodKind.Constructor, ContainingSymbol.Name: "Base" }, + invokedMemberNameConstraint: (c, n) => c.Equals("Base", n), + invokedMemberNodeConstraint: (_, _, _) => true, + parameterConstraint: x => x.Name is "i", + argumentListConstraint: (_, _) => true, + refKind: null); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeTrue(); + } + + [TestMethod] + public void Constructor_InitializerCalls_Base() + { + var snippet = $$""" + class Base + { + public Base(int i) { } + } + class Derived: Base + { + public Derived() : base($$1) { } + } + """; + var context = ArgumentContextCS(snippet); + + var descriptor = ArgumentDescriptor.ConstructorInvocation(invokedMethodSymbol: x => x is { MethodKind: MethodKind.Constructor, ContainingSymbol.Name: "Base" }, + invokedMemberNameConstraint: (c, n) => c.Equals("Base", n), + invokedMemberNodeConstraint: (_, _, _) => true, + parameterConstraint: x => x.Name is "i", + argumentListConstraint: (_, _) => true, + refKind: null); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeTrue(); + } + + [TestMethod] + public void Constructor_InitializerCalls_Base_MyException() + { + var snippet = """ + using System; + + class MyException: Exception + { + public MyException(string message) : base($$message) + { } + } + """; + var context = ArgumentContextCS(snippet); + + var descriptor = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Exception, "message", 0); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeTrue(); + } + + [TestMethod] + public void Constructor_InitializerCalls_Base_MyException_VB() + { + var snippet = $$""" + Imports System + + Public Class MyException + Inherits Exception + + Public Sub New(ByVal message As String) + MyBase.New($$message) + End Sub + End Class + """; + var context = ArgumentContextVB(snippet); + + var descriptor = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Exception, "message", 0); + var result = MatchArgumentVB(context, descriptor); + result.Should().BeFalse("FN. MyBase.New and Me.New are not supported."); + } + + [TestMethod] + public void Indexer_List_Get() + { + var snippet = $$""" + var list = new System.Collections.Generic.List(); + _ = list[$$1]; + """; + var context = ArgumentContextCS(WrapInMethodCS(snippet)); + + var descriptor = ArgumentDescriptor.ElementAccess(KnownType.System_Collections_Generic_List_T, "list", + x => x is { Name: "index", Type.SpecialType: SpecialType.System_Int32, ContainingSymbol: IMethodSymbol { MethodKind: MethodKind.PropertyGet } }, 0); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeTrue(); + context.Parameter.Name.Should().Be("index"); + var associatedSymbol = context.Parameter.ContainingSymbol.Should().BeAssignableTo().Which.AssociatedSymbol.Should().BeAssignableTo().Which; + associatedSymbol.IsIndexer.Should().BeTrue(); + associatedSymbol.Name.Should().Be("this[]"); + } + + [TestMethod] + public void Indexer_List_Get_VB() + { + var snippet = $$""" + Dim list = New System.Collections.Generic.List(Of Integer)() + Dim a = list($$1) + """; + var context = ArgumentContextVB(WrapInMethodVB(snippet)); + + var descriptor = ArgumentDescriptor.ElementAccess(KnownType.System_Collections_Generic_List_T, "list", + x => x is { Name: "index", Type.SpecialType: SpecialType.System_Int32, ContainingSymbol: IMethodSymbol { MethodKind: MethodKind.PropertyGet } }, 0); + var result = MatchArgumentVB(context, descriptor); + result.Should().BeTrue(); + context.Parameter.Name.Should().Be("index"); + var associatedSymbol = context.Parameter.ContainingSymbol.Should().BeAssignableTo().Which.AssociatedSymbol.Should().BeAssignableTo().Which; + associatedSymbol.IsIndexer.Should().BeTrue(); + associatedSymbol.Name.Should().Be("Item"); + } + + [DataTestMethod] + [DataRow("list[$$1] = 1;")] + [DataRow("(list[$$1], list[2]) = (1, 2);")] + [DataRow("list[$$1]++;")] + [DataRow("list[$$1]--;")] + public void Indexer_List_Set(string writeExpression) + { + var snippet = $$""" + var list = new System.Collections.Generic.List(); + {{writeExpression}}; + """; + var context = ArgumentContextCS(WrapInMethodCS(snippet)); + + var descriptor = ArgumentDescriptor.ElementAccess(KnownType.System_Collections_Generic_List_T, + x => x is { Name: "index", ContainingSymbol: IMethodSymbol { MethodKind: MethodKind.PropertySet } }, 0); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeTrue(); + context.Parameter.ContainingSymbol.Should().BeAssignableTo().Which.MethodKind.Should().Be(MethodKind.PropertySet); + } + + [DataTestMethod] + [DataRow("list($$1) = 1")] + [DataRow("list($$1) += 1")] + [DataRow("list($$1) -= 1")] + public void Indexer_List_Set_VB(string writeExpression) + { + var snippet = $$""" + Dim list = New System.Collections.Generic.List(Of Integer)() + {{writeExpression}} + """; + var context = ArgumentContextVB(WrapInMethodVB(snippet)); + + var descriptor = ArgumentDescriptor.ElementAccess(KnownType.System_Collections_Generic_List_T, + x => x is { Name: "index", ContainingSymbol: IMethodSymbol { MethodKind: MethodKind.PropertySet } }, 0); + var result = MatchArgumentVB(context, descriptor); + result.Should().BeTrue(); + context.Parameter.ContainingSymbol.Should().BeAssignableTo().Which.MethodKind.Should().Be(MethodKind.PropertySet); + } + + [DataTestMethod] + [DataRow("""Environment.GetEnvironmentVariables()[$$"TEMP"]""")] + [DataRow("""Environment.GetEnvironmentVariables()?[$$"TEMP"]""")] + public void Indexer_DictionaryGet(string environmentVariableAccess) + { + var snippet = $$""" + _ = {{environmentVariableAccess}}; + """; + var context = ArgumentContextCS(WrapInMethodCS(snippet)); + + var descriptor = ArgumentDescriptor.ElementAccess(x => x is { MethodKind: MethodKind.PropertyGet, ContainingType.Name: "IDictionary" }, + (n, c) => n.Equals("GetEnvironmentVariables", c), (_, _, _) => true, x => x.Name == "key", (_, p) => p is null or 0); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeTrue(); + } + + [TestMethod] + public void Indexer_DictionaryGet_VB() + { + var snippet = """ + Dim a = Environment.GetEnvironmentVariables()($$"TEMP") + """; + var context = ArgumentContextVB(WrapInMethodVB(snippet)); + + var descriptor = ArgumentDescriptor.ElementAccess(x => x is { MethodKind: MethodKind.PropertyGet, ContainingType.Name: "IDictionary" }, + (n, c) => n.Equals("GetEnvironmentVariables", c), (_, _, _) => true, x => x.Name == "key", (_, p) => p is null or 0); + var result = MatchArgumentVB(context, descriptor); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""_ = this[$$0,0];""", "x", true)] + [DataRow("""_ = this[0,$$0];""", "y", true)] + [DataRow("""_ = this[$$y: 0,x: 0];""", "y", true)] + [DataRow("""_ = this[y: 0,$$x: 0];""", "x", true)] + [DataRow("""this[$$0, 0] = 1;""", "x", false)] + [DataRow("""this[0, $$0] = 1;""", "y", false)] + [DataRow("""this[y: $$0, x: 0] = 1;""", "y", false)] + [DataRow("""this[y: 0, $$x: 0] = 1;""", "x", false)] + public void Indexer_MultiDimensional(string access, string parameterName, bool isGetter) + { + var snippet = $$""" + public class C { + public int this[int x, int y] + { + get => 1; + set { } + } + + public void M() { + {{access}} + } + } + """; + var context = ArgumentContextCS(snippet); + + var descriptor = ArgumentDescriptor.ElementAccess( + x => x is { MethodKind: var kind, ContainingType.Name: "C" } && (isGetter ? kind == MethodKind.PropertyGet : kind == MethodKind.PropertySet), + (_, _) => true, + (_, _, _) => true, + x => x.Name == parameterName, + (_, _) => true); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""Dim a = Me($$0, 0)""", "x", true)] + [DataRow("""Dim a = Me(0, $$0)""", "y", true)] + [DataRow("""Dim a = Me(y := $$0, x := 0)""", "y", true)] + [DataRow("""Dim a = Me(y := 0, $$x := 0)""", "x", true)] + [DataRow("""Me($$0, 0) = 1""", "x", false)] + [DataRow("""Me(0, $$0) = 1""", "y", false)] + [DataRow("""Me(y := $$0, x := 0) = 1""", "y", false)] + [DataRow("""Me(y := 0, $$x := 0) = 1""", "x", false)] + public void Indexer_MultiDimensional_VB(string access, string parameterName, bool isGetter) + { + var snippet = $$""" + Public Class C + Default Public Property Item(ByVal x As Integer, ByVal y As Integer) As Integer + Get + Return 1 + End Get + Set(ByVal value As Integer) + End Set + End Property + + Public Sub M() + {{access}} + End Sub + End Class + """; + var context = ArgumentContextVB(snippet); + + var descriptor = ArgumentDescriptor.ElementAccess( + x => x is { MethodKind: var kind, ContainingType.Name: "C" } && (isGetter ? kind == MethodKind.PropertyGet : kind == MethodKind.PropertySet), + (_, _) => true, + (_, _, _) => true, + x => x.Name == parameterName, + (_, _) => true); + var result = MatchArgumentVB(context, descriptor); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""process.Modules[$$0]""")] + [DataRow("""process?.Modules[$$0]""")] + [DataRow("""process.Modules?[$$0]""")] + [DataRow("""process?.Modules?[$$0]""")] + [DataRow("""process.Modules[index: $$0]""")] + [DataRow("""process?.Modules?[index: $$0]""")] + public void Indexer_ConditionalAccess_Collection(string modulesAccess) + { + var snippet = $$""" + public class Test + { + public void M(System.Diagnostics.Process process) + { + _ = {{modulesAccess}}; + } + } + """; + var context = ArgumentContextCS(snippet); + + var descriptor = ArgumentDescriptor.ElementAccess(x => x is { MethodKind: MethodKind.PropertyGet, ContainingType.Name: "ProcessModuleCollection" }, + (n, c) => n.Equals("Modules", c), + (_, _, _) => true, + x => x.Name == "index", + (_, p) => p is null or 0); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""processStartInfo.Environment[$$"TEMP"]""")] + [DataRow("""processStartInfo?.Environment[$$"TEMP"]""")] + [DataRow("""processStartInfo.Environment?[$$"TEMP"]""")] + [DataRow("""processStartInfo?.Environment?[$$"TEMP"]""")] + [DataRow("""processStartInfo.Environment[key: $$"TEMP"]""")] + [DataRow("""processStartInfo?.Environment?[key: $$"TEMP"]""")] + public void Indexer_ConditionalAccess_Dictionary(string environmentAccess) + { + var snippet = $$""" + public class Test + { + public void M(System.Diagnostics.ProcessStartInfo processStartInfo) + { + _ = {{environmentAccess}}; + } + } + """; + var context = ArgumentContextCS(snippet); + + var descriptor = ArgumentDescriptor.ElementAccess(KnownType.System_Collections_Generic_IDictionary_TKey_TValue, "Environment", x => x.Name == "key", 0); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""this[$$"TEMP"]""", true)] + [DataRow("""this[42, $$"TEMP"]""", false)] + [DataRow("""this[42, 43, $$"TEMP"]""", true)] + public void Indexer_ConditionalPosition(string expression, bool expectedResult) + { + var snippet = $$""" + public class Test + { + public int this[string key] => 0; + public int this[int a, string key] => 0; + public int this[int a, int b, string key] => 0; + + public void M() + { + _ = {{expression}}; + } + } + """; + var context = ArgumentContextCS(snippet); + var descriptor = ArgumentDescriptor.ElementAccess( + new KnownType("Test"), + x => x.Name == "key", + x => x is 0 or 2); + var result = MatchArgumentCS(context, descriptor); + result.Should().Be(expectedResult); + } + + [DataTestMethod] + [DataRow("System.Int32", false)] + [DataRow("System.Collections.Generic.IDictionary", true)] + public void Indexer_WrongKnownType(string type, bool expectedResult) + { + var snippet = $$""" + public class Test + { + public void M(System.Diagnostics.ProcessStartInfo psi) + { + _ = psi.Environment?[key: $$"TEMP"]; + } + } + """; + var context = ArgumentContextCS(snippet); + var descriptor = ArgumentDescriptor.ElementAccess( + new(type, "TKey", "TValue"), + "Environment", + x => x.Name == "key", + 0); + var result = MatchArgumentCS(context, descriptor); + result.Should().Be(expectedResult); + } + +#if NET + + [TestMethod] + public void Attribute_ConstructorParameter() + { + var snippet = $$""" + using System; + public class Test + { + [Obsolete($$"message", UrlFormat = "")] + public void M() + { + } + } + """; + var context = ArgumentContextCS(snippet); + + var descriptor = ArgumentDescriptor.AttributeArgument(x => x is { MethodKind: MethodKind.Constructor, ContainingType.Name: "ObsoleteAttribute" }, + (s, c) => s.StartsWith("Obsolete", c), + (_, _, _) => true, + x => x.Name == "message", + (_, i) => i is 0); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeTrue(); + } + + [TestMethod] + public void Attribute_ConstructorParameter_VB() + { + var snippet = $$""" + Imports System + + Public Class Test + + Public Sub M() + End Sub + End Class + """; + var context = ArgumentContextVB(snippet); + + var descriptor = ArgumentDescriptor.AttributeArgument(x => x is { MethodKind: MethodKind.Constructor, ContainingType.Name: "ObsoleteAttribute" }, + (s, c) => s.StartsWith("Obsolete", c), + (_, _, _) => true, + x => x.Name == "message", + (_, i) => i is 0); + var result = MatchArgumentVB(context, descriptor); + result.Should().BeTrue(); + } + +#endif + + [DataTestMethod] + [DataRow("""[Designer($$"designerTypeName")]""", "designerTypeName", 0)] + [DataRow("""[DesignerAttribute($$"designerTypeName")]""", "designerTypeName", 0)] + [DataRow("""[DesignerAttribute($$"designerTypeName", "designerBaseTypeName")]""", "designerTypeName", 0)] + [DataRow("""[DesignerAttribute("designerTypeName", $$"designerBaseTypeName")]""", "designerBaseTypeName", 1)] + [DataRow("""[Designer($$designerBaseTypeName: "designerBaseTypeName", designerTypeName: "designerTypeName")]""", "designerBaseTypeName", 1)] + [DataRow("""[Designer(designerBaseTypeName: "designerBaseTypeName", $$designerTypeName: "designerTypeName")]""", "designerTypeName", 0)] + [DataRow("""[Designer(designerBaseTypeName: "designerBaseTypeName", $$designerTypeName: "designerTypeName")]""", "designerTypeName", 1)] + [DataRow("""[Designer($$"designerTypeName"$$, "designerBaseTypeName"), DesignerCategory("Form")]""", "designerTypeName", 0)] + [DataRow("""[DesignerCategory("Form"), Designer($$"designerTypeName"$$, "designerBaseTypeName")]""", "designerTypeName", 0)] + public void Attribute_ConstructorParameters(string attribute, string parameterName, int descriptorPosition) + { + var snippet = $$""" + using System.ComponentModel; + + {{attribute}} + public class Test + { + public void M() + { + } + } + """; + var context = ArgumentContextCS(snippet); + + var descriptor = ArgumentDescriptor.AttributeArgument("Designer", parameterName, descriptorPosition); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeTrue(); + } + + [TestMethod] + public void Attribute_PropertySetter_WrongMethod() + { + var snippet = $$""" + [Designer($$designerTypeName = "hey")] + public class Test { } + + public class Designer : System.Attribute + { + public string designerTypeName { get; set; } + } + """; + var context = ArgumentContextCS(snippet); + + // This should call AttributeProperty, not AttributeArgument. + var descriptor = ArgumentDescriptor.AttributeArgument("Designer", "designerTypeName", 0); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeFalse(); + } + + [TestMethod] + public void Attribute_ConstructorParameters_WrongMethod() + { + var snippet = $$""" + [Designer($$"value")] + public class Test { } + + public class Designer(string designerTypeName) : System.Attribute { } + """; + var context = ArgumentContextCS(snippet); + + // This should call AttributeArgument, no AttributeProperty. + var descriptor = ArgumentDescriptor.AttributeProperty("Designer", "designerTypeName"); + var result = MatchArgumentCS(context, descriptor); + result.Should().BeFalse(); + } + + [DataTestMethod] + [DataRow("""""", "designerTypeName", 0)] + [DataRow("""""", "designerTypeName", 0)] + [DataRow("""""", "designerTypeName", 0)] + [DataRow("""""", "designerBaseTypeName", 1)] + public void Attribute_ConstructorParameters_VB(string attribute, string parameterName, int descriptorPosition) + { + var snippet = $$""" + Imports System.ComponentModel + + {{attribute}} + Public Class Test + End Class + """; + var context = ArgumentContextVB(snippet); + + var descriptor = ArgumentDescriptor.AttributeArgument("Designer", parameterName, descriptorPosition); + var result = MatchArgumentVB(context, descriptor); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""[AttributeUsage(AttributeTargets.All, $$AllowMultiple = true)]""", "AllowMultiple", true)] + [DataRow("""[AttributeUsage(AttributeTargets.All, $$AllowMultiple = true, Inherited = true)]""", "AllowMultiple", true)] + [DataRow("""[AttributeUsage(AttributeTargets.All, $$AllowMultiple = true, Inherited = true)]""", "Inherited", false)] + [DataRow("""[AttributeUsage(AttributeTargets.All, AllowMultiple = true, $$Inherited = true)]""", "Inherited", true)] + public void Attribute_PropertySetter(string attribute, string propertyName, bool expected) + { + var snippet = $$""" + using System; + + {{attribute}} + public sealed class TestAttribute: Attribute + { + } + """; + var context = ArgumentContextCS(snippet); + + var descriptor = ArgumentDescriptor.AttributeProperty("AttributeUsage", propertyName); + var result = MatchArgumentCS(context, descriptor); + result.Should().Be(expected); + if (result) + { + // The mapped parameter is the "value" parameter of the property set method. + context.Parameter.Name.Should().Be("value"); + var method = context.Parameter.ContainingSymbol.Should().BeAssignableTo().Which; + method.MethodKind.Should().Be(MethodKind.PropertySet); + method.AssociatedSymbol.Should().BeAssignableTo().Which.Name.Should().Be(propertyName); + } + } + + [DataTestMethod] + [DataRow("""""", "AllowMultiple", true)] + [DataRow("""""", "AllowMultiple", true)] + [DataRow("""""", "Inherited", false)] + [DataRow("""""", "Inherited", true)] + public void Attribute_PropertySetter_VB(string attribute, string propertyName, bool expected) + { + var snippet = $$""" + Imports System + + {{attribute}} + Public NotInheritable Class TestAttribute + Inherits Attribute + End Class + """; + var context = ArgumentContextVB(snippet); + + var descriptor = ArgumentDescriptor.AttributeProperty("AttributeUsage", propertyName); + var result = MatchArgumentVB(context, descriptor); + result.Should().Be(expected); + if (result) + { + // The mapped parameter is the "value" parameter of the property set method. + context.Parameter.Name.Should().Be("value"); + var method = context.Parameter.ContainingSymbol.Should().BeAssignableTo().Which; + method.MethodKind.Should().Be(MethodKind.PropertySet); + method.AssociatedSymbol.Should().BeAssignableTo().Which.Name.Should().Be(propertyName); + } + } + + private static string WrapInMethodCS(string snippet) => + $$""" + using System; + class C + { + public void M() + { + {{snippet}} + } + } + """; + + private static string WrapInMethodVB(string snippet) => + $$""" + Imports System + Class C + Public Sub M() + {{snippet}} + End Sub + End Class + """; + + private static ArgumentContext ArgumentContextCS(string snippet) => + ArgumentContext(snippet, TestHelper.CompileCS, typeof(CS.ArgumentSyntax), typeof(CS.AttributeArgumentSyntax)); + + private static ArgumentContext ArgumentContextVB(string snippet) => + ArgumentContext(snippet, TestHelper.CompileVB, typeof(VB.ArgumentSyntax)); + + private static ArgumentContext ArgumentContext(string snippet, + Func compile, params Type[] descriptorNodeTypes) + { + var position = snippet.IndexOf("$$"); + if (position == -1) + { + throw new InvalidOperationException("The $$ maker was not found"); + } + snippet = snippet.Replace("$$", string.Empty); + var (tree, model) = compile(snippet, MetadataReferenceFacade.SystemCollections.Concat(MetadataReferenceFacade.SystemDiagnosticsProcess).ToArray()); + var node = tree.GetRoot() + .DescendantNodesAndSelf(new TextSpan(position, 1)) // root.Find does not work with OmittedArgument + .Reverse() + .First(x => Array.Exists(descriptorNodeTypes, t => t.IsInstanceOfType(x))); + return new(node, model); + } + + private static bool MatchArgumentCS(ArgumentContext context, ArgumentDescriptor descriptor) => + MatchArgument(context, descriptor); + + private static bool MatchArgumentVB(ArgumentContext context, ArgumentDescriptor descriptor) => + MatchArgument(context, descriptor); + + private static bool MatchArgument(ArgumentContext context, ArgumentDescriptor descriptor) + where TTracker : ArgumentTracker, new() + where TSyntaxKind : struct => + new TTracker().MatchArgument(descriptor)(context); +}