Skip to content

Commit

Permalink
New rule S6797: Blazor query parameter should be of supported type (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastien-marichal authored Nov 16, 2023
1 parent 458feb1 commit 9f6edc4
Show file tree
Hide file tree
Showing 36 changed files with 683 additions and 150 deletions.
26 changes: 26 additions & 0 deletions analyzers/its/expected/BlazorSample/BlazorSample--net7.0-S101.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,32 @@
"issues": [
{
"id": "S101",
"message": "Rename class 'S6797_CsharpOnly' to match pascal case naming rules, consider using 'S6797CsharpOnly'.",
"location": {
"uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/sources/BlazorSample/BlazorSample/Pages/S6797/S6797.CsharpOnly.cs#L7",
"region": {
"startLine": 7,
"startColumn": 18,
"endLine": 7,
"endColumn": 34
}
}
},
{
"id": "S101",
"message": "Rename class 'S6797_Partial' to match pascal case naming rules, consider using 'S6797Partial'.",
"location": {
"uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/sources/BlazorSample/BlazorSample/Pages/S6797/S6797.Partial.razor.cs#L5",
"region": {
"startLine": 5,
"startColumn": 26,
"endLine": 5,
"endColumn": 39
}
}
},
{
"id": "S101",
"message": "Rename class 'S6798_CSharpOnly' to match pascal case naming rules, consider using 'S6798CSharpOnly'.",
"location": {
"uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/sources/BlazorSample/BlazorSample/Pages/S6798/S6798.CSharpOnly.cs#L5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,32 @@
"id": "S1451",
"message": "Add or update the header of this file.",
"location": {
"uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/sources/BlazorSample/BlazorSample/Pages/S6797/S6797.CsharpOnly.cs#L1",
"region": {
"startLine": 1,
"startColumn": 1,
"endLine": 1,
"endColumn": 1
}
}
},
{
"id": "S1451",
"message": "Add or update the header of this file.",
"location": {
"uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/sources/BlazorSample/BlazorSample/Pages/S6797/S6797.Partial.razor.cs#L1",
"region": {
"startLine": 1,
"startColumn": 1,
"endLine": 1,
"endColumn": 1
}
}
},
{
"id": "S1451",
"message": "Add or update the header of this file.",
"location": {
"uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/sources/BlazorSample/BlazorSample/Pages/S6798/S6798.CSharpOnly.cs#L1",
"region": {
"startLine": 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@
"id": "S2333",
"message": "'partial' is gratuitous in this context.",
"location": {
"uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/sources/BlazorSample/BlazorSample/Pages/S6797/S6797.Partial.razor.cs#L5",
"region": {
"startLine": 5,
"startColumn": 12,
"endLine": 5,
"endColumn": 19
}
}
},
{
"id": "S2333",
"message": "'partial' is gratuitous in this context.",
"location": {
"uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/sources/BlazorSample/BlazorSample/Pages/S6798/S6798.Partial.razor.cs#L5",
"region": {
"startLine": 5,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"issues": [
{
"id": "S6797",
"message": "Query parameter type 'TimeSpan' is not supported.",
"location": {
"uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/sources/BlazorSample/BlazorSample/Pages/S6797/S6797.CsharpOnly.cs#L11",
"region": {
"startLine": 11,
"startColumn": 16,
"endLine": 11,
"endColumn": 24
}
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"issues": [
{
"id": "S6803",
"message": "Component parameters can only receive query parameter values in routable components.",
"location": {
"uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/sources/BlazorSample/BlazorSample/Pages/S6797/S6797.Partial.razor.cs#L9",
"region": {
"startLine": 9,
"startColumn": 25,
"endLine": 9,
"endColumn": 33
}
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages.S6797
{
[Route("/query-parameters")]
public class S6797_CsharpOnly : ComponentBase
{
[Parameter]
[SupplyParameterFromQuery]
public TimeSpan TimeSpan { get; set; }

[Parameter]
public TimeSpan TimeSpanParam { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@code {
[Parameter]
[SupplyParameterFromQuery]
public bool BoolParam { get; set; }

[Parameter]
[SupplyParameterFromQuery]
public DateTime DateTimeParam { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@page "/query-parameters"
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages.S6797
{
public partial class S6797_Partial : ComponentBase
{
[Parameter]
[SupplyParameterFromQuery]
public TimeSpan TimeSpan { get; set; }

[Parameter]
public TimeSpan TimeSpanParam { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@page "/query-parameters"

@code {
[Parameter]
[SupplyParameterFromQuery]
public bool BoolParam { get; set; }

[Parameter]
[SupplyParameterFromQuery]
public DateTime DateTimeParam { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Microsoft.JSInterop;

namespace BlazorSample.Pages;
namespace BlazorSample.Pages.S6798;

public partial class S6798_Partial
{
Expand Down
51 changes: 51 additions & 0 deletions analyzers/rspec/cs/S6797.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<h2>Why is this an issue?</h2>
<p>The <a
href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.supplyparameterfromqueryattribute">SupplyParameterFromQuery</a>
attribute can be used to specify that a component parameter, of a routable component, comes from the query string.</p>
<p>Component parameters supplied from the query string support the following types:</p>
<ul>
<li> bool, DateTime, decimal, double, float, Guid, int, long, string. </li>
<li> Nullable variants of the preceding types. </li>
<li> Arrays of the preceding types, whether they’re nullable or not nullable. </li>
</ul>
<p>Query parameters should have one of the supported types. Otherwise, an unhandled exception will be raised at runtime.</p>
<pre>
Unhandled exception rendering component: Querystring values cannot be parsed as type '&lt;type&gt;'.
System.NotSupportedException: Querystring values cannot be parsed as type '&lt;type&gt;'
...
</pre>
<h2>How to fix it</h2>
<p>Change the parameter type to one of the following ones:</p>
<ul>
<li> bool, DateTime, decimal, double, float, Guid, int, long, string. </li>
<li> Nullable variants of the preceding types. </li>
<li> Arrays of the preceding types, whether they’re nullable or not nullable. </li>
</ul>
<h3>Code examples</h3>
<h4>Noncompliant code example</h4>
<pre data-diff-id="1" data-diff-type="noncompliant">
@page "/print"
&lt;p&gt; Parameter value is: @Value &lt;/p&gt;
@code {
[Parameter]
[SupplyParameterFromQuery()]
public TimeSpan Value { get; set; } // Noncompliant
}
</pre>
<h4>Compliant solution</h4>
<pre data-diff-id="1" data-diff-type="compliant">
@page "/print"
&lt;p&gt; Parameter value is: @Value &lt;/p&gt;
@code {
[Parameter]
[SupplyParameterFromQuery()]
public long Value { get; set; } // Compliant
}
</pre>
<h2>Resources</h2>
<h3>Documentation</h3>
<ul>
<li> Microsoft Learn - <a
href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.supplyparameterfromqueryattribute">SupplyParameterFromQueryAttribute</a> </li>
</ul>

23 changes: 23 additions & 0 deletions analyzers/rspec/cs/S6797.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"title": "Blazor query parameter type should be supported",
"type": "BUG",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "5min"
},
"tags": [
"blazor"
],
"defaultSeverity": "Major",
"ruleSpecification": "RSPEC-6797",
"sqKey": "S6797",
"scope": "All",
"quickfix": "infeasible",
"code": {
"impacts": {
"RELIABILITY": "MEDIUM"
},
"attribute": "LOGICAL"
}
}
1 change: 1 addition & 0 deletions analyzers/rspec/cs/Sonar_way_profile.json
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@
"S6617",
"S6618",
"S6640",
"S6797",
"S6798",
"S6800",
"S6802",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* 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.
*/

namespace SonarAnalyzer.Rules.CSharp;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class BlazorQueryParameterRoutableComponent : SonarDiagnosticAnalyzer
{
private const string NoRouteQueryDiagnosticId = "S6803";
private const string NoRouteQueryMessageFormat = "Component parameters can only receive query parameter values in routable components.";

private const string QueryTypeDiagnosticId = "S6797";
private const string QueryTypeMessageFormat = "Query parameter type '{0}' is not supported.";

private static readonly DiagnosticDescriptor S6803Rule = DescriptorFactory.Create(NoRouteQueryDiagnosticId, NoRouteQueryMessageFormat);
private static readonly DiagnosticDescriptor S6797Rule = DescriptorFactory.Create(QueryTypeDiagnosticId, QueryTypeMessageFormat);

private static readonly ISet<KnownType> SupportedQueryTypes = new HashSet<KnownType>
{
KnownType.System_Boolean,
KnownType.System_DateTime,
KnownType.System_Decimal,
KnownType.System_Double,
KnownType.System_Single,
KnownType.System_Int32,
KnownType.System_Int64,
KnownType.System_String,
KnownType.System_Guid
};

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(S6803Rule, S6797Rule);

protected override void Initialize(SonarAnalysisContext context) =>
context.RegisterCompilationStartAction(c =>
{
if (c.Compilation.GetTypeByMetadataName(KnownType.Microsoft_AspNetCore_Components_RouteAttribute) is not null)
{
context.RegisterSymbolAction(CheckQueryProperties, SymbolKind.Property);
}
});

private static void CheckQueryProperties(SonarSymbolReportingContext c)
{
var property = (IPropertySymbol)c.Symbol;
if (property.HasAttribute(KnownType.Microsoft_AspNetCore_Components_SupplyParameterFromQueryAttribute)
&& property.HasAttribute(KnownType.Microsoft_AspNetCore_Components_ParameterAttribute))
{
if (!property.ContainingType.HasAttribute(KnownType.Microsoft_AspNetCore_Components_RouteAttribute))
{
foreach (var location in property.Locations)
{
c.ReportIssue(Diagnostic.Create(S6803Rule, location));
}
}
else if (!SupportedQueryTypes.Any(x => IsSupportedType(property.Type, x)))
{
foreach (var propertyType in property.DeclaringSyntaxReferences.Select(x => ((PropertyDeclarationSyntax)x.GetSyntax()).Type))
{
c.ReportIssue(Diagnostic.Create(S6797Rule, propertyType.GetLocation(), GetTypeName(propertyType)));
}
}
}
}

private static bool IsSupportedType(ITypeSymbol type, KnownType supportType)
{
if (type is IArrayTypeSymbol arrayTypeSymbol)
{
type = arrayTypeSymbol.ElementType;
}

if (KnownType.System_Nullable_T.Matches(type))
{
type = ((INamedTypeSymbol)type).TypeArguments[0];
}

return supportType.Matches(type);
}

private static string GetTypeName(TypeSyntax propertyType) =>
propertyType switch
{
GenericNameSyntax genericSyntax when propertyType.NameIs(KnownType.System_Nullable_T.TypeName) => genericSyntax.TypeArgumentList.Arguments[0].GetName(),
{} tuple when TupleTypeSyntaxWrapper.IsInstance(tuple) => KnownType.System_ValueTuple.TypeName,
_ => propertyType.GetName()
};
}
Loading

0 comments on commit 9f6edc4

Please sign in to comment.