diff --git a/analyzers/rspec/cs/S6932.html b/analyzers/rspec/cs/S6932.html new file mode 100644 index 00000000000..7f0cef39296 --- /dev/null +++ b/analyzers/rspec/cs/S6932.html @@ -0,0 +1,336 @@ +

The HttpRequest class provides access to the raw request data through the QueryString, Headers, and +Forms properties. However, whenever possible it is recommended to use model binding instead of directly accessing the input data.

+

Why is this an issue?

+

Both ASP.Net MVC implementations - Core and Framework - support model binding in a comparable fashion. Model binding streamlines the +process by automatically aligning data from HTTP requests with action method parameters, providing numerous benefits compared to manually parsing raw +incoming request data:

+
+
+ Simplicity +
+
+

Model binding simplifies the code by automatically mapping data from HTTP requests to action method parameters. You don’t need to write any + code to manually extract values from the request.

+
+
+ Type Safety +
+
+

Model binding provides type safety by automatically converting the incoming data into the appropriate .NET types. If the conversion fails, the + model state becomes invalid, which you can easily check using ModelState.IsValid.

+
+
+ Validation +
+
+

With model binding, you can easily apply validation rules to your models using data annotations. If the incoming data doesn’t comply with these + rules, the model state becomes invalid.

+
+
+ Security +
+
+

Model binding helps protect against over-posting attacks by only including properties in the model that you explicitly bind using the + [Bind] attribute or by using view models that only contain the properties you want to update.

+
+
+ Maintainability +
+
+

By using model binding, your code becomes cleaner, easier to read, and maintain. It promotes the use of strongly typed views, which can provide + compile-time checking of your views.

+
+
+

How to fix it in ASP.NET Core

+

Request.Form, Request.Form.Files, Request.Headers, Request.Query and Request.RouteValues are keyed +collections that expose data from the incoming HTTP request:

+ +

Model binding can bind these keyed collections to

+ +

To replace the keyed collection access, you can:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Replacewith parameter bindingor complex type bindingor route binding

Request.Form["id"]

optional [FromForm] + attribute on the parameter or a FormCollection parameter

optional [FromForm] + attribute on the property

Request.Form.Files

IFormFile, IFormFileCollection, or + IEnumerable<IFormFile> parameter

Request.Headers["id"]

[FromHeader] + attribute on the parameter

[FromHeader] + attribute on the property

Request.Query["id"]

optional [FromQuery] + attribute on the parameter

optional [FromQuery] + attribute on the property

Request.RouteValues["id"]

optional [FromRoute] + attribute on the parameter

optional [Route("{id}")]attribute on the + action method/controller or via conventional routing

+

The Model Binding in ASP.NET Core article describes the +mechanisms, conventions, and customization options for model binding in more detail. Route-based binding is described in the Routing to controller actions in ASP.NET Core document.

+

Code examples

+

Noncompliant code example

+
+public IActionResult Post()
+{
+    var name = Request.Form["name"];                           // Noncompliant: Request.Form
+    var birthdate = DateTime.Parse(Request.Form["Birthdate"]); // Noncompliant: Request.Form
+
+    var origin = Request.Headers[HeaderNames.Origin];          // Noncompliant: Request.Headers
+    var locale = Request.Query.TryGetValue("locale", out var locales)
+        ? locales.ToString()
+        : "en-US";                                             // Noncompliant: Request.Query
+    // ..
+}
+
+

Compliant solution

+
+public record User
+{
+    [Required, StringLength(100)]
+    public required string Name { get; init; }
+    [DataType(DataType.Date)]
+    public DateTime? Birthdate { get; init; }
+}
+
+public IActionResult Post(User user, [FromHeader] string origin, [FromQuery] string locale = "en-US")
+{
+    if (ModelState.IsValid)
+    {
+        // ...
+    }
+}
+
+

How does this work?

+

Model binding in ASP.NET Core MVC and ASP.NET MVC 4.x works by automatically mapping data from HTTP requests to action method parameters. Here’s a +step-by-step breakdown of how it works:

+
    +
  1. Request Data When a user submits a form or sends a request to an ASP.NET application, the request data might include form + data, query string parameters, request body, and HTTP headers.
  2. +
  3. Model Binder The model binder’s job is to create .NET objects from the request data. It looks at each parameter in the action + method and attempts to populate it with the incoming data.
  4. +
  5. Value Providers The model binder uses Value Providers to get data from various parts of the request, such as the query string, + form data, or route data. Each value provider tells the model binder where to find values in the request.
  6. +
  7. Binding The model binder tries to match the keys from the incoming data with the properties of the action method’s parameters. + If a match is found, it attempts to convert the incoming data into the appropriate .NET type and assigns it to the parameter.
  8. +
  9. Validation If the model binder can’t convert the value or if the converted value doesn’t pass any specified validation rules, + it adds an error to the ModelState.Errors collection. You can check ModelState.IsValid in your action method to see if any + errors occurred during model binding.
  10. +
  11. Action Method Execution The action method is executed with the bound parameters. If ModelState.IsValid is + false, you can handle the errors in your action method and return an appropriate response.
  12. +
+

See the links in the Resources section for more information.

+

How to fix it in ASP.NET MVC 4.x

+

Request.Form and Request.QueryString are keyed collections +that expose data from the incoming HTTP request:

+ +

Model binding can bind these keyed collections to

+ +

To replace the keyed collection access, you can:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Replacewith parameter bindingor complex type binding

Request.Form["id"]

optional [Bind] attribute on the + parameter or a FormCollection + parameter

optional [Bind] attribute on the + parameter or type

Request.QueryString["id"]

optional [Bind] attribute on the + parameter

property name must match query parameter key

+

Code examples

+

Noncompliant code example

+
+public ActionResult Post()
+{
+    var name = Request.Form["name"];                            // Noncompliant: Request.Form
+    Debug.WriteLine(Request.Form[0]);                           // Compliant: Binding by index is not supported.
+    var birthdate = DateTime.Parse(Request.Form["Birthdate"]);  // Noncompliant: Request.Form
+
+    var cultureName = Request.QueryString["locale"] ?? "en-US"; // Noncompliant: Request.QueryString
+    // ..
+}
+
+

Compliant solution

+
+public class User
+{
+    [Required, StringLength(100)]
+    public string Name { get; set; }
+    [DataType(DataType.Date)]
+    public DateTime? Birthdate { get; set; }
+}
+
+public ActionResult Post(User user, [Bind(Prefix = "locale")] string cultureName = "en-US")
+{
+    if (ModelState.IsValid)
+    {
+        // ...
+    }
+}
+
+

How does this work?

+

Model binding in ASP.NET Core MVC and ASP.NET MVC 4.x works by automatically mapping data from HTTP requests to action method parameters. Here’s a +step-by-step breakdown of how it works:

+
    +
  1. Request Data When a user submits a form or sends a request to an ASP.NET application, the request data might include form + data, query string parameters, request body, and HTTP headers.
  2. +
  3. Model Binder The model binder’s job is to create .NET objects from the request data. It looks at each parameter in the action + method and attempts to populate it with the incoming data.
  4. +
  5. Value Providers The model binder uses Value Providers to get data from various parts of the request, such as the query string, + form data, or route data. Each value provider tells the model binder where to find values in the request.
  6. +
  7. Binding The model binder tries to match the keys from the incoming data with the properties of the action method’s parameters. + If a match is found, it attempts to convert the incoming data into the appropriate .NET type and assigns it to the parameter.
  8. +
  9. Validation If the model binder can’t convert the value or if the converted value doesn’t pass any specified validation rules, + it adds an error to the ModelState.Errors collection. You can check ModelState.IsValid in your action method to see if any + errors occurred during model binding.
  10. +
  11. Action Method Execution The action method is executed with the bound parameters. If ModelState.IsValid is + false, you can handle the errors in your action method and return an appropriate response.
  12. +
+

See the links in the Resources section for more information.

+

Resources

+

Documentation

+ + diff --git a/analyzers/rspec/cs/S6932.json b/analyzers/rspec/cs/S6932.json new file mode 100644 index 00000000000..24c4c6e7f45 --- /dev/null +++ b/analyzers/rspec/cs/S6932.json @@ -0,0 +1,25 @@ +{ + "title": "Use model binding instead of reading raw request data", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [ + "asp.net" + ], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-6932", + "sqKey": "S6932", + "scope": "Main", + "quickfix": "infeasible", + "code": { + "impacts": { + "MAINTAINABILITY": "HIGH", + "RELIABILITY": "MEDIUM", + "SECURITY": "MEDIUM" + }, + "attribute": "FOCUSED" + } +} 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..871a8663981 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.CSharp/Extensions/ExpressionSyntaxExtensions.Roslyn.cs @@ -0,0 +1,288 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.CodeDom.Compiler; + +namespace SonarAnalyzer.Extensions; + +[GeneratedCode("Copied From Roslyn", "575bc42589145ba18b4f1cc2267d02695f861d8f")] +public partial class ExpressionSyntaxExtensions +{ + // Copied from + // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L319 + 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. + /// + // Copy of + // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L273 + 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; + } + } + } + + // Copy of + // 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; + + // Copy of + // 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); + } + + // Copy of + // 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. + /// + // Copy of + // https://github.com/dotnet/roslyn/blob/575bc42589145ba18b4f1cc2267d02695f861d8f/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ExpressionSyntaxExtensions.cs#L201 + 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; + } + + // Copy of + // 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/ExpressionSyntaxExtensions.cs b/analyzers/src/SonarAnalyzer.CSharp/Extensions/ExpressionSyntaxExtensions.cs index 2f75c0e0210..d60923bb424 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Extensions/ExpressionSyntaxExtensions.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Extensions/ExpressionSyntaxExtensions.cs @@ -20,7 +20,7 @@ namespace SonarAnalyzer.Extensions { - public static class ExpressionSyntaxExtensions + public static partial class ExpressionSyntaxExtensions { private static readonly ISet EqualsOrNotEquals = new HashSet { diff --git a/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs b/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs index 4dfe05a601d..cbfa91cec3b 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs @@ -179,7 +179,6 @@ public static SyntaxNode WalkUpParentheses(this SyntaxNode node) DestructorDeclarationSyntax { Identifier: var identifier } => identifier, EnumMemberDeclarationSyntax { Identifier: var identifier } => identifier, EventDeclarationSyntax { Identifier: var identifier } => identifier, - IdentifierNameSyntax { Identifier: var identifier } => identifier, IndexerDeclarationSyntax { ThisKeyword: var thisKeyword } => thisKeyword, InvocationExpressionSyntax { @@ -199,6 +198,7 @@ public static SyntaxNode WalkUpParentheses(this SyntaxNode node) 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, @@ -363,23 +363,14 @@ 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, + ElementAccessExpressionSyntax x => x.ArgumentList, + ElementBindingExpressionSyntax x => x.ArgumentList, ConstructorInitializerSyntax constructorInitializer => constructorInitializer.ArgumentList, null => null, _ when PrimaryConstructorBaseTypeSyntaxWrapper.IsInstance(node) => ((PrimaryConstructorBaseTypeSyntaxWrapper)node).ArgumentList, @@ -503,6 +494,44 @@ public static ConditionalAccessExpressionSyntax GetRootConditionalAccessExpressi 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()); + private static string GetUnknownType(SyntaxKind kind) => #if DEBUG diff --git a/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs index 25023b750ba..eefe6074bd6 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxFacade.cs @@ -108,6 +108,9 @@ public override bool IsMemberAccessOnKnownType(SyntaxNode memberAccess, string n 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..87983c5909f 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 { get; } = 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..7554163392c 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpAttributeParameterLookup.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpAttributeParameterLookup.cs @@ -30,4 +30,7 @@ protected override SyntaxNode Expression(AttributeArgumentSyntax argument) => protected override SyntaxToken? GetNameColonArgumentIdentifier(AttributeArgumentSyntax argument) => argument.NameColon?.Name.Identifier; + + protected override SyntaxToken? GetNameEqualsArgumentIdentifier(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..ffefb350e07 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpMethodParameterLookup.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Helpers/CSharpMethodParameterLookup.cs @@ -28,10 +28,10 @@ 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) => @@ -39,4 +39,7 @@ protected override SyntaxNode Expression(ArgumentSyntax argument) => protected override SyntaxToken? GetNameColonArgumentIdentifier(ArgumentSyntax argument) => argument.NameColon?.Name.Identifier; + + protected override SyntaxToken? GetNameEqualsArgumentIdentifier(ArgumentSyntax argument) => + null; } diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseAspNetModelBinding.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseAspNetModelBinding.cs new file mode 100644 index 00000000000..36d58d5889b --- /dev/null +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseAspNetModelBinding.cs @@ -0,0 +1,261 @@ +/* + * 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.Collections.Concurrent; + +namespace SonarAnalyzer.Rules.CSharp; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseAspNetModelBinding : SonarDiagnosticAnalyzer +{ + private const string DiagnosticId = "S6932"; + private const string UseAspNetModelBindingMessage = "Use model binding instead of accessing the raw request data"; + private const string UseIFormFileBindingMessage = "Use IFormFile or IFormFileCollection binding instead"; + + protected override string MessageFormat => "{0}"; + + protected override ILanguageFacade Language => CSharpFacade.Instance; + + public UseAspNetModelBinding() : base(DiagnosticId) { } + + protected override void Initialize(SonarAnalysisContext context) + { + context.RegisterCompilationStartAction(compilationStartContext => + { + var (argumentDescriptors, propertyAccessDescriptors) = GetDescriptors(compilationStartContext.Compilation); + if (argumentDescriptors.Any() || propertyAccessDescriptors.Any()) + { + compilationStartContext.RegisterSymbolStartAction(symbolStart => + { + // If the user overrides any action filters, model binding may not be working as expected. + // Then we do not want to raise on expressions that originate from parameters. + // See the OverridesController.Undecidable test cases for details. + var hasActionFiltersOverrides = false; + var candidates = new ConcurrentStack(); // In SymbolEnd, we filter the candidates based on the overriding we learn on the go. + if (symbolStart.Symbol is INamedTypeSymbol namedType && namedType.IsControllerType()) + { + symbolStart.RegisterCodeBlockStartAction(codeBlockStart => + { + if (IsOverridingFilterMethods(codeBlockStart.OwningSymbol)) + { + // We do not want to raise in ActionFilter overrides and so we do not register. + // The SymbolEndAction needs to be made aware, that there are + // ActionFilter overrides, so it can filter out some candidates. + hasActionFiltersOverrides = true; + } + else + { + RegisterCodeBlockActions(codeBlockStart, argumentDescriptors, propertyAccessDescriptors, candidates); + } + }); + } + symbolStart.RegisterSymbolEndAction(symbolEnd => + { + foreach (var candidate in candidates.Where(x => !(hasActionFiltersOverrides && x.OriginatesFromParameter))) + { + symbolEnd.ReportIssue(Diagnostic.Create(Rule, candidate.Location, candidate.Message)); + } + }); + }, SymbolKind.NamedType); + } + }); + } + + private void RegisterCodeBlockActions(SonarCodeBlockStartAnalysisContext codeBlockStart, + ArgumentDescriptor[] argumentDescriptors, MemberDescriptor[] propertyAccessDescriptors, + ConcurrentStack controllerCandidates) + { + // Within a single code block, access via constant and variable keys could be mixed. + // We only want to raise, if all access were done via constants. + var allConstantAccesses = true; + var codeBlockCandidates = new ConcurrentStack(); + if (argumentDescriptors.Any()) + { + codeBlockStart.RegisterNodeAction(nodeContext => + { + var argument = (ArgumentSyntax)nodeContext.Node; + var context = new ArgumentContext(argument, nodeContext.SemanticModel); + if (allConstantAccesses && Array.Exists(argumentDescriptors, x => Language.Tracker.Argument.MatchArgument(x)(context))) + { + allConstantAccesses &= nodeContext.SemanticModel.GetConstantValue(argument.Expression) is { HasValue: true, Value: string }; + codeBlockCandidates.Push(new(UseAspNetModelBindingMessage, GetPrimaryLocation(argument), IsOriginatingFromParameter(nodeContext.SemanticModel, argument))); + } + }, SyntaxKind.Argument); + } + if (propertyAccessDescriptors.Any()) + { + codeBlockStart.RegisterNodeAction(nodeContext => + { + // The property access of Request.Form.Files can be replaced by an IFormFile binding. + // Any access to a "Files" property is therefore noncompliant. This is different from the Argument handling above. + var memberAccess = (MemberAccessExpressionSyntax)nodeContext.Node; + var context = new PropertyAccessContext(memberAccess, nodeContext.SemanticModel, memberAccess.Name.Identifier.ValueText); + if (Language.Tracker.PropertyAccess.MatchProperty(propertyAccessDescriptors)(context)) + { + codeBlockCandidates.Push(new(UseIFormFileBindingMessage, memberAccess.GetLocation(), IsOriginatingFromParameter(nodeContext.SemanticModel, memberAccess))); + } + }, SyntaxKind.SimpleMemberAccessExpression); + } + codeBlockStart.RegisterCodeBlockEndAction(codeBlockEnd => + { + if (allConstantAccesses) + { + controllerCandidates.PushRange([.. codeBlockCandidates]); + } + }); + } + + private static (ArgumentDescriptor[] ArgumentDescriptors, MemberDescriptor[] PropertyAccessDescriptors) GetDescriptors(Compilation compilation) + { + var argumentDescriptors = new List(); + var propertyAccessDescriptors = new List(); + if (compilation.GetTypeByMetadataName(KnownType.Microsoft_AspNetCore_Mvc_ControllerAttribute) is { }) + { + AddAspNetCoreDescriptors(argumentDescriptors, propertyAccessDescriptors); + } + // TODO: Add descriptors for Asp.Net MVC 4.x + return ([.. argumentDescriptors], [.. propertyAccessDescriptors]); + } + + private static void AddAspNetCoreDescriptors(List argumentDescriptors, List propertyAccessDescriptors) + { + argumentDescriptors.AddRange([ + ArgumentDescriptor.ElementAccess(// Request.Form["id"] + invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IFormCollection, + invokedIndexerExpression: "Form", + parameterConstraint: _ => true, // There is only a single overload and it is getter only + argumentPosition: 0), + ArgumentDescriptor.MethodInvocation(// Request.Form.TryGetValue("id", out _) + invokedType: KnownType.Microsoft_AspNetCore_Http_IFormCollection, + methodName: "TryGetValue", + parameterName: "key", + argumentPosition: 0), + ArgumentDescriptor.MethodInvocation(// Request.Form.ContainsKey("id") + invokedType: KnownType.Microsoft_AspNetCore_Http_IFormCollection, + methodName: "ContainsKey", + parameterName: "key", + argumentPosition: 0), + ArgumentDescriptor.ElementAccess(// Request.Headers["id"] + invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IHeaderDictionary, + invokedIndexerExpression: "Headers", + parameterConstraint: IsGetterParameter, // Headers are read/write + argumentPosition: 0), + ArgumentDescriptor.MethodInvocation(// Request.Headers.TryGetValue("id", out _) + invokedMethodSymbol: x => IsIDictionaryStringStringValuesInvocation(x, "TryGetValue"), // TryGetValue is from IDictionary here. We check the type arguments. + invokedMemberNameConstraint: (name, comparison) => string.Equals(name, "TryGetValue", comparison), + invokedMemberNodeConstraint: IsAccessedViaHeaderDictionary, + parameterConstraint: x => string.Equals(x.Name, "key", StringComparison.Ordinal), + argumentListConstraint: (list, position) => list.Count == 2 && position is 0 or null, + refKind: RefKind.None), + ArgumentDescriptor.MethodInvocation(// Request.Headers.ContainsKey("id") + invokedMethodSymbol: x => IsIDictionaryStringStringValuesInvocation(x, "ContainsKey"), + invokedMemberNameConstraint: (name, comparison) => string.Equals(name, "ContainsKey", comparison), + invokedMemberNodeConstraint: IsAccessedViaHeaderDictionary, + parameterConstraint: x => string.Equals(x.Name, "key", StringComparison.Ordinal), + argumentListConstraint: (list, _) => list.Count == 1, + refKind: RefKind.None), + ArgumentDescriptor.ElementAccess(// Request.Query["id"] + invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Http_IQueryCollection, + invokedIndexerExpression: "Query", + parameterConstraint: _ => true, // There is only a single overload and it is getter only + argumentPosition: 0), + ArgumentDescriptor.MethodInvocation(// Request.Query.TryGetValue("id", out _) + invokedType: KnownType.Microsoft_AspNetCore_Http_IQueryCollection, + methodName: "TryGetValue", + parameterName: "key", + argumentPosition: 0), + ArgumentDescriptor.ElementAccess(// Request.RouteValues["id"] + invokedIndexerContainer: KnownType.Microsoft_AspNetCore_Routing_RouteValueDictionary, + invokedIndexerExpression: "RouteValues", + parameterConstraint: IsGetterParameter, // RouteValues are read/write + argumentPosition: 0), + ArgumentDescriptor.MethodInvocation(// Request.RouteValues.TryGetValue("id", out _) + invokedType: KnownType.Microsoft_AspNetCore_Routing_RouteValueDictionary, + methodName: "TryGetValue", + parameterName: "key", + argumentPosition: 0)]); + + propertyAccessDescriptors.Add(new(KnownType.Microsoft_AspNetCore_Http_IFormCollection, "Files")); // Request.Form.Files... + } + + // Check that the "Headers" expression in the Headers.TryGetValue("id", out _) invocation is of type IHeaderDictionary + private static bool IsAccessedViaHeaderDictionary(SemanticModel model, ILanguageFacade language, SyntaxNode invocation) => + invocation is InvocationExpressionSyntax { Expression: { } expression } + && GetLeftOfDot(expression) is { } left + && model.GetTypeInfo(left) is { Type: { } typeSymbol } && typeSymbol.Is(KnownType.Microsoft_AspNetCore_Http_IHeaderDictionary); + + private static bool IsOverridingFilterMethods(ISymbol owningSymbol) => + (owningSymbol.GetOverriddenMember() ?? owningSymbol).ExplicitOrImplicitInterfaceImplementations().Any(x => x is IMethodSymbol { ContainingType: { } container } + && container.IsAny( + KnownType.Microsoft_AspNetCore_Mvc_Filters_IActionFilter, + KnownType.Microsoft_AspNetCore_Mvc_Filters_IAsyncActionFilter)); + + private static bool IsOriginatingFromParameter(SemanticModel semanticModel, ArgumentSyntax argument) => + GetExpressionOfArgumentParent(argument) is { } parentExpression && IsOriginatingFromParameter(semanticModel, parentExpression); + + private static bool IsOriginatingFromParameter(SemanticModel semanticModel, ExpressionSyntax expression) => + MostLeftOfDottedChain(expression) is { } mostLeft && semanticModel.GetSymbolInfo(mostLeft).Symbol is IParameterSymbol; + + private static ExpressionSyntax GetLeftOfDot(ExpressionSyntax expression) => + expression switch + { + MemberAccessExpressionSyntax memberAccessExpression => memberAccessExpression.Expression, + MemberBindingExpressionSyntax memberBindingExpression => memberBindingExpression.GetParentConditionalAccessExpression()?.Expression, + _ => null, + }; + + private static ExpressionSyntax MostLeftOfDottedChain(ExpressionSyntax root) + { + var current = root.GetRootConditionalAccessExpression()?.Expression ?? root; + while (current.Kind() is SyntaxKind.SimpleMemberAccessExpression or SyntaxKind.ElementAccessExpression) + { + current = current switch + { + MemberAccessExpressionSyntax { Expression: { } left } => left, + ElementAccessExpressionSyntax { Expression: { } left } => left, + _ => throw new InvalidOperationException("Unreachable"), + }; + } + return current; + } + + private static ExpressionSyntax GetExpressionOfArgumentParent(ArgumentSyntax argument) => + argument switch + { + { Parent: BracketedArgumentListSyntax { Parent: ElementBindingExpressionSyntax expression } } => expression.GetParentConditionalAccessExpression(), + { Parent: BracketedArgumentListSyntax { Parent: ElementAccessExpressionSyntax { Expression: { } expression } } } => expression, + { Parent: ArgumentListSyntax { Parent: InvocationExpressionSyntax { Expression: { } expression } } } => expression, + _ => null, + }; + + private static Location GetPrimaryLocation(ArgumentSyntax argument) => + ((SyntaxNode)GetExpressionOfArgumentParent(argument) ?? argument).GetLocation(); + + private static bool IsGetterParameter(IParameterSymbol parameter) => + parameter.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.PropertyGet }; + + private static bool IsIDictionaryStringStringValuesInvocation(IMethodSymbol method, string name) => + method.Is(KnownType.System_Collections_Generic_IDictionary_TKey_TValue, name) + && method.ContainingType.TypeArguments is { Length: 2 } typeArguments + && typeArguments[0].Is(KnownType.System_String) + && typeArguments[1].Is(KnownType.Microsoft_Extensions_Primitives_StringValues); + + private readonly record struct ReportCandidate(string Message, Location Location, bool OriginatesFromParameter); +} diff --git a/analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs b/analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs new file mode 100644 index 00000000000..488ef59ad02 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.CSharp/Trackers/CSharpArgumentTracker.cs @@ -0,0 +1,93 @@ +/* + * 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 + { + AttributeArgumentSyntax => null, + ArgumentSyntax { RefOrOutKeyword: { } refOrOut } => refOrOut.Kind() switch { SyntaxKind.OutKeyword => RefKind.Out, SyntaxKind.RefKeyword => RefKind.Ref, _ => RefKind.None }, + _ => null, + }; + + protected override bool InvocationFitsMemberKind(SyntaxNode invokedExpression, InvokedMemberKind memberKind) => + memberKind switch + { + InvokedMemberKind.Method => invokedExpression is InvocationExpressionSyntax, + InvokedMemberKind.Constructor => invokedExpression is ObjectCreationExpressionSyntax + or ConstructorInitializerSyntax + || ImplicitObjectCreationExpressionSyntaxWrapper.IsInstance(invokedExpression), + InvokedMemberKind.Indexer => invokedExpression is ElementAccessExpressionSyntax or ElementBindingExpressionSyntax, + InvokedMemberKind.Attribute => invokedExpression is AttributeSyntax, + _ => false, + }; + + protected override bool InvokedMemberFits(SemanticModel model, SyntaxNode invokedExpression, InvokedMemberKind memberKind, Func invokedMemberNameConstraint) => + memberKind switch + { + InvokedMemberKind.Method => invokedMemberNameConstraint(invokedExpression.GetName()), + InvokedMemberKind.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, + }, + InvokedMemberKind.Indexer => invokedExpression switch + { + ElementAccessExpressionSyntax { Expression: { } accessedExpression } => invokedMemberNameConstraint(accessedExpression.GetName()), + ElementBindingExpressionSyntax { } binding => binding.GetParentConditionalAccessExpression() is + { Expression: { } accessedExpression } && invokedMemberNameConstraint(accessedExpression.GetName()), + _ => false, + }, + InvokedMemberKind.Attribute => invokedExpression is AttributeSyntax { Name: { } typeName } && invokedMemberNameConstraint(typeName.GetName()), + _ => false, + }; + + private 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..7edf1f8bfd0 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/ArgumentDescriptor.cs @@ -0,0 +1,177 @@ +/* + * 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 InvokedMemberKind +{ + Method, + Constructor, + Indexer, + Attribute +} + +public class ArgumentDescriptor +{ + private ArgumentDescriptor(InvokedMemberKind 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, p => p.Name == parameterName, argumentPosition, null); + + public static ArgumentDescriptor MethodInvocation(KnownType invokedType, string methodName, string parameterName, Func argumentPosition, RefKind refKind) => + MethodInvocation(invokedType, methodName, p => p.Name == parameterName, argumentPosition, refKind); + + public static ArgumentDescriptor MethodInvocation(KnownType invokedType, string methodName, Func parameterConstraint, Func argumentPosition, RefKind? refKind) => + MethodInvocation(invokedType, (n, c) => n.Equals(methodName, c), parameterConstraint, argumentPosition, refKind); + + public static ArgumentDescriptor MethodInvocation(KnownType invokedType, Func invokedMemberNameConstraint, Func parameterConstraint, + Func argumentPosition, RefKind? refKind) => + MethodInvocation(s => invokedType.Matches(s.ContainingType), invokedMemberNameConstraint, parameterConstraint, argumentPosition, refKind); + + public static ArgumentDescriptor MethodInvocation(Func invokedMethodSymbol, Func invokedMemberNameConstraint, + Func parameterConstraint, Func argumentPosition, RefKind? refKind) => + MethodInvocation(invokedMethodSymbol, + invokedMemberNameConstraint, + (_, _, _) => true, + parameterConstraint, + (_, position) => position is null || argumentPosition is null || argumentPosition(position.Value), + refKind); + + public static ArgumentDescriptor MethodInvocation(Func invokedMethodSymbol, Func invokedMemberNameConstraint, + Func invokedMemberNodeConstraint, Func parameterConstraint, + Func, int?, bool> argumentListConstraint, RefKind? refKind) => + new(InvokedMemberKind.Method, + invokedMemberConstraint: invokedMethodSymbol, + invokedMemberNameConstraint: invokedMemberNameConstraint, + invokedMemberNodeConstraint: invokedMemberNodeConstraint, + argumentListConstraint: argumentListConstraint, + parameterConstraint: parameterConstraint, + refKind: 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(InvokedMemberKind.Constructor, + invokedMemberConstraint: invokedMethodSymbol, + invokedMemberNameConstraint: invokedMemberNameConstraint, + invokedMemberNodeConstraint: invokedMemberNodeConstraint, + argumentListConstraint: argumentListConstraint, + parameterConstraint: parameterConstraint, + refKind: 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, + argumentListConstraint: (_, p) => argumentPositionConstraint is null || p is null || argumentPositionConstraint(p.Value), + parameterConstraint: parameterConstraint); + + public static ArgumentDescriptor ElementAccess(Func invokedIndexerPropertyMethod, Func invokedIndexerExpression, + Func invokedIndexerExpressionNodeConstraint, Func parameterConstraint, + Func, int?, bool> argumentListConstraint) => + new(InvokedMemberKind.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 } && (name == attributeName || name == $"{attributeName}Attribute"), + (x, c) => AttributeClassNameConstraint(attributeName, x, c), + (_, _, _) => true, + p => p.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(InvokedMemberKind.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: p => true, + argumentListConstraint: (_, _) => true); + + private static bool AttributeClassNameConstraint(string expectedAttributeName, string nodeClassName, StringComparison c) => + nodeClassName.Equals(expectedAttributeName, c) || nodeClassName.Equals($"{expectedAttributeName}Attribute"); + + public InvokedMemberKind 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; } +} diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/AspNetMvcHelper.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/AspNetMvcHelper.cs index 108b5694749..47d9d55a8a3 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/AspNetMvcHelper.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/AspNetMvcHelper.cs @@ -52,7 +52,7 @@ public static bool IsControllerMethod(this IMethodSymbol methodSymbol) => /// Returns a value indicating whether the provided type symbol is a ASP.NET MVC /// controller. /// - private static bool IsControllerType(this INamedTypeSymbol containingType) => + public static bool IsControllerType(this INamedTypeSymbol containingType) => containingType != null && (containingType.DerivesFromAny(ControllerTypes) || containingType.GetAttributes(ControllerAttributeTypes).Any()) diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs index aee81d18351..ab37fc5b4f1 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs @@ -64,13 +64,18 @@ public sealed partial class KnownType public static readonly KnownType Microsoft_AspNetCore_Hosting_WebHostBuilderExtensions = new("Microsoft.AspNetCore.Hosting.WebHostBuilderExtensions"); public static readonly KnownType Microsoft_AspNetCore_Http_CookieOptions = new("Microsoft.AspNetCore.Http.CookieOptions"); public static readonly KnownType Microsoft_AspNetCore_Http_HeaderDictionaryExtensions = new("Microsoft.AspNetCore.Http.HeaderDictionaryExtensions"); + public static readonly KnownType Microsoft_AspNetCore_Http_HttpRequest = new("Microsoft.AspNetCore.Http.HttpRequest"); + public static readonly KnownType Microsoft_AspNetCore_Http_IFormCollection = new("Microsoft.AspNetCore.Http.IFormCollection"); public static readonly KnownType Microsoft_AspNetCore_Http_IHeaderDictionary = new("Microsoft.AspNetCore.Http.IHeaderDictionary"); + public static readonly KnownType Microsoft_AspNetCore_Http_IQueryCollection = new("Microsoft.AspNetCore.Http.IQueryCollection"); public static readonly KnownType Microsoft_AspNetCore_Http_IRequestCookieCollection = new("Microsoft.AspNetCore.Http.IRequestCookieCollection"); public static readonly KnownType Microsoft_AspNetCore_Http_IResponseCookies = new("Microsoft.AspNetCore.Http.IResponseCookies"); public static readonly KnownType Microsoft_AspNetCore_Mvc_Controller = new("Microsoft.AspNetCore.Mvc.Controller"); public static readonly KnownType Microsoft_AspNetCore_Mvc_ControllerBase = new("Microsoft.AspNetCore.Mvc.ControllerBase"); public static readonly KnownType Microsoft_AspNetCore_Mvc_ControllerAttribute = new("Microsoft.AspNetCore.Mvc.ControllerAttribute"); public static readonly KnownType Microsoft_AspNetCore_Mvc_DisableRequestSizeLimitAttribute = new("Microsoft.AspNetCore.Mvc.DisableRequestSizeLimitAttribute"); + public static readonly KnownType Microsoft_AspNetCore_Mvc_Filters_IActionFilter = new("Microsoft.AspNetCore.Mvc.Filters.IActionFilter"); + public static readonly KnownType Microsoft_AspNetCore_Mvc_Filters_IAsyncActionFilter = new("Microsoft.AspNetCore.Mvc.Filters.IAsyncActionFilter"); public static readonly KnownType Microsoft_AspNetCore_Mvc_FromServicesAttribute = new("Microsoft.AspNetCore.Mvc.FromServicesAttribute"); public static readonly KnownType Microsoft_AspNetCore_Mvc_IActionResult = new("Microsoft.AspNetCore.Mvc.IActionResult"); public static readonly KnownType Microsoft_AspNetCore_Mvc_IgnoreAntiforgeryTokenAttribute = new("Microsoft.AspNetCore.Mvc.IgnoreAntiforgeryTokenAttribute"); @@ -82,6 +87,7 @@ public sealed partial class KnownType public static readonly KnownType Microsoft_AspNetCore_Mvc_RouteAttribute = new("Microsoft.AspNetCore.Mvc.RouteAttribute"); public static readonly KnownType Microsoft_AspNetCore_Mvc_Routing_HttpMethodAttribute = new("Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute"); public static readonly KnownType Microsoft_AspNetCore_Razor_Hosting_RazorCompiledItemAttribute = new("Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemAttribute"); + public static readonly KnownType Microsoft_AspNetCore_Routing_RouteValueDictionary = new("Microsoft.AspNetCore.Routing.RouteValueDictionary"); public static readonly KnownType Microsoft_Azure_Cosmos_CosmosClient = new("Microsoft.Azure.Cosmos.CosmosClient"); public static readonly KnownType Microsoft_Azure_Documents_Client_DocumentClient = new("Microsoft.Azure.Documents.Client.DocumentClient"); public static readonly KnownType Microsoft_Azure_ServiceBus_Management_ManagementClient = new("Microsoft.Azure.ServiceBus.Management.ManagementClient"); @@ -214,6 +220,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..b2f83dba712 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/MethodParameterLookupBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/MethodParameterLookupBase.cs @@ -36,6 +36,7 @@ internal abstract class MethodParameterLookupBase : IMethodPara private readonly SeparatedSyntaxList argumentList; protected abstract SyntaxToken? GetNameColonArgumentIdentifier(TArgumentSyntax argument); + protected abstract SyntaxToken? GetNameEqualsArgumentIdentifier(TArgumentSyntax argument); protected abstract SyntaxNode Expression(TArgumentSyntax argument); public IMethodSymbol MethodSymbol { get; } @@ -75,6 +76,16 @@ private bool TryGetSymbol(SyntaxNode argument, IMethodSymbol methodSymbol, out I return parameter != null; } + if (GetNameEqualsArgumentIdentifier(arg) is { } nameEqualsArgumentIdentifier + && methodSymbol.ContainingType.GetMembers(nameEqualsArgumentIdentifier.ValueText) is { Length: 1 } properties + && properties[0] is IPropertySymbol { SetMethod: { } setter } property + && property.Name == nameEqualsArgumentIdentifier.ValueText + && setter.Parameters is { Length: 1 } parameters) + { + parameter = parameters[0]; + return parameter != null; + } + var index = argumentList.IndexOf(arg); if (index >= methodSymbol.Parameters.Length) { 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..691fccd214d --- /dev/null +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/SyntaxNodeExtensions.Roslyn.cs @@ -0,0 +1,82 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.CodeDom.Compiler; + +namespace SonarAnalyzer.Helpers; + +[GeneratedCode("Copied from Roslyn", "5a1cc5f83e4baba57f0355a685a5d1f487bfac66")] +internal static partial 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. + // Copy of + // https://github.com/dotnet/roslyn/blob/5a1cc5f83e4baba57f0355a685a5d1f487bfac66/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/SyntaxNodeExtensions.cs#L142 + 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/Helpers/SyntaxNodeExtensions.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/SyntaxNodeExtensions.cs index 51209ee6981..057e1e00a5f 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/SyntaxNodeExtensions.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/SyntaxNodeExtensions.cs @@ -20,7 +20,7 @@ namespace SonarAnalyzer.Helpers; -internal static class SyntaxNodeExtensions +internal static partial class SyntaxNodeExtensions { public static SemanticModel EnsureCorrectSemanticModelOrDefault(this SyntaxNode node, SemanticModel semanticModel) => node.SyntaxTree.GetSemanticModelOrDefault(semanticModel); 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..180d4211e22 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.Common/Trackers/ArgumentTracker.cs @@ -0,0 +1,86 @@ +/* + * 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 InvocationFitsMemberKind(SyntaxNode invokedExpression, InvokedMemberKind memberKind); + protected abstract bool InvokedMemberFits(SemanticModel model, SyntaxNode invokedExpression, InvokedMemberKind memberKind, Func invokedMemberNameConstraint); + + protected override ArgumentContext CreateContext(SonarSyntaxNodeReportingContext context) => + new(context); + + public Condition MatchArgument(ArgumentDescriptor descriptor) => + context => + { + if (context.Node is { } argumentNode + && argumentNode is { Parent.Parent: { } invoked } + && SyntacticChecks(context.SemanticModel, descriptor, argumentNode, invoked) + && (descriptor.InvokedMemberNodeConstraint?.Invoke(context.SemanticModel, Language, invoked) ?? true) + && MethodSymbol(context.SemanticModel, invoked) is { } methodSymbol + && Language.MethodParameterLookup(invoked, methodSymbol).TryGetSymbol(argumentNode, out var parameter) + && ParameterFits(parameter, descriptor.ParameterConstraint, descriptor.InvokedMemberConstraint)) + { + context.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, + }; + + private bool SyntacticChecks(SemanticModel model, ArgumentDescriptor descriptor, SyntaxNode argumentNode, SyntaxNode invokedExpression) => + InvocationFitsMemberKind(invokedExpression, descriptor.MemberKind) + && (descriptor.RefKind is not { } expectedRefKind || ArgumentRefKind(argumentNode) is not { } actualRefKind || actualRefKind == expectedRefKind) + && (descriptor.ArgumentListConstraint == null + || (ArgumentList(argumentNode) is { } argList && descriptor.ArgumentListConstraint(argList, Position(argumentNode)))) + && (descriptor.InvokedMemberNameConstraint == null + || InvokedMemberFits(model, invokedExpression, descriptor.MemberKind, x => descriptor.InvokedMemberNameConstraint(x, Language.NameComparison))); + + private static bool ParameterFits(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) != null); + } + return false; + } +} diff --git a/analyzers/src/SonarAnalyzer.Common/Trackers/InvocationTracker.cs b/analyzers/src/SonarAnalyzer.Common/Trackers/InvocationTracker.cs index d95f82a9f3e..f59f4198967 100644 --- a/analyzers/src/SonarAnalyzer.Common/Trackers/InvocationTracker.cs +++ b/analyzers/src/SonarAnalyzer.Common/Trackers/InvocationTracker.cs @@ -51,18 +51,6 @@ public Condition MethodHasParameters(int count) => public Condition IsInvalidBuilderInitialization(BuilderPatternCondition condition) where TInvocationSyntax : SyntaxNode => condition.IsInvalidBuilderInitialization; - public Condition ExceptWhen(Condition condition) => - value => !condition(value); - - public Condition And(Condition condition1, Condition condition2) => - value => condition1(value) && condition2(value); - - public Condition Or(Condition condition1, Condition condition2) => - value => condition1(value) || condition2(value); - - public Condition Or(Condition condition1, Condition condition2, Condition condition3) => - value => condition1(value) || condition2(value) || condition3(value); - internal Condition MethodReturnTypeIs(KnownType returnType) => context => context.MethodSymbol.Value != null && context.MethodSymbol.Value.ReturnType.DerivesFrom(returnType); diff --git a/analyzers/src/SonarAnalyzer.Common/Trackers/ObjectCreationTracker.cs b/analyzers/src/SonarAnalyzer.Common/Trackers/ObjectCreationTracker.cs index acbe8a90b4e..5592eb03377 100644 --- a/analyzers/src/SonarAnalyzer.Common/Trackers/ObjectCreationTracker.cs +++ b/analyzers/src/SonarAnalyzer.Common/Trackers/ObjectCreationTracker.cs @@ -28,18 +28,6 @@ public abstract class ObjectCreationTracker : SyntaxTrackerBase - value => !condition(value); - - public Condition And(Condition condition1, Condition condition2) => - value => condition1(value) && condition2(value); - - public Condition Or(Condition condition1, Condition condition2) => - value => condition1(value) || condition2(value); - - public Condition Or(Condition condition1, Condition condition2, Condition condition3) => - value => condition1(value) || condition2(value) || condition3(value); - internal Condition ArgumentIsBoolConstant(string parameterName, bool expectedValue) => context => ConstArgumentForParameter(context, parameterName) is bool boolValue && boolValue == expectedValue; diff --git a/analyzers/src/SonarAnalyzer.Common/Trackers/PropertyAccessContext.cs b/analyzers/src/SonarAnalyzer.Common/Trackers/PropertyAccessContext.cs index a31699a9d90..c331e3d7f1d 100644 --- a/analyzers/src/SonarAnalyzer.Common/Trackers/PropertyAccessContext.cs +++ b/analyzers/src/SonarAnalyzer.Common/Trackers/PropertyAccessContext.cs @@ -30,5 +30,11 @@ public PropertyAccessContext(SonarSyntaxNodeReportingContext context, string pro PropertyName = propertyName; PropertySymbol = new Lazy(() => context.SemanticModel.GetSymbolInfo(context.Node).Symbol as IPropertySymbol); } + + public PropertyAccessContext(SyntaxNode node, SemanticModel semanticModel, string propertyName) : base(node, semanticModel) + { + PropertyName = propertyName; + PropertySymbol = new Lazy(() => semanticModel.GetSymbolInfo(node).Symbol as IPropertySymbol); + } } } diff --git a/analyzers/src/SonarAnalyzer.Common/Trackers/PropertyAccessTracker.cs b/analyzers/src/SonarAnalyzer.Common/Trackers/PropertyAccessTracker.cs index 60c72d2c12a..a04c1af3b5c 100644 --- a/analyzers/src/SonarAnalyzer.Common/Trackers/PropertyAccessTracker.cs +++ b/analyzers/src/SonarAnalyzer.Common/Trackers/PropertyAccessTracker.cs @@ -35,18 +35,6 @@ public Condition MatchProperty(params MemberDescriptor[] properties) => public Condition MatchProperty(bool checkOverridenProperties, params MemberDescriptor[] properties) => context => MemberDescriptor.MatchesAny(context.PropertyName, context.PropertySymbol, checkOverridenProperties, Language.NameComparison, properties); - public Condition ExceptWhen(Condition condition) => - value => !condition(value); - - public Condition And(Condition condition1, Condition condition2) => - value => condition1(value) && condition2(value); - - public Condition Or(Condition condition1, Condition condition2) => - value => condition1(value) || condition2(value); - - public Condition Or(Condition condition1, Condition condition2, Condition condition3) => - value => condition1(value) || condition2(value) || condition3(value); - protected override PropertyAccessContext CreateContext(SonarSyntaxNodeReportingContext context) { // We register for both MemberAccess and IdentifierName and we want to diff --git a/analyzers/src/SonarAnalyzer.Common/Trackers/SyntaxTrackerBase.cs b/analyzers/src/SonarAnalyzer.Common/Trackers/SyntaxTrackerBase.cs index 4d64f815aab..b7f63edf513 100644 --- a/analyzers/src/SonarAnalyzer.Common/Trackers/SyntaxTrackerBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Trackers/SyntaxTrackerBase.cs @@ -59,5 +59,17 @@ void TrackAndReportIfNecessary(SonarSyntaxNodeReportingContext c) } } } + + public Condition ExceptWhen(Condition condition) => + value => !condition(value); + + public Condition And(Condition condition1, Condition condition2) => + value => condition1(value) && condition2(value); + + public Condition Or(Condition condition1, Condition condition2) => + value => condition1(value) || condition2(value); + + public Condition Or(Condition condition1, Condition condition2, Condition condition3) => + value => condition1(value) || condition2(value) || condition3(value); } } diff --git a/analyzers/src/SonarAnalyzer.RuleDescriptorGenerator/packages.lock.json b/analyzers/src/SonarAnalyzer.RuleDescriptorGenerator/packages.lock.json deleted file mode 100644 index 29e4b65ee7c..00000000000 --- a/analyzers/src/SonarAnalyzer.RuleDescriptorGenerator/packages.lock.json +++ /dev/null @@ -1,938 +0,0 @@ -{ - "version": 1, - "dependencies": { - "net6.0": { - "Microsoft.CodeAnalysis.CSharp.Workspaces": { - "type": "Direct", - "requested": "[1.3.2, )", - "resolved": "1.3.2", - "contentHash": "MwGmrrPx3okEJuCogSn4TM3yTtJUDdmTt8RXpnjVo0dPund0YSAq4bHQQ9bxgArbrrapcopJmkb7UOLAvanXkg==", - "dependencies": { - "Microsoft.CodeAnalysis.CSharp": "[1.3.2]", - "Microsoft.CodeAnalysis.Workspaces.Common": "[1.3.2]" - } - }, - "Microsoft.CodeAnalysis.VisualBasic.Workspaces": { - "type": "Direct", - "requested": "[1.3.2, )", - "resolved": "1.3.2", - "contentHash": "I5Z2WBgFsx0G22Na1uVFPDkT6Ob4XI+g91GPN8JWldYUMlmIBcUDBfGmfr8oQPdUipvThpaU1x1xZrnNwRR8JA==", - "dependencies": { - "Microsoft.CodeAnalysis.VisualBasic": "[1.3.2]", - "Microsoft.CodeAnalysis.Workspaces.Common": "[1.3.2]" - } - }, - "StyleCop.Analyzers": { - "type": "Direct", - "requested": "[1.2.0-beta.556, )", - "resolved": "1.2.0-beta.556", - "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", - "dependencies": { - "StyleCop.Analyzers.Unstable": "1.2.0.556" - } - }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "741fGeDQjixBJaU2j+0CbrmZXsNJkTn/hWbOh4fLVXndHsCclJmWznCPWrJmPoZKvajBvAz3e8ECJOUvRtwjNQ==", - "dependencies": { - "NETStandard.Library": "1.6.1" - } - }, - "Google.Protobuf.Tools": { - "type": "Transitive", - "resolved": "3.6.1", - "contentHash": "mNgfZ1A7UtbZUOIA8+UcKOouKnbd2tu9CKctCvGXFunZGrViWk6QbNwSBc268Sle9Gwl+WQB+u6qQezp5f9y3w==" - }, - "Microsoft.CodeAnalysis.Analyzers": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "HS3iRWZKcUw/8eZ/08GXKY2Bn7xNzQPzf8gRPHGSowX7u7XXu9i9YEaBeBNKUXWfI7qjvT2zXtLUvbN0hds8vg==" - }, - "Microsoft.CodeAnalysis.Common": { - "type": "Transitive", - "resolved": "1.3.2", - "contentHash": "lOinFNbjpCvkeYQHutjKi+CfsjoKu88wAFT6hAumSR/XJSJmmVGvmnbzCWW8kUJnDVrw1RrcqS8BzgPMj263og==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "1.1.0", - "System.AppContext": "4.1.0", - "System.Collections": "4.0.11", - "System.Collections.Concurrent": "4.0.12", - "System.Collections.Immutable": "1.2.0", - "System.Console": "4.0.0", - "System.Diagnostics.Debug": "4.0.11", - "System.Diagnostics.FileVersionInfo": "4.0.0", - "System.Diagnostics.StackTrace": "4.0.1", - "System.Diagnostics.Tools": "4.0.1", - "System.Dynamic.Runtime": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO.FileSystem": "4.0.1", - "System.IO.FileSystem.Primitives": "4.0.1", - "System.Linq": "4.1.0", - "System.Linq.Expressions": "4.1.0", - "System.Reflection": "4.1.0", - "System.Reflection.Metadata": "1.3.0", - "System.Reflection.Primitives": "4.0.1", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Runtime.Numerics": "4.0.1", - "System.Security.Cryptography.Algorithms": "4.2.0", - "System.Security.Cryptography.Encoding": "4.0.0", - "System.Security.Cryptography.X509Certificates": "4.1.0", - "System.Text.Encoding": "4.0.11", - "System.Text.Encoding.CodePages": "4.0.1", - "System.Text.Encoding.Extensions": "4.0.11", - "System.Threading": "4.0.11", - "System.Threading.Tasks": "4.0.11", - "System.Threading.Tasks.Parallel": "4.0.1", - "System.Threading.Thread": "4.0.0", - "System.Xml.ReaderWriter": "4.0.11", - "System.Xml.XDocument": "4.0.11", - "System.Xml.XPath.XDocument": "4.0.1", - "System.Xml.XmlDocument": "4.0.1" - } - }, - "Microsoft.CodeAnalysis.CSharp": { - "type": "Transitive", - "resolved": "1.3.2", - "contentHash": "GrYMp6ScZDOMR0fNn/Ce6SegNVFw1G/QRT/8FiKv7lAP+V6lEZx9e42n0FvFUgjjcKgcEJOI4muU6i+3LSvOBA==", - "dependencies": { - "Microsoft.CodeAnalysis.Common": "[1.3.2]" - } - }, - "Microsoft.CodeAnalysis.VisualBasic": { - "type": "Transitive", - "resolved": "1.3.2", - "contentHash": "yllH3rSYEc0bV15CJ2T9Jtx+tSXO5/OVNb+xofuWrACn65Q5VqeFBKgcbgwpyVY/98ypPcGQIWNQL2A/L1seJg==", - "dependencies": { - "Microsoft.CodeAnalysis.Common": "1.3.2" - } - }, - "Microsoft.CodeAnalysis.Workspaces.Common": { - "type": "Transitive", - "resolved": "1.3.2", - "contentHash": "kvdo+rkImlx5MuBgkayl4OV3Mg8/qirUdYgCIfQ9EqN15QasJFlQXmDAtCGqpkK9sYLLO/VK+y+4mvKjfh/FOA==", - "dependencies": { - "Microsoft.CodeAnalysis.Common": "[1.3.2]", - "Microsoft.Composition": "1.0.27" - } - }, - "Microsoft.Composition": { - "type": "Transitive", - "resolved": "1.0.27", - "contentHash": "pwu80Ohe7SBzZ6i69LVdzowp6V+LaVRzd5F7A6QlD42vQkX0oT7KXKWWPlM/S00w1gnMQMRnEdbtOV12z6rXdQ==" - }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "1.0.1", - "contentHash": "2G6OjjJzwBfNOO8myRV/nFrbTw5iA+DEm0N+qUqhrOmaVtn4pC77h38I1jsXGw5VH55+dPfQsqHD0We9sCl9FQ==" - }, - "Microsoft.NETCore.Targets": { - "type": "Transitive", - "resolved": "1.0.1", - "contentHash": "rkn+fKobF/cbWfnnfBOQHKVKIOpxMZBvlSHkqDWgBpwGDcLRduvs3D9OLGeV6GWGvVwNlVi2CBbTjuPmtHvyNw==" - }, - "runtime.native.System": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "QfS/nQI7k/BLgmLrw7qm7YBoULEvgWnPI+cYsbfCVFTW8Aj+i8JhccxcFMu1RWms0YZzF+UHguNBK4Qn89e2Sg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1" - } - }, - "runtime.native.System.Net.Http": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "Nh0UPZx2Vifh8r+J+H2jxifZUD3sBrmolgiFWJd2yiNrxO0xTa6bAw3YwRn1VOiSen/tUXMS31ttNItCZ6lKuA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1" - } - }, - "runtime.native.System.Security.Cryptography": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "2CQK0jmO6Eu7ZeMgD+LOFbNJSXHFVQbCJJkEyEwowh1SCgYnrn9W9RykMfpeeVGw7h4IBvYikzpGUlmZTUafJw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1" - } - }, - "StyleCop.Analyzers.Unstable": { - "type": "Transitive", - "resolved": "1.2.0.556", - "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" - }, - "System.AppContext": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "3QjO4jNV7PdKkmQAVp9atA+usVnKRwI3Kx1nMwJ93T0LcQfx7pKAYk0nKz5wn1oP5iqlhZuy6RXOFdhr7rDwow==", - "dependencies": { - "System.Runtime": "4.1.0" - } - }, - "System.Collections": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "YUJGz6eFKqS0V//mLt25vFGrrCvOnsXjlvFQs+KimpwNxug9x0Pzy4PlFMU3Q2IzqAa9G2L4LsK3+9vCBK7oTg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Collections.Concurrent": { - "type": "Transitive", - "resolved": "4.0.12", - "contentHash": "2gBcbb3drMLgxlI0fBfxMA31ec6AEyYCHygGse4vxceJan8mRIWeKJ24BFzN7+bi/NFTgdIgufzb94LWO5EERQ==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Diagnostics.Tracing": "4.1.0", - "System.Globalization": "4.0.11", - "System.Reflection": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11", - "System.Threading.Tasks": "4.0.11" - } - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "1.2.0", - "contentHash": "Cma8cBW6di16ZLibL8LYQ+cLjGzoKxpOTu/faZfDcx94ZjAGq6Nv5RO7+T1YZXqEXTZP9rt1wLVEONVpURtUqw==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.Linq": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11" - } - }, - "System.Console": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "qSKUSOIiYA/a0g5XXdxFcUFmv1hNICBD7QZ0QhGYVipPIhvpiydY8VZqr1thmCXvmn8aipMg64zuanB4eotK9A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.IO": "4.1.0", - "System.Runtime": "4.1.0", - "System.Text.Encoding": "4.0.11" - } - }, - "System.Diagnostics.Debug": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "w5U95fVKHY4G8ASs/K5iK3J5LY+/dLFd4vKejsnI/ZhBsWS9hQakfx3Zr7lRWKg4tAw9r4iktyvsTagWkqYCiw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Diagnostics.FileVersionInfo": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "qjF74OTAU+mRhLaL4YSfiWy3vj6T3AOz8AW37l5zCwfbBfj0k7E94XnEsRaf2TnhE/7QaV6Hvqakoy2LoV8MVg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.IO.FileSystem": "4.0.1", - "System.IO.FileSystem.Primitives": "4.0.1", - "System.Reflection.Metadata": "1.3.0", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.InteropServices": "4.1.0" - } - }, - "System.Diagnostics.StackTrace": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "6i2EbRq0lgGfiZ+FDf0gVaw9qeEU+7IS2+wbZJmFVpvVzVOgZEt0ScZtyenuBvs6iDYbGiF51bMAa0oDP/tujQ==", - "dependencies": { - "System.Collections.Immutable": "1.2.0", - "System.IO.FileSystem": "4.0.1", - "System.Reflection": "4.1.0", - "System.Reflection.Metadata": "1.3.0", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0" - } - }, - "System.Diagnostics.Tools": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "xBfJ8pnd4C17dWaC9FM6aShzbJcRNMChUMD42I6772KGGrqaFdumwhn9OdM68erj1ueNo3xdQ1EwiFjK5k8p0g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Diagnostics.Tracing": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "vDN1PoMZCkkdNjvZLql592oYJZgS7URcJzJ7bxeBgGtx5UtR5leNm49VmfHGqIffX4FKacHbI3H6UyNSHQknBg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Dynamic.Runtime": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "db34f6LHYM0U0JpE+sOmjar27BnqTVkbLJhgfwMpTdgTigG/Hna3m2MYVwnFzGGKnEJk2UXFuoVTr8WUbU91/A==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.Linq": "4.1.0", - "System.Linq.Expressions": "4.1.0", - "System.ObjectModel": "4.0.12", - "System.Reflection": "4.1.0", - "System.Reflection.Emit": "4.0.1", - "System.Reflection.Emit.ILGeneration": "4.0.1", - "System.Reflection.Primitives": "4.0.1", - "System.Reflection.TypeExtensions": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11" - } - }, - "System.Globalization": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "B95h0YLEL2oSnwF/XjqSWKnwKOy/01VWkNlsCeMTFJLLabflpGV26nK164eRs5GiaRSBGpOxQ3pKoSnnyZN5pg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Globalization.Calendars": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "L1c6IqeQ88vuzC1P81JeHmHA8mxq8a18NUBNXnIY/BVb+TCyAaGIFbhpZt60h9FJNmisymoQkHEFSE9Vslja1Q==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Globalization": "4.0.11", - "System.Runtime": "4.1.0" - } - }, - "System.IO": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "3KlTJceQc3gnGIaHZ7UBZO26SHL1SHE4ddrmiwumFnId+CEHP+O8r386tZKaE6zlk5/mF8vifMBzHj9SaXN+mQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0", - "System.Text.Encoding": "4.0.11", - "System.Threading.Tasks": "4.0.11" - } - }, - "System.IO.FileSystem": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "IBErlVq5jOggAD69bg1t0pJcHaDbJbWNUZTPI96fkYWzwYbN6D9wRHMULLDd9dHsl7C2YsxXL31LMfPI1SWt8w==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.IO": "4.1.0", - "System.IO.FileSystem.Primitives": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Text.Encoding": "4.0.11", - "System.Threading.Tasks": "4.0.11" - } - }, - "System.IO.FileSystem.Primitives": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "kWkKD203JJKxJeE74p8aF8y4Qc9r9WQx4C0cHzHPrY3fv/L/IhWnyCHaFJ3H1QPOH6A93whlQ2vG5nHlBDvzWQ==", - "dependencies": { - "System.Runtime": "4.1.0" - } - }, - "System.Linq": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "bQ0iYFOQI0nuTnt+NQADns6ucV4DUvMdwN6CbkB1yj8i7arTGiTN5eok1kQwdnnNWSDZfIUySQY+J3d5KjWn0g==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0" - } - }, - "System.Linq.Expressions": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "I+y02iqkgmCAyfbqOmSDOgqdZQ5tTj80Akm5BPSS8EeB0VGWdy6X1KCoYe8Pk6pwDoAKZUOdLVxnTJcExiv5zw==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.Linq": "4.1.0", - "System.ObjectModel": "4.0.12", - "System.Reflection": "4.1.0", - "System.Reflection.Emit": "4.0.1", - "System.Reflection.Emit.ILGeneration": "4.0.1", - "System.Reflection.Emit.Lightweight": "4.0.1", - "System.Reflection.Extensions": "4.0.1", - "System.Reflection.Primitives": "4.0.1", - "System.Reflection.TypeExtensions": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11" - } - }, - "System.ObjectModel": { - "type": "Transitive", - "resolved": "4.0.12", - "contentHash": "tAgJM1xt3ytyMoW4qn4wIqgJYm7L7TShRZG4+Q4Qsi2PCcj96pXN7nRywS9KkB3p/xDUjc2HSwP9SROyPYDYKQ==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Threading": "4.0.11" - } - }, - "System.Reflection": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "JCKANJ0TI7kzoQzuwB/OoJANy1Lg338B6+JVacPl4TpUwi3cReg3nMLplMq2uqYfHFQpKIlHAUVAJlImZz/4ng==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.IO": "4.1.0", - "System.Reflection.Primitives": "4.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Reflection.Emit": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "P2wqAj72fFjpP6wb9nSfDqNBMab+2ovzSDzUZK7MVIm54tBJEPr9jWfSjjoTpPwj1LeKcmX3vr0ttyjSSFM47g==", - "dependencies": { - "System.IO": "4.1.0", - "System.Reflection": "4.1.0", - "System.Reflection.Emit.ILGeneration": "4.0.1", - "System.Reflection.Primitives": "4.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Reflection.Emit.ILGeneration": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "Ov6dU8Bu15Bc7zuqttgHF12J5lwSWyTf1S+FJouUXVMSqImLZzYaQ+vRr1rQ0OZ0HqsrwWl4dsKHELckQkVpgA==", - "dependencies": { - "System.Reflection": "4.1.0", - "System.Reflection.Primitives": "4.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Reflection.Emit.Lightweight": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "sSzHHXueZ5Uh0OLpUQprhr+ZYJrLPA2Cmr4gn0wj9+FftNKXx8RIMKvO9qnjk2ebPYUjZ+F2ulGdPOsvj+MEjA==", - "dependencies": { - "System.Reflection": "4.1.0", - "System.Reflection.Emit.ILGeneration": "4.0.1", - "System.Reflection.Primitives": "4.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Reflection.Extensions": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "GYrtRsZcMuHF3sbmRHfMYpvxZoIN2bQGrYGerUiWLEkqdEUQZhH3TRSaC/oI4wO0II1RKBPlpIa1TOMxIcOOzQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Reflection": "4.1.0", - "System.Runtime": "4.1.0" - } - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "1.3.0", - "contentHash": "jMSCxA4LSyKBGRDm/WtfkO03FkcgRzHxwvQRib1bm2GZ8ifKM1MX1al6breGCEQK280mdl9uQS7JNPXRYk90jw==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Collections.Immutable": "1.2.0", - "System.Diagnostics.Debug": "4.0.11", - "System.IO": "4.1.0", - "System.Linq": "4.1.0", - "System.Reflection": "4.1.0", - "System.Reflection.Extensions": "4.0.1", - "System.Reflection.Primitives": "4.0.1", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.InteropServices": "4.1.0", - "System.Text.Encoding": "4.0.11", - "System.Text.Encoding.Extensions": "4.0.11", - "System.Threading": "4.0.11" - } - }, - "System.Reflection.Primitives": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "4inTox4wTBaDhB7V3mPvp9XlCbeGYWVEM9/fXALd52vNEAVisc1BoVWQPuUuD0Ga//dNbA/WeMy9u9mzLxGTHQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Reflection.TypeExtensions": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "tsQ/ptQ3H5FYfON8lL4MxRk/8kFyE0A+tGPXmVP967cT/gzLHYxIejIYSxp4JmIeFHVP78g/F2FE1mUUTbDtrg==", - "dependencies": { - "System.Reflection": "4.1.0", - "System.Runtime": "4.1.0" - } - }, - "System.Resources.ResourceManager": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "TxwVeUNoTgUOdQ09gfTjvW411MF+w9MBYL7AtNVc+HtBCFlutPLhUCdZjNkjbhj3bNQWMdHboF0KIWEOjJssbA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Globalization": "4.0.11", - "System.Reflection": "4.1.0", - "System.Runtime": "4.1.0" - } - }, - "System.Runtime": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "v6c/4Yaa9uWsq+JMhnOFewrYkgdNHNG2eMKuNqRn8P733rNXeRCGvV5FkkjBXn2dbVkPXOsO0xjsEeM1q2zC0g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1" - } - }, - "System.Runtime.Extensions": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "CUOHjTT/vgP0qGW22U4/hDlOqXmcPq5YicBaXdUR2UiUoLwBT+olO6we4DVbq57jeX5uXH2uerVZhf0qGj+sVQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Runtime.Handles": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "nCJvEKguXEvk2ymk1gqj625vVnlK3/xdGzx0vOKicQkoquaTBJTP13AIYkocSUwHCLNBwUbXTqTWGDxBTWpt7g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Runtime.InteropServices": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "16eu3kjHS633yYdkjwShDHZLRNMKVi/s0bY8ODiqJ2RfMhDMAwxZaUaWVnZ2P71kr/or+X9o/xFWtNqz8ivieQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Reflection": "4.1.0", - "System.Reflection.Primitives": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Handles": "4.0.1" - } - }, - "System.Runtime.Numerics": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "+XbKFuzdmLP3d1o9pdHu2nxjNr2OEPqGzKeegPLCUMM71a0t50A/rOcIRmGs9wR7a8KuHX6hYs/7/TymIGLNqg==", - "dependencies": { - "System.Globalization": "4.0.11", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0" - } - }, - "System.Security.Cryptography.Algorithms": { - "type": "Transitive", - "resolved": "4.2.0", - "contentHash": "8JQFxbLVdrtIOKMDN38Fn0GWnqYZw/oMlwOUG/qz1jqChvyZlnUmu+0s7wLx7JYua/nAXoESpHA3iw11QFWhXg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "System.Collections": "4.0.11", - "System.IO": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Runtime.Numerics": "4.0.1", - "System.Security.Cryptography.Encoding": "4.0.0", - "System.Security.Cryptography.Primitives": "4.0.0", - "System.Text.Encoding": "4.0.11", - "runtime.native.System.Security.Cryptography": "4.0.0" - } - }, - "System.Security.Cryptography.Cng": { - "type": "Transitive", - "resolved": "4.2.0", - "contentHash": "cUJ2h+ZvONDe28Szw3st5dOHdjndhJzQ2WObDEXAWRPEQBtVItVoxbXM/OEsTthl3cNn2dk2k0I3y45igCQcLw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "System.IO": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Security.Cryptography.Algorithms": "4.2.0", - "System.Security.Cryptography.Encoding": "4.0.0", - "System.Security.Cryptography.Primitives": "4.0.0", - "System.Text.Encoding": "4.0.11" - } - }, - "System.Security.Cryptography.Csp": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "/i1Usuo4PgAqgbPNC0NjbO3jPW//BoBlTpcWFD1EHVbidH21y4c1ap5bbEMSGAXjAShhMH4abi/K8fILrnu4BQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "System.IO": "4.1.0", - "System.Reflection": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Security.Cryptography.Algorithms": "4.2.0", - "System.Security.Cryptography.Encoding": "4.0.0", - "System.Security.Cryptography.Primitives": "4.0.0", - "System.Text.Encoding": "4.0.11", - "System.Threading": "4.0.11" - } - }, - "System.Security.Cryptography.Encoding": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "FbKgE5MbxSQMPcSVRgwM6bXN3GtyAh04NkV8E5zKCBE26X0vYW0UtTa2FIgkH33WVqBVxRgxljlVYumWtU+HcQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "System.Collections": "4.0.11", - "System.Collections.Concurrent": "4.0.12", - "System.Linq": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Security.Cryptography.Primitives": "4.0.0", - "System.Text.Encoding": "4.0.11", - "runtime.native.System.Security.Cryptography": "4.0.0" - } - }, - "System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "HUG/zNUJwEiLkoURDixzkzZdB5yGA5pQhDP93ArOpDPQMteURIGERRNzzoJlmTreLBWr5lkFSjjMSk8ySEpQMw==", - "dependencies": { - "System.Collections": "4.0.11", - "System.IO": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Runtime.Numerics": "4.0.1", - "System.Security.Cryptography.Algorithms": "4.2.0", - "System.Security.Cryptography.Encoding": "4.0.0", - "System.Security.Cryptography.Primitives": "4.0.0", - "System.Text.Encoding": "4.0.11", - "runtime.native.System.Security.Cryptography": "4.0.0" - } - }, - "System.Security.Cryptography.Primitives": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "Wkd7QryWYjkQclX0bngpntW5HSlMzeJU24UaLJQ7YTfI8ydAVAaU2J+HXLLABOVJlKTVvAeL0Aj39VeTe7L+oA==", - "dependencies": { - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Threading": "4.0.11", - "System.Threading.Tasks": "4.0.11" - } - }, - "System.Security.Cryptography.X509Certificates": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "4HEfsQIKAhA1+ApNn729Gi09zh+lYWwyIuViihoMDWp1vQnEkL2ct7mAbhBlLYm+x/L4Rr/pyGge1lIY635e0w==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.Globalization.Calendars": "4.0.1", - "System.IO": "4.1.0", - "System.IO.FileSystem": "4.0.1", - "System.IO.FileSystem.Primitives": "4.0.1", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Runtime.Numerics": "4.0.1", - "System.Security.Cryptography.Algorithms": "4.2.0", - "System.Security.Cryptography.Cng": "4.2.0", - "System.Security.Cryptography.Csp": "4.0.0", - "System.Security.Cryptography.Encoding": "4.0.0", - "System.Security.Cryptography.OpenSsl": "4.0.0", - "System.Security.Cryptography.Primitives": "4.0.0", - "System.Text.Encoding": "4.0.11", - "System.Threading": "4.0.11", - "runtime.native.System": "4.0.0", - "runtime.native.System.Net.Http": "4.0.1", - "runtime.native.System.Security.Cryptography": "4.0.0" - } - }, - "System.Text.Encoding": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "U3gGeMlDZXxCEiY4DwVLSacg+DFWCvoiX+JThA/rvw37Sqrku7sEFeVBBBMBnfB6FeZHsyDx85HlKL19x0HtZA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Text.Encoding.CodePages": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "h4z6rrA/hxWf4655D18IIZ0eaLRa3tQC/j+e26W+VinIHY0l07iEXaAvO0YSYq3MvCjMYy8Zs5AdC1sxNQOB7Q==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "System.Collections": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.Reflection": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.Handles": "4.0.1", - "System.Runtime.InteropServices": "4.1.0", - "System.Text.Encoding": "4.0.11", - "System.Threading": "4.0.11" - } - }, - "System.Text.Encoding.Extensions": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "jtbiTDtvfLYgXn8PTfWI+SiBs51rrmO4AAckx4KR6vFK9Wzf6tI8kcRdsYQNwriUeQ1+CtQbM1W4cMbLXnj/OQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0", - "System.Text.Encoding": "4.0.11" - } - }, - "System.Text.RegularExpressions": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "i88YCXpRTjCnoSQZtdlHkAOx4KNNik4hMy83n0+Ftlb7jvV6ZiZWMpnEZHhjBp6hQVh8gWd/iKNPzlPF7iyA2g==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Globalization": "4.0.11", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11" - } - }, - "System.Threading": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "N+3xqIcg3VDKyjwwCGaZ9HawG9aC6cSDI+s7ROma310GQo8vilFZa86hqKppwTHleR/G0sfOzhvgnUxWCR/DrQ==", - "dependencies": { - "System.Runtime": "4.1.0", - "System.Threading.Tasks": "4.0.11" - } - }, - "System.Threading.Tasks": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "k1S4Gc6IGwtHGT8188RSeGaX86Qw/wnrgNLshJvsdNUOPP9etMmo8S07c+UlOAx4K/xLuN9ivA1bD0LVurtIxQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1", - "System.Runtime": "4.1.0" - } - }, - "System.Threading.Tasks.Extensions": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "pH4FZDsZQ/WmgJtN4LWYmRdJAEeVkyriSwrv2Teoe5FOU0Yxlb6II6GL8dBPOfRmutHGATduj3ooMt7dJ2+i+w==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Runtime": "4.1.0", - "System.Threading.Tasks": "4.0.11" - } - }, - "System.Threading.Tasks.Parallel": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "7Pc9t25bcynT9FpMvkUw4ZjYwUiGup/5cJFW72/5MgCG+np2cfVUMdh29u8d7onxX7d8PS3J+wL73zQRqkdrSA==", - "dependencies": { - "System.Collections.Concurrent": "4.0.12", - "System.Diagnostics.Debug": "4.0.11", - "System.Diagnostics.Tracing": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11", - "System.Threading.Tasks": "4.0.11" - } - }, - "System.Threading.Thread": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "gIdJqDXlOr5W9zeqFErLw3dsOsiShSCYtF9SEHitACycmvNvY8odf9kiKvp6V7aibc8C4HzzNBkWXjyfn7plbQ==", - "dependencies": { - "System.Runtime": "4.1.0" - } - }, - "System.Xml.ReaderWriter": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "ZIiLPsf67YZ9zgr31vzrFaYQqxRPX9cVHjtPSnmx4eN6lbS/yEyYNr2vs1doGDEscF0tjCZFsk9yUg1sC9e8tg==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.IO.FileSystem": "4.0.1", - "System.IO.FileSystem.Primitives": "4.0.1", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Runtime.InteropServices": "4.1.0", - "System.Text.Encoding": "4.0.11", - "System.Text.Encoding.Extensions": "4.0.11", - "System.Text.RegularExpressions": "4.1.0", - "System.Threading.Tasks": "4.0.11", - "System.Threading.Tasks.Extensions": "4.0.0" - } - }, - "System.Xml.XDocument": { - "type": "Transitive", - "resolved": "4.0.11", - "contentHash": "Mk2mKmPi0nWaoiYeotq1dgeNK1fqWh61+EK+w4Wu8SWuTYLzpUnschb59bJtGywaPq7SmTuPf44wrXRwbIrukg==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Diagnostics.Tools": "4.0.1", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.Reflection": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Text.Encoding": "4.0.11", - "System.Threading": "4.0.11", - "System.Xml.ReaderWriter": "4.0.11" - } - }, - "System.Xml.XmlDocument": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "2eZu6IP+etFVBBFUFzw2w6J21DqIN5eL9Y8r8JfJWUmV28Z5P0SNU01oCisVHQgHsDhHPnmq2s1hJrJCFZWloQ==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Text.Encoding": "4.0.11", - "System.Threading": "4.0.11", - "System.Xml.ReaderWriter": "4.0.11" - } - }, - "System.Xml.XPath": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "UWd1H+1IJ9Wlq5nognZ/XJdyj8qPE4XufBUkAW59ijsCPjZkZe0MUzKKJFBr+ZWBe5Wq1u1d5f2CYgE93uH7DA==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Diagnostics.Debug": "4.0.11", - "System.Globalization": "4.0.11", - "System.IO": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11", - "System.Xml.ReaderWriter": "4.0.11" - } - }, - "System.Xml.XPath.XDocument": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "FLhdYJx4331oGovQypQ8JIw2kEmNzCsjVOVYY/16kZTUoquZG85oVn7yUhBE2OZt1yGPSXAL0HTEfzjlbNpM7Q==", - "dependencies": { - "System.Diagnostics.Debug": "4.0.11", - "System.Linq": "4.1.0", - "System.Resources.ResourceManager": "4.0.1", - "System.Runtime": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "System.Threading": "4.0.11", - "System.Xml.ReaderWriter": "4.0.11", - "System.Xml.XDocument": "4.0.11", - "System.Xml.XPath": "4.0.1" - } - }, - "SonarAnalyzer": { - "type": "Project", - "dependencies": { - "Google.Protobuf": "[3.6.1, )", - "Google.Protobuf.Tools": "[3.6.1, )", - "Microsoft.CodeAnalysis.Workspaces.Common": "[1.3.2, )", - "Microsoft.Composition": "[1.0.27, )", - "SonarAnalyzer.CFG": "[1.0.0, )", - "System.Collections.Immutable": "[1.1.37, )" - } - }, - "sonaranalyzer.cfg": { - "type": "Project", - "dependencies": { - "Microsoft.CodeAnalysis.CSharp.Workspaces": "[1.3.2, )", - "Microsoft.Composition": "[1.0.27, )", - "System.Collections.Immutable": "[1.1.37, )" - } - } - } - } -} \ No newline at end of file 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..3d1668222e7 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/ExpressionSyntaxExtensions.Roslyn.cs @@ -0,0 +1,175 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.CodeDom.Compiler; + +namespace SonarAnalyzer.Extensions +{ + [GeneratedCode("Copied and converted from Roslyn", "5a1cc5f83e4baba57f0355a685a5d1f487bfac66")] + internal static class ExpressionSyntaxExtensions + { + // Copied and converted from + // https://github.com/dotnet/roslyn/blob/5a1cc5f83e4baba57f0355a685a5d1f487bfac66/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb#L362 + 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; + } + + // copied and converted from + // 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; + } + + // Copied and converted from + // 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(); + } + + // Copied and converted from + // 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; + } + + // Copied and converted from + // 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; + } + + // Copied and converted from + // 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..d11a30a5a72 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/SyntaxNodeExtensions.Roslyn.cs @@ -0,0 +1,34 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.CodeDom.Compiler; + +namespace SonarAnalyzer.Extensions; + +[GeneratedCode("Copied and converted from Roslyn", "5a1cc5f83e4baba57f0355a685a5d1f487bfac66")] +internal static partial 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/SyntaxNodeExtensions.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/SyntaxNodeExtensions.cs index 0dc09b66547..3e555ee89ff 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/SyntaxNodeExtensions.cs +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Extensions/SyntaxNodeExtensions.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 @@ public object FindConstantValue(SemanticModel model, SyntaxNode node) => 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..d7a3764030a 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxFacade.cs +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxFacade.cs @@ -108,6 +108,9 @@ public override bool IsMemberAccessOnKnownType(SyntaxNode memberAccess, string n public override bool IsStatic(SyntaxNode node) => Cast(node).IsShared(); + public override bool IsWrittenTo(SyntaxNode expression, SemanticModel semanticModel, CancellationToken cancellationToken) => + expression is ExpressionSyntax ex && ex.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..f573cc99a33 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicTrackerFacade.cs +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicTrackerFacade.cs @@ -31,5 +31,6 @@ internal sealed class VisualBasicTrackerFacade : ITrackerFacade public MethodDeclarationTracker MethodDeclaration { get; } = new VisualBasicMethodDeclarationTracker(); public ObjectCreationTracker ObjectCreation { get; } = new VisualBasicObjectCreationTracker(); public PropertyAccessTracker PropertyAccess { get; } = new VisualBasicPropertyAccessTracker(); + public ArgumentTracker Argument => new VisualBasicArgumentTracker(); } } diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicAttributeParameterLookup.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicAttributeParameterLookup.cs new file mode 100644 index 00000000000..60979bf8a55 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicAttributeParameterLookup.cs @@ -0,0 +1,39 @@ +/* + * 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 : MethodParameterLookupBase +{ + public VisualBasicAttributeParameterLookup(SeparatedSyntaxList argumentList, IMethodSymbol methodSymbol) : base(argumentList, methodSymbol) + { + } + + protected override SyntaxNode Expression(ArgumentSyntax argument) => + argument.GetExpression(); + + protected override SyntaxToken? GetNameColonArgumentIdentifier(ArgumentSyntax argument) => + null; + + protected override SyntaxToken? GetNameEqualsArgumentIdentifier(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..c1acde9d473 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicMethodParameterLookup.cs +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Helpers/VisualBasicMethodParameterLookup.cs @@ -39,4 +39,7 @@ public VisualBasicMethodParameterLookup(ArgumentListSyntax argumentList, IMethod protected override SyntaxNode Expression(ArgumentSyntax argument) => argument.GetExpression(); + + protected override SyntaxToken? GetNameEqualsArgumentIdentifier(ArgumentSyntax argument) => + null; } diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Trackers/VisualBasicArgumentTracker.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Trackers/VisualBasicArgumentTracker.cs new file mode 100644 index 00000000000..dabc2664fe6 --- /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 InvocationFitsMemberKind(SyntaxNode invokedExpression, InvokedMemberKind memberKind) => + memberKind switch + { + InvokedMemberKind.Method => invokedExpression is InvocationExpressionSyntax, + InvokedMemberKind.Constructor => invokedExpression is ObjectCreationExpressionSyntax, + InvokedMemberKind.Indexer => invokedExpression is InvocationExpressionSyntax, + InvokedMemberKind.Attribute => invokedExpression is AttributeSyntax, + _ => false, + }; + + protected override bool InvokedMemberFits(SemanticModel model, SyntaxNode invokedExpression, InvokedMemberKind memberKind, Func invokedMemberNameConstraint) => + invokedMemberNameConstraint(invokedExpression.GetName()); +} 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/PackagingTests/RuleTypeMappingCS.cs b/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs index b09c62fd2f3..cc4034c6cdc 100644 --- a/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs +++ b/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs @@ -6856,7 +6856,7 @@ internal static class RuleTypeMappingCS // ["S6929"], ["S6930"] = "BUG", // ["S6931"], - // ["S6932"], + ["S6932"] = "CODE_SMELL", // ["S6933"], // ["S6934"], // ["S6935"], diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/UseAspNetModelBindingTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/UseAspNetModelBindingTest.cs new file mode 100644 index 00000000000..b9bc1410de5 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/UseAspNetModelBindingTest.cs @@ -0,0 +1,291 @@ +/* + * 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 Microsoft.VisualStudio.TestTools.UnitTesting; +using SonarAnalyzer.Rules.CSharp; + +namespace SonarAnalyzer.Test.Rules; + +[TestClass] +public class UseAspNetModelBindingTest +{ +#if NET + private readonly VerifierBuilder builderAspNetCore = new VerifierBuilder() + .WithOptions(ParseOptionsHelper.FromCSharp12) + .AddReferences([ + AspNetCoreMetadataReference.MicrosoftAspNetCoreMvcCore, + AspNetCoreMetadataReference.MicrosoftAspNetCoreHttpAbstractions, + AspNetCoreMetadataReference.MicrosoftAspNetCoreMvcViewFeatures, + AspNetCoreMetadataReference.MicrosoftAspNetCoreMvcAbstractions, + AspNetCoreMetadataReference.MicrosoftAspNetCoreHttpFeatures, + AspNetCoreMetadataReference.MicrosoftExtensionsPrimitives, + ]); + + [TestMethod] + public void UseAspNetModelBinding_NoRegistrationIfNotAspNet() => + new VerifierBuilder().AddSnippet(string.Empty).Verify(); + + [TestMethod] + public void UseAspNetModelBinding_AspNetCore_CSharp12() => + builderAspNetCore.AddPaths("UseAspNetModelBinding_AspNetCore.cs").Verify(); + + [DataTestMethod] + [DataRow("Form")] + [DataRow("Query")] + [DataRow("RouteValues")] + [DataRow("Headers")] + public void UseAspNetModelBinding_NonCompliantAccess(string property) => + builderAspNetCore.AddSnippet($$"""" + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Filters; + using System; + using System.Linq; + using System.Threading.Tasks; + + public class TestController : Controller + { + async Task NoncompliantKeyVariations() + { + _ = Request.{{property}}[@"key"]; // Noncompliant + _ = Request.{{property}}.TryGetValue(@"key", out _); // Noncompliant + _ = Request.{{property}}["""key"""]; // Noncompliant + _ = Request.{{property}}.TryGetValue("""key""", out _); // Noncompliant + + const string key = "id"; + _ = Request.{{property}}[key]; // Noncompliant + _ = Request.{{property}}.TryGetValue(key, out _); // Noncompliant + _ = Request.{{property}}[$"prefix.{key}"]; // Noncompliant + _ = Request.{{property}}.TryGetValue($"prefix.{key}", out _); // Noncompliant + _ = Request.{{property}}[$"""prefix.{key}"""]; // Noncompliant + _ = Request.{{property}}.TryGetValue($"""prefix.{key}""", out _); // Noncompliant + + _ = Request.{{property}}[key: "id"]; // Noncompliant + _ = Request.{{property}}.TryGetValue(value: out _, key: "id"); // Noncompliant + } + + private static void HandleRequest(HttpRequest request) + { + _ = request.{{property}}["id"]; // Noncompliant: Containing type is a controller + void LocalFunction() + { + _ = request.{{property}}["id"]; // Noncompliant: Containing type is a controller + } + static void StaticLocalFunction(HttpRequest request) + { + _ = request.{{property}}["id"]; // Noncompliant: Containing type is a controller + } + } + } + """").Verify(); + + [TestMethod] + [CombinatorialData] + public void UseAspNetModelBinding_CompliantAccess( + [DataValues( + "_ = {0}.Keys", + "_ = {0}.Count", + "foreach (var kvp in {0}) {{ }}", + "_ = {0}.Select(x => x);", + "_ = {0}[key]; // Compliant: The accessed key is not a compile time constant")] string statementFormat, + [DataValues("Request", "this.Request", "ControllerContext.HttpContext.Request", "request")] string request, + [DataValues("Form", "Headers", "Query", "RouteValues")] string property, + [DataValues("[FromForm]", "[FromQuery]", "[FromRoute]", "[FromHeader]")] string attribute) => + builderAspNetCore.AddSnippet($$""" + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Filters; + using System; + using System.Linq; + using System.Threading.Tasks; + + public class TestController : Controller + { + async Task Compliant({{attribute}} string key, HttpRequest request) + { + {{string.Format(statementFormat, $"{request}.{property}")}}; + } + } + """).Verify(); + + [DataTestMethod] + [DataRow("Form")] + [DataRow("Headers")] + [DataRow("Query")] + [DataRow("RouteValues")] + public void UseAspNetModelBinding_DottedExpressions(string property) => + builderAspNetCore.AddSnippet($$""" + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Filters; + using Microsoft.AspNetCore.Routing; + using System; + using System.Linq; + using System.Threading.Tasks; + + public class TestController : Controller + { + HttpRequest ValidRequest => Request; + IFormCollection Form => Request.Form; + IHeaderDictionary Headers => Request.Headers; + IQueryCollection Query => Request.Query; + RouteValueDictionary RouteValues => Request.RouteValues; + + async Task DottedExpressions() + { + _ = (true ? Request : Request).{{property}}["id"]; // Noncompliant + _ = ValidatedRequest().{{property}}["id"]; // Noncompliant + _ = ValidRequest.{{property}}["id"]; // Noncompliant + _ = {{property}}["id"]; // Noncompliant + _ = this.{{property}}["id"]; // Noncompliant + _ = new TestController().{{property}}["id"]; // Noncompliant + + _ = this.Request.{{property}}["id"]; // Noncompliant + _ = Request?.{{property}}?["id"]; // Noncompliant + _ = Request?.{{property}}?.TryGetValue("id", out _); // Noncompliant + _ = Request.{{property}}?.TryGetValue("id", out _); // Noncompliant + _ = Request.{{property}}?.TryGetValue("id", out _).ToString(); // Noncompliant + _ = HttpContext.Request.{{property}}["id"]; // Noncompliant + _ = Request.HttpContext.Request.{{property}}["id"]; // Noncompliant + _ = this.ControllerContext.HttpContext.Request.{{property}}["id"]; // Noncompliant + var r1 = HttpContext.Request; + _ = r1.{{property}}["id"]; // Noncompliant + var r2 = ControllerContext; + _ = r2.HttpContext.Request.{{property}}["id"]; // Noncompliant + + HttpRequest ValidatedRequest() => Request; + } + } + """).Verify(); + + [DataTestMethod] + [DataRow("public class MyController: Controller")] + [DataRow("public class MyController: ControllerBase")] + [DataRow("[Controller] public class My: Controller")] + // [DataRow("public class MyController")] FN: Poco controller are not detected + public void UseAspNetModelBinding_PocoController(string classDeclaration) => + builderAspNetCore.AddSnippet($$"""" + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Filters; + using System; + using System.Linq; + using System.Threading.Tasks; + + {{classDeclaration}} + { + public async Task Action([FromServices]IHttpContextAccessor httpContextAccessor) + { + _ = httpContextAccessor.HttpContext.Request.Form["id"]; // Noncompliant + } + } + """").Verify(); + + [DataTestMethod] + [DataRow("public class My")] + [DataRow("[NonController] public class My: Controller")] + [DataRow("[NonController] public class MyController: Controller")] + public void UseAspNetModelBinding_NoController(string classDeclaration) => + builderAspNetCore.AddSnippet($$"""" + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Filters; + using System; + using System.Linq; + using System.Threading.Tasks; + + {{classDeclaration}} + { + public async Task Action([FromServices]IHttpContextAccessor httpContextAccessor) + { + _ = httpContextAccessor.HttpContext.Request.Form["id"]; // Compliant + } + } + """").Verify(); + + [DataTestMethod] + [DataRow("Form")] + [DataRow("Headers")] + [DataRow("Query")] + [DataRow("RouteValues")] + public void UseAspNetModelBinding_NoControllerHelpers(string property) => + builderAspNetCore.AddSnippet($$"""" + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Filters; + using System; + using System.Linq; + using System.Threading.Tasks; + + static class HttpRequestExtensions + { + public static void Ext(this HttpRequest request) + { + _ = request.{{property}}["id"]; // Compliant: Not in a controller + } + } + + class RequestService + { + public HttpRequest Request { get; } + + public void HandleRequest(HttpRequest request) + { + _ = Request.{{property}}["id"]; // Compliant: Not in a controller + _ = request.{{property}}["id"]; // Compliant: Not in a controller + } + } + """").Verify(); + + [TestMethod] + [CombinatorialData] + public void UseAspNetModelBinding_InheritanceAccess( + [DataValues( + ": Controller", + ": ControllerBase", + ": MyBaseController", + ": MyBaseBaseController")]string baseList, + [DataValues( + """_ = Request.Form["id"]""", + """_ = Request.Form.TryGetValue("id", out var _)""", + """_ = Request.Headers["id"]""", + """_ = Request.Query["id"]""", + """_ = Request.RouteValues["id"]""")]string nonCompliantStatement) => + builderAspNetCore.AddSnippet($$"""" + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Filters; + using System; + using System.Linq; + using System.Threading.Tasks; + + public class MyBaseController : ControllerBase { } + public class MyBaseBaseController : MyBaseController { } + + public class MyTestController {{baseList}} + { + public void Action() + { + {{nonCompliantStatement}}; // Noncompliant + } + } + """").Verify(); +#endif +} diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAspNetModelBinding_AspNetCore.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAspNetModelBinding_AspNetCore.cs new file mode 100644 index 00000000000..5e47dd444f9 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAspNetModelBinding_AspNetCore.cs @@ -0,0 +1,214 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using System; +using System.Linq; +using System.Threading.Tasks; + +public class TestController : Controller +{ + private readonly string Key = "id"; + + public IActionResult Post(string key) + { + _ = Request.Form["id"]; // Noncompliant {{Use model binding instead of accessing the raw request data}} + // ^^^^^^^^^^^^ + _ = Request.Form.TryGetValue("id", out _); // Noncompliant {{Use model binding instead of accessing the raw request data}} + // ^^^^^^^^^^^^^^^^^^^^^^^^ + _ = Request.Form.ContainsKey("id"); // Noncompliant {{Use model binding instead of accessing the raw request data}} + // ^^^^^^^^^^^^^^^^^^^^^^^^ + _ = Request.Headers["id"]; // Noncompliant {{Use model binding instead of accessing the raw request data}} + // ^^^^^^^^^^^^^^^ + _ = Request.Headers.TryGetValue("id", out _); // Noncompliant {{Use model binding instead of accessing the raw request data}} + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + _ = Request.Headers.ContainsKey("id"); // Noncompliant {{Use model binding instead of accessing the raw request data}} + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + _ = Request.Query["id"]; // Noncompliant {{Use model binding instead of accessing the raw request data}} + // ^^^^^^^^^^^^^ + _ = Request.Query.TryGetValue("id", out _); // Noncompliant {{Use model binding instead of accessing the raw request data}} + // ^^^^^^^^^^^^^^^^^^^^^^^^^ + _ = Request.RouteValues["id"]; // Noncompliant {{Use model binding instead of accessing the raw request data}} + // ^^^^^^^^^^^^^^^^^^^ + _ = Request.RouteValues.TryGetValue("id", out _); // Noncompliant {{Use model binding instead of accessing the raw request data}} + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + _ = Request.Form.Files; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} + // ^^^^^^^^^^^^^^^^^^ + _ = Request.Form.File\u0073; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} + // ^^^^^^^^^^^^^^^^^^^^^^^ + _ = Request.Form.Files["file"]; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} + // ^^^^^^^^^^^^^^^^^^ + _ = Request.Form.Files[key]; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} + // ^^^^^^^^^^^^^^^^^^ + _ = Request.Form.Files[0]; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} + // ^^^^^^^^^^^^^^^^^^ + _ = Request.Form.Files.Any(); // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} + // ^^^^^^^^^^^^^^^^^^ + _ = Request.Form.Files.Count; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} + // ^^^^^^^^^^^^^^^^^^ + _ = Request.Form.Files.GetFile("file"); // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} + // ^^^^^^^^^^^^^^^^^^ + _ = Request.Form.Files.GetFiles("file"); // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}} + // ^^^^^^^^^^^^^^^^^^ + return default; + } + + void MixedAccess_Form(string key) + { + _ = Request.Form["id"]; // Compliant (a mixed access with constant and non-constant keys is compliant) + _ = Request.Form[key]; // Compliant + } + + void MixedAccess_Form_Query(string key) + { + _ = Request.Form["id"]; // Compliant (a mixed access with constant and non-constant keys is compliant) + _ = Request.Query[key]; // Compliant + } + + void FalseNegatives() + { + string localKey = "id"; + _ = Request.Form[localKey]; // FN (Requires SE) + _ = Request.Form[Key]; // FN: Key is a readonly field with a constant initializer (Requires cross procedure SE) + } + + void FormCollection(IFormCollection form) + { + _ = form["id"]; // Compliant. Using IFormCollection is model binding + } + + void WriteAccess() + { + Request.Headers["id"] = "Assignment"; // Compliant + Request.RouteValues["id"] = "Assignment"; // Compliant + } + + async Task Compliant() + { + _ = Request.Cookies["cookie"]; // Compliant: Cookies are not bound by default + _ = Request.QueryString; // Compliant: Accessing the whole raw string is fine. + } + + async Task CompliantFormAccess() + { + var form = await Request.ReadFormAsync(); // Compliant: This might be used for optimization purposes e.g. conditional form value access. + _ = form["id"]; + } +} + +public class CodeBlocksController : Controller +{ + public CodeBlocksController() + { + _ = Request.Form["id"]; // Noncompliant + } + + public CodeBlocksController(object o) => _ = Request.Form["id"]; // Noncompliant + + HttpRequest ValidRequest => Request; + IFormCollection Form => Request.Form; + + string P1 => Request.Form["id"]; // Noncompliant + string P2 + { + get => Request.Form["id"]; // Noncompliant + } + string P3 + { + get + { + return Request.Form["id"]; // Noncompliant + } + } + void M1() => _ = Request.Form["id"]; // Noncompliant + void M2() + { + Func f1 = () => Request.Form["id"]; // Noncompliant + Func f2 = x => Request.Form["id"]; // Noncompliant + Func f3 = delegate (object x) { return Request.Form["id"]; }; // Noncompliant + } + void M3() + { + // see also parameterized test "DottedExpressions" + _ = Request.Form["id"][0]; // Noncompliant + _ = Request?.Form["id"][0]; // Noncompliant + _ = Request.Form?["id"][0]; // Noncompliant + _ = Request?.Form?["id"][0]; // Noncompliant + _ = Request.Form?["id"][0]; // Noncompliant + + _ = Request.Form?["id"][0][0]; // Noncompliant + _ = Request.Form?["id"][0]?[0]; // Noncompliant + _ = Request.Form["id"][0]?[0]; // Noncompliant + } + ~CodeBlocksController() => _ = Request.Form["id"]; // Noncompliant +} + +public class OverridesController : Controller +{ + public void Action() + { + _ = Request.Form["id"]; // Noncompliant + } + private void Undecidable(HttpContext context) + { + // Implementation: It might be difficult to distinguish between access to "Request" that originate from overrides vs. "Request" access that originate from action methods. + // This is especially true for "Request" which originate from parameters like here. We may need to redeclare such cases as FNs (see e.g HandleRequest above). + _ = context.Request.Form["id"]; // Undecidable: request may originate from an action method (which supports binding), or from one of the following overrides (which don't). + _ = context.Request.Form["id"][0]; + _ = context.Request?.Form["id"][0]; + _ = context.Request.Form?["id"][0]; + _ = context.Request?.Form?["id"][0]; + _ = context.Request.Form?["id"][0]; + + _ = context.Request.Form?["id"][0][0]; + _ = context.Request.Form?["id"][0]?[0]; + _ = context.Request.Form["id"][0]?[0]; + } + private void Undecidable(HttpRequest request) + { + _ = request.Form["id"]; // Undecidable: request may originate from an action method (which supports binding), or from one of the following overloads (which don't). + } + public override void OnActionExecuted(ActionExecutedContext context) + { + _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here + } + public override void OnActionExecuting(ActionExecutingContext context) + { + _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here + } + public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here + return base.OnActionExecutionAsync(context, next); + } +} + +[Controller] +public class PocoController : IActionFilter, IAsyncActionFilter +{ + public void OnActionExecuted(ActionExecutedContext context) + { + _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here + } + void IActionFilter.OnActionExecuted(Microsoft.AspNetCore.Mvc.Filters.ActionExecutedContext context) + { + _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here + } + public void OnActionExecuting(ActionExecutingContext context) + { + _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here + } + void IActionFilter.OnActionExecuting(ActionExecutingContext context) + { + _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here + } + public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here + return Task.CompletedTask; + } + Task IAsyncActionFilter.OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + _ = context.HttpContext.Request.Form["id"]; // Compliant: Model binding is not supported here + return Task.CompletedTask; + } +} diff --git a/analyzers/tests/SonarAnalyzer.Test/Trackers/ArgumentTrackerTest.cs b/analyzers/tests/SonarAnalyzer.Test/Trackers/ArgumentTrackerTest.cs new file mode 100644 index 00000000000..f2848434231 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.Test/Trackers/ArgumentTrackerTest.cs @@ -0,0 +1,1288 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Microsoft.CodeAnalysis.Text; +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 (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "ToString", "provider", 0); + var (result, context) = MatchArgumentCS(model, node, argument); + 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 (node, model) = ArgumentAndModelVB(WrapInMethodVB(snippet)); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "ToString", "provider", 0); + var (result, context) = MatchArgumentVB(model, node, argument); + 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 (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.MethodInvocation(m => m.Name == "M", (s, c) => s.Equals("M", c), p => p.Name == parameterName, _ => true, null); + var (result, _) = MatchArgumentVB(model, node, argument); + 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 (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "ToString", "provider", position); + var (result, _) = MatchArgumentCS(model, node, argument); + 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 (node, model) = ArgumentAndModelVB(WrapInMethodVB(snippet)); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "ToString", "provider", position); + var (result, _) = MatchArgumentVB(model, node, argument); + 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 (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "TryParse", "result", x => true, RefKind.Out); + var (result, context) = MatchArgumentCS(model, node, argument); + 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 (node, model) = ArgumentAndModelVB(WrapInMethodVB(snippet)); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "TryParse", "result", x => true, RefKind.Out); + var (result, _) = MatchArgumentVB(model, node, argument); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""int.TryParse("", $$out var result);""", RefKind.Ref)] + [DataRow("""int.TryParse($$"", out var result);""", RefKind.Out)] + [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.Out)] + public void Method_RefOut_False(string invocation, RefKind refKind) + { + var snippet = $$""" + System.IFormatProvider provider = null; + {{invocation}} + """; + var (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "TryParse", "result", x => true, refKind); + var (result, context) = MatchArgumentCS(model, node, argument); + 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 (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Int32, "TryParse", "result", x => true); + var (result, context) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + context.Parameter.RefKind.Should().Be(RefKind.Out); + } + + [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 (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.MethodInvocation(m => true, (m, c) => m.Equals("M", c), p => p.Name == "parameter", x => true, null); + var (result, _) = MatchArgumentCS(model, node, argument); + 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 (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.MethodInvocation(m => true, (m, c) => m.Equals("M", c), p => p.Name == "parameter", x => true, null); + var (result, _) = MatchArgumentVB(model, node, argument); + 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 (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Collections_Generic_Comparer_T, "Compare", "x", 0); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""comparer.Compare($$Nothing, Nothing)""")] + [DataRow("""Call New MyComparer(Of Integer)().Compare($$1, 2)""")] + public void Method_Inheritance_BaseClasses_Generics_VB(string invocation) + { + var snippet = $$""" + Imports System.Collections.Generic + + Public Class MyComparer(Of T) + Inherits Comparer(Of T) + + Public Sub New() + End Sub + + 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 (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Collections_Generic_Comparer_T, "Compare", "x", 0); + var (result, _) = MatchArgumentVB(model, node, argument); + result.Should().BeTrue(); + } + + [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 (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Collections_CollectionBase, "OnInsert", "index", 0); + var (result, _) = MatchArgumentCS(model, node, argument); + 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 (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Collections_CollectionBase, "OnInsert", "index", 0); + var (result, _) = MatchArgumentVB(model, node, argument); + 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[0])""", "args")] + public void Method_ParamsArray(string invocation, string parameterName) + { + var snippet = $$""" + _ = {{invocation}}; + """; + var (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_String, "Format", parameterName, i => i >= 1); + var (result, _) = MatchArgumentCS(model, node, argument); + 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 (node, model) = ArgumentAndModelVB(WrapInMethodVB(snippet)); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_String, "Format", parameterName, i => i >= 1); + var (result, _) = MatchArgumentVB(model, node, argument); + result.Should().BeTrue(); + } + + [TestMethod] + public void Method_NamelessMethod() + { + var snippet = $$""" + using System; + class C + { + Action ActionReturning() => null; + + void M() + { + ActionReturning()($$1); + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.MethodInvocation(KnownType.System_Action_T, methodName: string.Empty, "obj", 0); + var (result, context) = MatchArgumentCS(model, node, argument); + 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 (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.MethodInvocation( + invokedMethodSymbol: 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, context) = MatchArgumentCS(model, node, argument); + 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 argumentPosition, bool expected) + { + var snippet = $$""" + _ = new System.Diagnostics.{{constructor}}; + """; + var (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Diagnostics_ProcessStartInfo, parameterName, argumentPosition); + var (result, context) = MatchArgumentCS(model, node, argument); + 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 argumentPosition, bool expected) + { + var snippet = $$""" + Dim a = New System.Diagnostics.{{constructor}} + """; + var (node, model) = ArgumentAndModelVB(WrapInMethodVB(snippet)); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Diagnostics_ProcessStartInfo, parameterName, argumentPosition); + var (result, _) = MatchArgumentVB(model, node, argument); + 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 argumentPosition, bool expected) + { + var snippet = $$""" + System.Diagnostics.ProcessStartInfo psi = new{{constructor}}; + """; + var (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Diagnostics_ProcessStartInfo, parameterName, argumentPosition); + var (result, _) = MatchArgumentCS(model, node, argument); + 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, EqualityComparer.Default)""", "capacity", 0, true)] + [DataRow("""new Dictionary(1, $$EqualityComparer.Default)""", "comparer", 1, true)] + public void Constructor_Generic(string constructor, string parameterName, int argumentPosition, bool expected) + { + var snippet = $$""" + using System.Collections.Generic; + class C + { + public void M() where TKey : notnull + { + _ = {{constructor}}; + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Collections_Generic_Dictionary_TKey_TValue, parameterName, argumentPosition); + var (result, _) = MatchArgumentCS(model, node, argument); + 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, 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 argumentPosition, bool expected) + { + var snippet = $$""" + Imports System.Collections.Generic + + Class C + Public Sub M(Of TKey, TValue)() + {{constructor}} + End Sub + End Class + """; + var (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Collections_Generic_Dictionary_TKey_TValue, parameterName, argumentPosition); + var (result, _) = MatchArgumentVB(model, node, argument); + result.Should().Be(expected); + } + + [TestMethod] + public void Constructor_BaseCall() + { + var snippet = $$""" + using System.Collections.Generic; + class MyList: List + { + public MyList(int capacity) : base(capacity) // Unsupported + { + } + } + public class Test + { + public void M() + { + _ = new MyList($$1); // Requires tracking of the parameter to the base constructor + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Collections_Generic_List_T, "capacity", 0); + var (result, _) = MatchArgumentCS(model, node, argument); + 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 (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Collections_Generic_List_T, "capacity", 0); + var (result, _) = MatchArgumentVB(model, node, argument); + 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 argumentPosition, bool expected) + { + var snippet = $$""" + using NumberList = System.Collections.Generic.List; + class C + { + public void M() + { + NumberList nl = {{constructor}}; + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Collections_Generic_List_T, parameterName, argumentPosition); + var (result, _) = MatchArgumentCS(model, node, argument); + 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 (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Collections_Generic_List_T, "capacity", 0); + var (result, _) = MatchArgumentVB(model, node, argument); + 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 (node, model) = ArgumentAndModelCS(snippet); + + var argument = 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: p => p.Name is "i" or "j", + argumentListConstraint: (n, i) => i is null or 0 or 1 && n.Count > 1, + refKind: null); + var (result, _) = MatchArgumentCS(model, node, argument); + 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 (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.ConstructorInvocation(invokedMethodSymbol: x => x is { MethodKind: MethodKind.Constructor, ContainingSymbol.Name: "Base" }, + invokedMemberNameConstraint: (c, n) => c.Equals("Base", n), + invokedMemberNodeConstraint: (_, _, _) => true, + parameterConstraint: p => p.Name is "i", + argumentListConstraint: (_, _) => true, + refKind: null); + var (result, _) = MatchArgumentCS(model, node, argument); + 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 (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.ConstructorInvocation(invokedMethodSymbol: x => x is { MethodKind: MethodKind.Constructor, ContainingSymbol.Name: "Base" }, + invokedMemberNameConstraint: (c, n) => c.Equals("Base", n), + invokedMemberNodeConstraint: (_, _, _) => true, + parameterConstraint: p => p.Name is "i", + argumentListConstraint: (_, _) => true, + refKind: null); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + } + + [TestMethod] + public void Constructor_InitializerCalls_Base_MyException() + { + var snippet = """ + using System; + + class MyException: Exception + { + public MyException(string message) : base($$message) + { } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Exception, "message", 0); + var (result, _) = MatchArgumentCS(model, node, argument); + 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 (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.ConstructorInvocation(KnownType.System_Exception, "message", 0); + var (result, _) = MatchArgumentVB(model, node, argument); + 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 (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.ElementAccess(KnownType.System_Collections_Generic_List_T, "list", + p => p is { Name: "index", Type.SpecialType: SpecialType.System_Int32, ContainingSymbol: IMethodSymbol { MethodKind: MethodKind.PropertyGet } }, 0); + var (result, context) = MatchArgumentCS(model, node, argument); + 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 (node, model) = ArgumentAndModelVB(WrapInMethodVB(snippet)); + + var argument = ArgumentDescriptor.ElementAccess(KnownType.System_Collections_Generic_List_T, "list", + p => p is { Name: "index", Type.SpecialType: SpecialType.System_Int32, ContainingSymbol: IMethodSymbol { MethodKind: MethodKind.PropertyGet } }, 0); + var (result, context) = MatchArgumentVB(model, node, argument); + 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 (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.ElementAccess(KnownType.System_Collections_Generic_List_T, + p => p is { Name: "index", ContainingSymbol: IMethodSymbol { MethodKind: MethodKind.PropertySet } }, 0); + var (result, context) = MatchArgumentCS(model, node, argument); + 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 (node, model) = ArgumentAndModelVB(WrapInMethodVB(snippet)); + + var argument = ArgumentDescriptor.ElementAccess(KnownType.System_Collections_Generic_List_T, + p => p is { Name: "index", ContainingSymbol: IMethodSymbol { MethodKind: MethodKind.PropertySet } }, 0); + var (result, context) = MatchArgumentVB(model, node, argument); + 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 (node, model) = ArgumentAndModelCS(WrapInMethodCS(snippet)); + + var argument = ArgumentDescriptor.ElementAccess(m => m is { MethodKind: MethodKind.PropertyGet, ContainingType: { } type } && type.Name == "IDictionary", + (n, c) => n.Equals("GetEnvironmentVariables", c), (_, _, _) => true, p => p.Name == "key", (_, p) => p is null or 0); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + } + + [TestMethod] + public void Indexer_DictionaryGet_VB() + { + var snippet = """ + Dim a = Environment.GetEnvironmentVariables()($$"TEMP") + """; + var (node, model) = ArgumentAndModelVB(WrapInMethodVB(snippet)); + + var argument = ArgumentDescriptor.ElementAccess(m => m is { MethodKind: MethodKind.PropertyGet, ContainingType: { } type } && type.Name == "IDictionary", + (n, c) => n.Equals("GetEnvironmentVariables", c), (_, _, _) => true, p => p.Name == "key", (_, p) => p is null or 0); + var (result, _) = MatchArgumentVB(model, node, argument); + 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 (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.ElementAccess( + m => m is { MethodKind: var kind, ContainingType: { } type } && type.Name == "C" && (isGetter ? kind == MethodKind.PropertyGet : kind == MethodKind.PropertySet), + (n, c) => true, + (_, _, _) => true, + p => p.Name == parameterName, (_, _) => true); + var (result, _) = MatchArgumentCS(model, node, argument); + 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 (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.ElementAccess( + m => m is { MethodKind: var kind, ContainingType: { } type } && type.Name == "C" && (isGetter ? kind == MethodKind.PropertyGet : kind == MethodKind.PropertySet), + (n, c) => true, + (_, _, _) => true, + p => p.Name == parameterName, (_, _) => true); + var (result, _) = MatchArgumentVB(model, node, argument); + 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_ModulesAccess(string modulesAccess) + { + var snippet = $$""" + public class Test + { + public void M(System.Diagnostics.Process process) + { + _ = {{modulesAccess}}; + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.ElementAccess(m => m is { MethodKind: MethodKind.PropertyGet, ContainingType: { } type } && type.Name == "ProcessModuleCollection", + (n, c) => n.Equals("Modules", c), (_, _, _) => true, p => p.Name == "index", (_, p) => p is null or 0); + var (result, _) = MatchArgumentCS(model, node, argument); + 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_Environment(string environmentAccess) + { + var snippet = $$""" + public class Test + { + public void M(System.Diagnostics.ProcessStartInfo processStartInfo) + { + _ = {{environmentAccess}}; + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.ElementAccess(KnownType.System_Collections_Generic_IDictionary_TKey_TValue, "Environment", p => p.Name == "key", 0); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + } + +#if NET5_0_OR_GREATER + + [TestMethod] + public void Attribute_Obsolete() + { + var snippet = $$""" + using System; + public class Test + { + [Obsolete($$"message", UrlFormat = "")] + public void M() + { + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.AttributeArgument(x => x is { MethodKind: MethodKind.Constructor, ContainingType.Name: "ObsoleteAttribute" }, + (s, c) => s.StartsWith("Obsolete", c), (_, _, _) => true, p => p.Name == "message", (_, i) => i is 0); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + } + + [TestMethod] + public void Attribute_Obsolete_VB() + { + var snippet = $$""" + Imports System + + Public Class Test + + Public Sub M() + End Sub + End Class + """; + var (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.AttributeArgument(x => x is { MethodKind: MethodKind.Constructor, ContainingType.Name: "ObsoleteAttribute" }, + (s, c) => s.StartsWith("Obsolete", c), (_, _, _) => true, p => p.Name == "message", (_, i) => i is 0); + var (result, _) = MatchArgumentVB(model, node, argument); + 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)] + public void Attribute_Designer(string attribute, string parameterName, int argumentPosition) + { + var snippet = $$""" + using System.ComponentModel; + + {{attribute}} + public class Test + { + public void M() + { + } + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.AttributeArgument("Designer", parameterName, argumentPosition); + var (result, _) = MatchArgumentCS(model, node, argument); + result.Should().BeTrue(); + } + + [DataTestMethod] + [DataRow("""""", "designerTypeName", 0)] + [DataRow("""""", "designerTypeName", 0)] + [DataRow("""""", "designerTypeName", 0)] + [DataRow("""""", "designerBaseTypeName", 1)] + public void Attribute_Designer_VB(string attribute, string parameterName, int argumentPosition) + { + var snippet = $$""" + Imports System.ComponentModel + + {{attribute}} + Public Class Test + End Class + """; + var (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.AttributeArgument("Designer", parameterName, argumentPosition); + var (result, _) = MatchArgumentVB(model, node, argument); + 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_Property(string attribute, string propertyName, bool expected) + { + var snippet = $$""" + using System; + + {{attribute}} + public sealed class TestAttribute: Attribute + { + } + """; + var (node, model) = ArgumentAndModelCS(snippet); + + var argument = ArgumentDescriptor.AttributeProperty("AttributeUsage", propertyName); + var (result, context) = MatchArgumentCS(model, node, argument); + 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_Property_VB(string attribute, string propertyName, bool expected) + { + var snippet = $$""" + Imports System + + {{attribute}} + Public NotInheritable Class TestAttribute + Inherits Attribute + End Class + """; + var (node, model) = ArgumentAndModelVB(snippet); + + var argument = ArgumentDescriptor.AttributeProperty("AttributeUsage", propertyName); + var (result, context) = MatchArgumentVB(model, node, argument); + 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 (SyntaxNode Node, SemanticModel Model) ArgumentAndModel(string snippet, + Func compile, params Type[] argumentNodeTypes) + { + var pos = snippet.IndexOf("$$"); + if (pos == -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(pos, 1)).Reverse().First(x => argumentNodeTypes.Any(t => t.IsInstanceOfType(x))); // root.Find does not work with OmittedArgument + return (node, model); + } + + private static (SyntaxNode Node, SemanticModel Model) ArgumentAndModelCS(string snippet) => + ArgumentAndModel(snippet, TestHelper.CompileCS, typeof(CS.ArgumentSyntax), typeof(CS.AttributeArgumentSyntax)); + + private static (SyntaxNode Node, SemanticModel Model) ArgumentAndModelVB(string snippet) => + ArgumentAndModel(snippet, TestHelper.CompileVB, typeof(VB.ArgumentSyntax)); + + private static (bool Match, ArgumentContext Context) MatchArgumentCS(SemanticModel model, SyntaxNode node, ArgumentDescriptor descriptor) => + MatchArgument(model, node, descriptor); + + private static (bool Match, ArgumentContext Context) MatchArgumentVB(SemanticModel model, SyntaxNode node, ArgumentDescriptor descriptor) => + MatchArgument(model, node, descriptor); + + private static (bool Match, ArgumentContext Context) MatchArgument(SemanticModel model, SyntaxNode node, ArgumentDescriptor descriptor) + where TTracker : ArgumentTracker, new() + where TSyntaxKind : struct + { + var context = new ArgumentContext(node, model); + var result = new TTracker().MatchArgument(descriptor)(context); + return (result, context); + } +} diff --git a/analyzers/tests/SonarAnalyzer.TestFramework.Test/Common/CombinatorialDataAttributeTest.cs b/analyzers/tests/SonarAnalyzer.TestFramework.Test/Common/CombinatorialDataAttributeTest.cs new file mode 100644 index 00000000000..81c5f65b075 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.TestFramework.Test/Common/CombinatorialDataAttributeTest.cs @@ -0,0 +1,122 @@ +/* + * 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.Test.TestFramework.Tests.Common; + +[TestClass] + +public class CombinatorialDataAttributeTest_TwoDimensions +{ + private static List<(int X, int Y)> combinations; + + [ClassInitialize] + public static void Initialize(TestContext context) => + combinations = new(); + + [TestMethod] + [CombinatorialData] +#pragma warning disable S2699 // Tests should include assertions. Assertion happens in cleanup + public void Combinatorial([DataValues(1, 2, 3)] int x, [DataValues(-1, -2, -3)] int y) => + combinations.Add((x, y)); +#pragma warning restore S2699 + + [ClassCleanup] + public static void Cleanup() => + combinations.Should().BeEquivalentTo([ + (1, -1), + (1, -2), + (1, -3), + (2, -1), + (2, -2), + (2, -3), + (3, -1), + (3, -2), + (3, -3), + ]); +} + +[TestClass] +public class CombinatorialDataAttributeTest_ThreeDimensions +{ + private static List<(int X, string Y, bool Z)> combinations; + + [ClassInitialize] + public static void Initialize(TestContext context) + { + combinations = new(); + } + + [TestMethod] + [CombinatorialData] +#pragma warning disable S2699 // Tests should include assertions. Assertion happens in cleanup + public void Combinatorial([DataValues(1, 2, 3)] int x, [DataValues("A", "B")] string y, [DataValues(true, false)] bool z) +#pragma warning restore S2699 + { + combinations.Add((x, y, z)); + } + + [ClassCleanup] + public static void Cleanup() + { + combinations.Should().BeEquivalentTo([ + (1, "A", true), + (1, "B", true), + (1, "A", false), + (1, "B", false), + (2, "A", true), + (2, "B", true), + (2, "A", false), + (2, "B", false), + (3, "A", true), + (3, "B", true), + (3, "A", false), + (3, "B", false), + ]); + } +} + +[TestClass] +public class CombinatorialDataAttributeTest_AttributeTest +{ + [TestMethod] + public void CombinatorialData() + { + var attribute = new CombinatorialDataAttribute(); + var data = attribute.GetData(typeof(CombinatorialDataAttributeTest_AttributeTest).GetMethod(nameof(Combinatorial))); + data.Should().BeEquivalentTo([ + [1, "A", true], + [1, "B", true], + [1, "A", false], + [1, "B", false], + [2, "A", true], + [2, "B", true], + [2, "A", false], + [2, "B", false], + [3, "A", true], + [3, "B", true], + [3, "A", false], + [3, "B", false], + ]); + } + public static void Combinatorial([DataValues(1, 2, 3)] int x, [DataValues("A", "B")] string y, [DataValues(true, false)] bool z) + { + // For Attribute reflection only + } +} diff --git a/analyzers/tests/SonarAnalyzer.TestFramework/Common/CombinatorialDataAttribute.cs b/analyzers/tests/SonarAnalyzer.TestFramework/Common/CombinatorialDataAttribute.cs new file mode 100644 index 00000000000..38b4da2b813 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.TestFramework/Common/CombinatorialDataAttribute.cs @@ -0,0 +1,83 @@ +/* + * 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.Globalization; +using System.Reflection; + +namespace SonarAnalyzer.TestFramework.Common; + +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] +public sealed class DataValuesAttribute : Attribute +{ + public object[] Values { get; } + + public DataValuesAttribute(params object[] values) + { + Values = values; + } +} + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public sealed class CombinatorialDataAttribute : Attribute, ITestDataSource +{ + // Based on https://stackoverflow.com/a/75531690 + public IEnumerable GetData(MethodInfo methodInfo) + { + var valuesPerParameter = methodInfo.GetParameters().Select(p => p.GetCustomAttribute()?.Values + ?? throw new InvalidOperationException("Combinatorial test requires all parameters to have the [DataValues] attribute set")).ToArray(); + var parameterIndices = new int[valuesPerParameter.Length]; + + while (true) + { + // Create new arguments + var arg = new object[parameterIndices.Length]; + for (var i = 0; i < parameterIndices.Length; i++) + { + arg[i] = valuesPerParameter[i][parameterIndices[i]]; + } + + yield return arg; + + // Increment indices + for (var i = parameterIndices.Length - 1; i >= 0; i--) + { + parameterIndices[i]++; + if (parameterIndices[i] >= valuesPerParameter[i].Length) + { + parameterIndices[i] = 0; + + if (i == 0) + { + yield break; + } + } + else + { + break; + } + } + } + } + + public string GetDisplayName(MethodInfo methodInfo, object[] data) => + data == null + ? null + : $"{methodInfo.Name} ({string.Join(",", data)})"; +} diff --git a/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/AspNetCoreMetadataReference.cs b/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/AspNetCoreMetadataReference.cs index cbe770ec542..1e09a226b4d 100644 --- a/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/AspNetCoreMetadataReference.cs +++ b/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/AspNetCoreMetadataReference.cs @@ -42,5 +42,6 @@ public static class AspNetCoreMetadataReference public static MetadataReference MicrosoftAspNetCoreRouting { get; } = Create(typeof(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder)); public static MetadataReference MicrosoftAspNetCoreWebHost { get; } = Create(typeof(Microsoft.AspNetCore.WebHost)); public static MetadataReference MicrosoftExtensionsHostingAbstractions { get; } = Create(typeof(Microsoft.Extensions.Hosting.IHost)); + public static MetadataReference MicrosoftExtensionsPrimitives { get; } = Create(typeof(Microsoft.Extensions.Primitives.CancellationChangeToken)); } #endif