Skip to content

Commit e9396b7

Browse files
committed
ASP008 check route parameter name. Close #20.
1 parent 6f26b70 commit e9396b7

File tree

8 files changed

+262
-30
lines changed

8 files changed

+262
-30
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
namespace AspNetCoreAnalyzers.Tests.ASP008ValidRouteParameterNameTests
2+
{
3+
using Gu.Roslyn.Asserts;
4+
using Microsoft.CodeAnalysis.CodeFixes;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
using NUnit.Framework;
7+
8+
public class CodeFix
9+
{
10+
private static readonly DiagnosticAnalyzer Analyzer = new AttributeAnalyzer();
11+
private static readonly ExpectedDiagnostic ExpectedDiagnostic = ExpectedDiagnostic.Create(ASP008ValidRouteParameterName.Descriptor);
12+
private static readonly CodeFixProvider Fix = new TemplateTextFix();
13+
14+
[TestCase("\"api/orders/{↓id }\"", "\"api/orders/{id}\"")]
15+
[TestCase("\"api/orders/{↓ id}\"", "\"api/orders/{id}\"")]
16+
[TestCase("\"api/orders/{↓ id }\"", "\"api/orders/{id}\"")]
17+
public void When(string before, string after)
18+
{
19+
var code = @"
20+
namespace ValidCode
21+
{
22+
using Microsoft.AspNetCore.Mvc;
23+
24+
[ApiController]
25+
public class OrdersController : Controller
26+
{
27+
[HttpGet(""api/orders/{id}"")]
28+
public IActionResult GetId(string id)
29+
{
30+
return this.Ok(id);
31+
}
32+
}
33+
}".AssertReplace("\"api/orders/{id}\"", before);
34+
35+
var fixedCode = @"
36+
namespace ValidCode
37+
{
38+
using Microsoft.AspNetCore.Mvc;
39+
40+
[ApiController]
41+
public class OrdersController : Controller
42+
{
43+
[HttpGet(""api/orders/{id}"")]
44+
public IActionResult GetId(string id)
45+
{
46+
return this.Ok(id);
47+
}
48+
}
49+
}".AssertReplace("\"api/orders/{id}\"", after);
50+
AnalyzerAssert.CodeFix(Analyzer, Fix, ExpectedDiagnostic, code, fixedCode);
51+
}
52+
}
53+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
namespace AspNetCoreAnalyzers.Tests.ASP008ValidRouteParameterNameTests
2+
{
3+
using Gu.Roslyn.Asserts;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
using NUnit.Framework;
6+
7+
public class ValidCode
8+
{
9+
private static readonly DiagnosticAnalyzer Analyzer = new AttributeAnalyzer();
10+
11+
[TestCase("\"{value}\"", "string")]
12+
[TestCase("\"{value?}\"", "string")]
13+
[TestCase("\"{value:bool}\"", "bool")]
14+
[TestCase("\"{value:datetime}\"", "System.DateTime")]
15+
[TestCase("\"{value:decimal}\"", "decimal")]
16+
[TestCase("\"{value:double}\"", "double")]
17+
[TestCase("\"{value:float}\"", "float")]
18+
[TestCase("\"{value:int}\"", "int")]
19+
[TestCase("\"api/orders/{value:int:min(1)}\"", "int")]
20+
[TestCase("\"api/orders/{value:int:max(1)}\"", "int")]
21+
[TestCase("\"api/orders/{value:int:range(1,10)}\"", "int")]
22+
[TestCase("\"api/orders/{value:int:required}\"", "int")]
23+
[TestCase("\"{value:long}\"", "long")]
24+
[TestCase("\"api/orders/{value:min(1)}\"", "long")]
25+
[TestCase("\"api/orders/{value:max(1)}\"", "long")]
26+
[TestCase("\"api/orders/{value:range(1,10)}\"", "long")]
27+
[TestCase("\"api/orders/{value:required}\"", "long")]
28+
[TestCase("\"{value:guid}\"", "System.Guid")]
29+
[TestCase("\"api/orders/{value:minlength(1)}\"", "string")]
30+
[TestCase("\"api/orders/{value:maxlength(1)}\"", "string")]
31+
[TestCase("\"api/orders/{value:length(1)}\"", "string")]
32+
[TestCase("\"api/orders/{value:length(1,3)}\"", "string")]
33+
[TestCase("\"api/orders/{value:alpha}\"", "string")]
34+
[TestCase("\"api/orders/{value:regex(a-(0|1))}\"", "string")]
35+
[TestCase("\"api/orders/{value:regex(^\\\\\\\\d{{3}}-\\\\\\\\d{{2}}-\\\\\\\\d{{4}}$)}\"", "string")]
36+
[TestCase("@\"api/orders/{value:regex(^\\\\\\\\d{{3}}-\\\\\\\\d{{2}}-\\\\\\\\d{{4}}$)}\"", "string")]
37+
[TestCase("@\"api/orders/{value:regex(^\\\\d{{3}}-\\\\d{{2}}-\\\\d{{4}}$)}\"", "string")]
38+
[TestCase("\"api/orders/{value:required}\"", "string")]
39+
public void WithParameter(string parameter, string type)
40+
{
41+
var code = @"
42+
namespace ValidCode
43+
{
44+
using System.Threading.Tasks;
45+
using Microsoft.AspNetCore.Mvc;
46+
using Microsoft.EntityFrameworkCore;
47+
48+
[ApiController]
49+
public class OrdersController : Controller
50+
{
51+
[HttpGet(""api/{value}"")]
52+
public IActionResult GetValue(string value)
53+
{
54+
return this.Ok(value);
55+
}
56+
}
57+
}".AssertReplace("\"api/{value}\"", parameter)
58+
.AssertReplace("string", type);
59+
AnalyzerAssert.Valid(Analyzer, code);
60+
}
61+
}
62+
}

AspNetCoreAnalyzers.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".docs", ".docs", "{1C271AF2
3535
documentation\ASP004.md = documentation\ASP004.md
3636
documentation\ASP005.md = documentation\ASP005.md
3737
documentation\ASP006.md = documentation\ASP006.md
38+
documentation\ASP008.md = documentation\ASP008.md
3839
README.md = README.md
3940
RELEASE_NOTES.md = RELEASE_NOTES.md
4041
EndProjectSection
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace AspNetCoreAnalyzers
2+
{
3+
using Microsoft.CodeAnalysis;
4+
5+
internal static class ASP008ValidRouteParameterName
6+
{
7+
public const string DiagnosticId = "ASP008";
8+
9+
internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor(
10+
id: DiagnosticId,
11+
title: "Invalid route parameter name.",
12+
messageFormat: "Invalid route parameter name.",
13+
category: AnalyzerCategory.Routing,
14+
defaultSeverity: DiagnosticSeverity.Warning,
15+
isEnabledByDefault: true,
16+
description: "Invalid route parameter name.",
17+
helpLinkUri: HelpLink.ForId(DiagnosticId));
18+
}
19+
}

AspNetCoreAnalyzers/Analyzers/AttributeAnalyzer.cs

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ public class AttributeAnalyzer : DiagnosticAnalyzer
2121
ASP004RouteParameterType.Descriptor,
2222
ASP005ParameterSyntax.Descriptor,
2323
ASP006ParameterRegex.Descriptor,
24-
ASP007MissingParameter.Descriptor);
24+
ASP007MissingParameter.Descriptor,
25+
ASP008ValidRouteParameterName.Descriptor);
2526

2627
public override void Initialize(AnalysisContext context)
2728
{
@@ -86,19 +87,15 @@ context.ContainingSymbol is IMethodSymbol method &&
8687
Diagnostic.Create(
8788
ASP003ParameterSymbolType.Descriptor,
8889
parameterSyntax.Type.GetLocation(),
89-
ImmutableDictionary<string, string>.Empty.Add(
90-
nameof(TypeSyntax),
91-
typeName)));
90+
ImmutableDictionary<string, string>.Empty.Add(nameof(TypeSyntax), typeName)));
9291

9392
context.ReportDiagnostic(
9493
Diagnostic.Create(
9594
ASP004RouteParameterType.Descriptor,
9695
constraintLocation,
9796
text == null
9897
? ImmutableDictionary<string, string>.Empty
99-
: ImmutableDictionary<string, string>.Empty.Add(
100-
nameof(Text),
101-
text)));
98+
: ImmutableDictionary<string, string>.Empty.Add(nameof(Text), text)));
10299
}
103100
}
104101

@@ -112,9 +109,7 @@ context.ContainingSymbol is IMethodSymbol method &&
112109
location,
113110
syntax == null
114111
? ImmutableDictionary<string, string>.Empty
115-
: ImmutableDictionary<string, string>.Empty.Add(
116-
nameof(Text),
117-
syntax)));
112+
: ImmutableDictionary<string, string>.Empty.Add(nameof(Text), syntax)));
118113
}
119114

120115
if (HasWrongRegexSyntax(segment, out location, out syntax))
@@ -125,9 +120,18 @@ context.ContainingSymbol is IMethodSymbol method &&
125120
location,
126121
syntax == null
127122
? ImmutableDictionary<string, string>.Empty
128-
: ImmutableDictionary<string, string>.Empty.Add(
129-
nameof(Text),
130-
syntax)));
123+
: ImmutableDictionary<string, string>.Empty.Add(nameof(Text), syntax)));
124+
}
125+
126+
if (HasInvalidName(segment, out location, out var name))
127+
{
128+
context.ReportDiagnostic(
129+
Diagnostic.Create(
130+
ASP008ValidRouteParameterName.Descriptor,
131+
location,
132+
name == null
133+
? ImmutableDictionary<string, string>.Empty
134+
: ImmutableDictionary<string, string>.Empty.Add(nameof(Text), name)));
131135
}
132136
}
133137
}
@@ -154,21 +158,6 @@ argument.Expression is LiteralExpressionSyntax literal &&
154158
return false;
155159
}
156160

157-
private static bool IsFromRoute(IParameterSymbol p)
158-
{
159-
foreach (var attributeData in p.GetAttributes())
160-
{
161-
if (attributeData.AttributeClass == KnownSymbol.FromRouteAttribute)
162-
{
163-
continue;
164-
}
165-
166-
return false;
167-
}
168-
169-
return true;
170-
}
171-
172161
private static PooledList<ParameterPair> GetPairs(UrlTemplate template, IMethodSymbol method)
173162
{
174163
var list = PooledList<ParameterPair>.Borrow();
@@ -192,6 +181,21 @@ private static PooledList<ParameterPair> GetPairs(UrlTemplate template, IMethodS
192181
}
193182

194183
return list;
184+
185+
bool IsFromRoute(IParameterSymbol p)
186+
{
187+
foreach (var attributeData in p.GetAttributes())
188+
{
189+
if (attributeData.AttributeClass == KnownSymbol.FromRouteAttribute)
190+
{
191+
continue;
192+
}
193+
194+
return false;
195+
}
196+
197+
return true;
198+
}
195199
}
196200

197201
private static bool HasWrongType(ParameterPair pair, out string correctType, out Location constraintLocation, out string correctConstraint)
@@ -317,7 +321,8 @@ string GetCorrectConstraintType(RouteConstraint constraint)
317321
constraint.Span.Equals("datetime", StringComparison.Ordinal) ||
318322
constraint.Span.Equals("guid", StringComparison.Ordinal))
319323
{
320-
return parameterSymbol.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat).ToLower();
324+
return parameterSymbol.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)
325+
.ToLower();
321326
}
322327

323328
return null;
@@ -487,5 +492,23 @@ bool NotEscapedChar(char c)
487492
correctSyntax = null;
488493
return false;
489494
}
495+
496+
private static bool HasInvalidName(PathSegment segment, out Location location, out string correctName)
497+
{
498+
if (segment.Parameter is TemplateParameter parameter)
499+
{
500+
if (parameter.Name.StartsWith(" ", StringComparison.OrdinalIgnoreCase) ||
501+
parameter.Name.EndsWith(" ", StringComparison.OrdinalIgnoreCase))
502+
{
503+
location = parameter.Name.GetLocation();
504+
correctName = parameter.Name.ToString().Trim();
505+
return true;
506+
}
507+
}
508+
509+
location = null;
510+
correctName = null;
511+
return false;
512+
}
490513
}
491514
}

AspNetCoreAnalyzers/CodeFixes/TemplateTextFix.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ public class TemplateTextFix : CodeFixProvider
1919
ASP002RouteParameterName.DiagnosticId,
2020
ASP004RouteParameterType.DiagnosticId,
2121
ASP005ParameterSyntax.DiagnosticId,
22-
ASP006ParameterRegex.DiagnosticId);
22+
ASP006ParameterRegex.DiagnosticId,
23+
ASP008ValidRouteParameterName.DiagnosticId);
2324

2425
public override FixAllProvider GetFixAllProvider() => null;
2526

@@ -62,6 +63,8 @@ private static string GetTitle(Diagnostic diagnostic)
6263
return "Fix syntax error.";
6364
case ASP006ParameterRegex.DiagnosticId:
6465
return "Escape regex.";
66+
case ASP008ValidRouteParameterName.DiagnosticId:
67+
return "Fix name.";
6568
default:
6669
throw new InvalidOperationException("Should never get here.");
6770
}

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ Roslyn analyzers for ASP.NET.Core.
3939
<td><a href="https://github.com/DotNetAnalyzers/AspNetCoreAnalyzers/tree/master/documentation/ASP007.md">ASP007</a></td>
4040
<td>The method has no corresponding parameter.</td>
4141
</tr>
42+
<tr>
43+
<td><a href="https://github.com/DotNetAnalyzers/AspNetCoreAnalyzers/tree/master/documentation/ASP008.md">ASP008</a></td>
44+
<td>Invalid route parameter name.</td>
45+
</tr>
4246
<table>
4347
<!-- end generated table -->
4448

documentation/ASP008.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# ASP008
2+
## Invalid route parameter name.
3+
4+
<!-- start generated table -->
5+
<table>
6+
<tr>
7+
<td>CheckId</td>
8+
<td>ASP008</td>
9+
</tr>
10+
<tr>
11+
<td>Severity</td>
12+
<td>Warning</td>
13+
</tr>
14+
<tr>
15+
<td>Enabled</td>
16+
<td>True</td>
17+
</tr>
18+
<tr>
19+
<td>Category</td>
20+
<td>AspNetCoreAnalyzers.Routing</td>
21+
</tr>
22+
<tr>
23+
<td>Code</td>
24+
<td><a href="https://github.com/DotNetAnalyzers/AspNetCoreAnalyzers/blob/master/AspNetCoreAnalyzers/Analyzers/AttributeAnalyzer.cs">AttributeAnalyzer</a></td>
25+
</tr>
26+
</table>
27+
<!-- end generated table -->
28+
29+
## Description
30+
31+
Invalid route parameter name.
32+
33+
## Motivation
34+
35+
ADD MOTIVATION HERE
36+
37+
## How to fix violations
38+
39+
ADD HOW TO FIX VIOLATIONS HERE
40+
41+
<!-- start generated config severity -->
42+
## Configure severity
43+
44+
### Via ruleset file.
45+
46+
Configure the severity per project, for more info see [MSDN](https://msdn.microsoft.com/en-us/library/dd264949.aspx).
47+
48+
### Via #pragma directive.
49+
```C#
50+
#pragma warning disable ASP008 // Invalid route parameter name.
51+
Code violating the rule here
52+
#pragma warning restore ASP008 // Invalid route parameter name.
53+
```
54+
55+
Or put this at the top of the file to disable all instances.
56+
```C#
57+
#pragma warning disable ASP008 // Invalid route parameter name.
58+
```
59+
60+
### Via attribute `[SuppressMessage]`.
61+
62+
```C#
63+
[System.Diagnostics.CodeAnalysis.SuppressMessage("AspNetCoreAnalyzers.Routing",
64+
"ASP008:Invalid route parameter name.",
65+
Justification = "Reason...")]
66+
```
67+
<!-- end generated config severity -->

0 commit comments

Comments
 (0)