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