Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create rule S6932: Use model binding instead of reading raw request data #8930

Closed
wants to merge 62 commits into from

Conversation

martin-strecker-sonarsource
Copy link
Contributor

Fixes #8871

Replaces #8902

@github-actions github-actions bot added this to In progress in Best Kanban Mar 14, 2024
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a left over after the project was moved. It can safely be deleted.

@martin-strecker-sonarsource martin-strecker-sonarsource changed the title Backup/martin/new s6932 Create rule S6932: Use model binding instead of reading raw request data Mar 14, 2024
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file seems to follow Roslyn code conventions, which is where the file is taken from, as opposed to our own.
Given that we don't have an outcome yet, for this Trello card, I will not review this file from a stylistic point of view in this PR, but only functionally.
However, I would ask you to mention this file in the "How we work" ticket, as a possible action to to review/refactor this file, once a decision is taken on how to deal with imported code from Roslyn.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not review. We will not make any edits to the file; the code is already being tested. We want to treat it as if it was a third party dll.

@antonioaversa antonioaversa self-requested a review March 14, 2024 18:03
Copy link

sonarcloud bot commented Mar 14, 2024

Quality Gate Failed Quality Gate failed

Failed conditions
9 New issues
86.8% Coverage on New Code (required ≥ 95%)

See analysis details on SonarCloud

Catch issues before they fail your Quality Gate with our IDE extension SonarLint

Copy link
Contributor

@antonioaversa antonioaversa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am releasing a first bunch of comments, mostly on the rule, C# extensions, facade and helpers. I still need to review the rest (common, C# trackers, VB, rule tests).

@@ -179,7 +179,6 @@ node switch
DestructorDeclarationSyntax { Identifier: var identifier } => identifier,
EnumMemberDeclarationSyntax { Identifier: var identifier } => identifier,
EventDeclarationSyntax { Identifier: var identifier } => identifier,
IdentifierNameSyntax { Identifier: var identifier } => identifier,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this case wasn't used at all in the code base, and has hence been removed?
I don't see connections between IdentifierNameSyntax and SimpleBaseTypeSyntax: their closest common ancestor is CSharpSyntaxNode.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IdentifierNameSyntax is already covered by it's base class SimpleNameSyntax in line 202.

SimpleNameSyntax { Identifier: var identifier } => identifier,

This change is "clean as you code" and unrelated to the SimpleBaseTypeSyntax

@@ -199,6 +198,7 @@ node switch
PointerTypeSyntax { ElementType: { } elementType } => GetIdentifier(elementType),
PredefinedTypeSyntax { Keyword: var keyword } => keyword,
QualifiedNameSyntax { Right.Identifier: var identifier } => identifier,
SimpleBaseTypeSyntax { Type: { } type } => GetIdentifier(type),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given this production rule, I wonder whether Type: { } type is unnecessary, and should be replaced with { Type: var type } instead, as we do in the same method for other cases such as AliasQualifiedNameSyntax, ArgumentSyntax etc.

I am not sure, but I think so, because, when I look at the code auto-generated by Roslyn, I see the following:

public sealed partial class SimpleBaseTypeSyntax : BaseTypeSyntax
{
    private TypeSyntax? type; // This is nullable

    internal SimpleBaseTypeSyntax(InternalSyntax.CSharpSyntaxNode green, SyntaxNode? parent, int position)
      : base(green, parent, position)
    {
    }

    public override TypeSyntax Type => GetRedAtZero(ref this.type)!; 
    // However, the Type property is not nullable, and non-nullability is asserted with a bang

Do you have a case of Type being null?
In any case, I don't think this is a blocking issue, also because the code seems to be covered fully: https://sonarcloud.io/code?id=sonaranalyzer-dotnet&pullRequest=8902&selected=sonaranalyzer-dotnet%3Aanalyzers%2Fsrc%2FSonarAnalyzer.CSharp%2FExtensions%2FSyntaxNodeExtensions.cs&line=201

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as we do not use version of Roslyn with nullable annotations, we should always assume null as a possible value. Otherwise, we a re always in the "not sure" camp and have to consult Syntax.xml or any other kind of documentation. Let's just play "Better safe than sorry".

@@ -503,6 +494,44 @@ public static ConditionalAccessExpressionSyntax GetRootConditionalAccessExpressi
return current;
}

// Copy of
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike for analyzers/src/SonarAnalyzer.CSharp/Extensions/ExpressionSyntaxExtensions.Roslyn.cs, this file contains also our code, not just code based on, or copied from Roslyn.
So I would need to comment on style since I think we should be consistent on stylistic rules at least within the same file. That makes review quite challenging.
I think it's best to keep Roslyn imported code segregated, in its own file, and with its own namespace (also for copyright concerns, see comment above).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. You may want to join https://trello.com/c/nw3yLmEY We have a meeting today.
The way I did it here, is proposed but not decided yet. In this file, there are already a lot of copied parts from Roslyn and I kept that for the time being.

// 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) =>
public static BaseArgumentListSyntax ArgumentList(this SyntaxNode node) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The signature seems now incompatible with the comment above.

What I understand from based on Type="ArgumentListSyntax" is that this method gets the ArgumentList property/field from all the syntax types for which Syntax.xml defines an AbstractNode that has a Field with Name="ArgumentList" and Type="ArgumentListSyntax".

In that case, the method should return a ArgumentListSyntax, not a BaseArgumentListSyntax.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment was wrong.

// 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) =>
public static BaseArgumentListSyntax ArgumentList(this SyntaxNode node) =>
node switch
{
ObjectCreationExpressionSyntax creation => creation.ArgumentList,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Syntax.xml this is actually BaseObjectCreationExpressionSyntax (see here).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no BaseObjectCreationExpressionSyntax in our version of Roslyn. The ImplicitObjectCreationExpressionSyntaxWrapper case below covers that case.

}
// 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 allConstantAccess = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subjective: allConstantAccesses (using access as countable) or onlyConstantAccess (using access as non-countable).

{
var argument = (ArgumentSyntax)nodeContext.Node;
var context = new ArgumentContext(argument, nodeContext.SemanticModel);
if (allConstantAccess && Array.Exists(argumentDescriptors, x => Language.Tracker.Argument.MatchArgument(x)(context)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Educational: can this new Language.Tracker.Argument be used in cases like this one?
What are the pros and cons w.r.t. use a MethodParameterLookup?

&& 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 = false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value false of OriginatesFromParameter doesn't seem to ever be used (it is always explicitly set on ReportCandidate construction), so it can be removed.

{
var memberAccess = (MemberAccessExpressionSyntax)nodeContext.Node;
var context = new PropertyAccessContext(memberAccess, nodeContext.SemanticModel, memberAccess.Name.Identifier.ValueText);
if (Language.Tracker.PropertyAccess.MatchProperty(propertyAccessDescriptors)(context))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Educational: just to test my understanding. In the following scenario:

_ = Request.Form.Files[F()];

string F() => "a string";

We would still raise a non-compliance, because, even if the parameter of the indexer is not a constant value, the user can still use model binding:

  • using IFormFileCollection to bind all the forms to a parameter of type collection
  • using that collection to access the F()-th element
  • and that is considered proper use of model binding

This is different from the ArgumentSyntax cases above, where, if the argument is not a de-facto constant, we just don't raise, as there is no "collection binding" feature available.

Is all the above correct? Can we add a comment explaining all of that? I think this difference between forms and the rest is very important to understand the frame of this implementation, and in particular the need to register to both ArgumentSyntax and SimpleMemberAccessSyntax.
The comment should probably go above the signature of the method.

// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ = Request.Form.Files; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}}
// ^^^^^^^^^^^^^^^^^^
_ = Request.Form.Files["file"]; // Noncompliant {{Use IFormFile or IFormFileCollection binding instead}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add tests for Form.Files that take the result of a method?

_ = Request.Form.Files[F()];

Form.Files is "special", compared to the other Request members, as it doesn't require its parameter to be constant, for the "model binding" suggestion to be valid. Form.Files doesn't seem to be included in the parameterized tests, so I think we should be explicit here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used a parameter instead. It serves the same purpose.

Copy link
Contributor

@antonioaversa antonioaversa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am releasing a couple of comments more.
Review still in progress.

return current;
}

private static ExpressionSyntax GetExpressionOfArgumentParent(ArgumentSyntax argument) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is called by GetPrimaryLocation, so it should be placed under it, as for the following codying style rule:

Once grouped as specified above, methods which are called by other methods in the same group should be placed below the callers.


private static ExpressionSyntax GetExpressionOfArgumentParent(ArgumentSyntax argument) =>
argument switch
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method doesn't take into account parenthesized expressions, yet it seems to work correctly.
Can we add some unit tests to make that sure? Diagnostic location should also be asserted.

        _ = Request.Form.Files[("file")]; // Noncompliant
        //  ^^^^^^^^^^^^^^^^^^
        _ = Request.Form.Files[(("file"))]; // Noncompliant
        //  ^^^^^^^^^^^^^^^^^^
        _ = (Request.Form.Files)["file"]; // Noncompliant
        //   ^^^^^^^^^^^^^^^^^^
        _ = ((Request.Form.Files))["file"]; // Noncompliant
        //    ^^^^^^^^^^^^^^^^^^

Remark: I am surprised how the round brackets are not included in the primary location in cases like the following: _ = (Request.Form.Files)["file"];
image

The parent of the ArgumentSyntax is a BracketedArgumentListSyntax, whose parent is a ElementAccessExpressionSyntax, whose Expression is the ParenthesizedExpression, which includes the ( and ) tokens.

What am I missing?

@@ -1,938 +0,0 @@
{

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this deleted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@costin-zaharia-sonarsource
Copy link
Member

Hi @martin-strecker-sonarsource, this is an impressive PR. With 4788 lines changed, it is very hard to review properly.

I can see multiple topics handled in this PR:

  • Introduction of new extensions: (analyzers/src/SonarAnalyzer.CSharp/Extensions/ExpressionSyntaxExtensions.Roslyn.cs, analyzers/src/SonarAnalyzer.Common/Helpers/SyntaxNodeExtensions.Roslyn.cs, analyzers/src/SonarAnalyzer.VisualBasic/Extensions/ExpressionSyntaxExtensions.Roslyn.cs)
  • ArgumentTracker and ArgumentDescriptor
  • Refactoring for the SyntaxTracker -> analyzers/src/SonarAnalyzer.Common/Trackers/SyntaxTrackerBase.cs
  • Introduction of CombinatorialDataAttribute
  • Test cases
  • Rule implementation

I suggest splitting this into digestible pieces :)

Regarding the implementation. I just started to review it, and I'm still wrapping my head around it, but can't we start with a simple implementation that uses a syntax walker? at least for the code blocks?

AddAspNetCoreDescriptors(argumentDescriptors, propertyAccessDescriptors);
}
// TODO: Add descriptors for Asp.Net MVC 4.x
return ([.. argumentDescriptors], [.. propertyAccessDescriptors]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not return the lists directly?

Copy link

sonarcloud bot commented Mar 18, 2024

Quality Gate Passed Quality Gate passed

Issues
0 New issues
0 Accepted issues

Measures
0 Security Hotspots
No data about Coverage
No data about Duplication

See analysis details on SonarCloud

@martin-strecker-sonarsource
Copy link
Contributor Author

Superseded by
#8947
#8950
#8953

#8952
#8951
#8949
#8948

Best Kanban automation moved this from In progress to Done Mar 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Best Kanban
  
Done
Development

Successfully merging this pull request may close these issues.

New rule S6932: Use model binding instead of reading raw request data
3 participants