Skip to content

Commit 9b6fccd

Browse files
committed
ASP010 Unexpected character in url. #19
1 parent 2de0850 commit 9b6fccd

File tree

9 files changed

+239
-7
lines changed

9 files changed

+239
-7
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
namespace AspNetCoreAnalyzers.Tests.ASP010UrlSyntaxTests
2+
{
3+
using Gu.Roslyn.Asserts;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
using NUnit.Framework;
6+
7+
public class Diagnostics
8+
{
9+
private static readonly DiagnosticAnalyzer Analyzer = new AttributeAnalyzer();
10+
private static readonly ExpectedDiagnostic ExpectedDiagnostic = ExpectedDiagnostic.Create(ASP010UrlSyntax.Descriptor);
11+
12+
[TestCase("\"api/a↓?b/{id}\"")]
13+
public void WhenMethodAttribute(string before)
14+
{
15+
var code = @"
16+
namespace AspBox
17+
{
18+
using Microsoft.AspNetCore.Mvc;
19+
20+
[ApiController]
21+
public class OrdersController : Controller
22+
{
23+
[HttpGet(""api/a↓?b/{id}"")]
24+
public IActionResult GetId(string id)
25+
{
26+
return this.Ok(id);
27+
}
28+
}
29+
}".AssertReplace("\"api/a↓?b/{id}\"", before);
30+
31+
AnalyzerAssert.Diagnostics(Analyzer, ExpectedDiagnostic, code);
32+
}
33+
34+
[TestCase("\"api/a↓?b\"")]
35+
public void WhenRouteAttribute(string before)
36+
{
37+
var code = @"
38+
namespace AspBox
39+
{
40+
using Microsoft.AspNetCore.Mvc;
41+
42+
[Route(""api/a↓?b"")]
43+
[ApiController]
44+
public class OrdersController : Controller
45+
{
46+
[HttpGet(""{id}"")]
47+
public IActionResult GetId(string id)
48+
{
49+
return this.Ok(id);
50+
}
51+
}
52+
}".AssertReplace("\"api/a↓?b\"", before);
53+
54+
AnalyzerAssert.Diagnostics(Analyzer, ExpectedDiagnostic, code);
55+
}
56+
}
57+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
namespace AspNetCoreAnalyzers.Tests.ASP010UrlSyntaxTests
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}\"")]
12+
[TestCase("\"api/orders/{value}\"")]
13+
[TestCase("\"api/two-words/{value}\"")]
14+
public void WithParameter(string parameter)
15+
{
16+
var code = @"
17+
namespace AspBox
18+
{
19+
using System.Threading.Tasks;
20+
using Microsoft.AspNetCore.Mvc;
21+
using Microsoft.EntityFrameworkCore;
22+
23+
[ApiController]
24+
public class OrdersController : Controller
25+
{
26+
[HttpGet(""api/{value}"")]
27+
public IActionResult GetValue(string value)
28+
{
29+
return this.Ok(value);
30+
}
31+
}
32+
}".AssertReplace("\"api/{value}\"", parameter);
33+
AnalyzerAssert.Valid(Analyzer, code);
34+
}
35+
}
36+
}

AspNetCoreAnalyzers.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".docs", ".docs", "{1C271AF2
3737
documentation\ASP006.md = documentation\ASP006.md
3838
documentation\ASP008.md = documentation\ASP008.md
3939
documentation\ASP009.md = documentation\ASP009.md
40+
documentation\ASP010.md = documentation\ASP010.md
4041
README.md = README.md
4142
RELEASE_NOTES.md = RELEASE_NOTES.md
4243
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 ASP010UrlSyntax
6+
{
7+
public const string DiagnosticId = "ASP010";
8+
9+
internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor(
10+
id: DiagnosticId,
11+
title: "Unexpected character in url.",
12+
messageFormat: "Literal sections cannot contain the '{0}' character",
13+
category: AnalyzerCategory.Routing,
14+
defaultSeverity: DiagnosticSeverity.Warning,
15+
isEnabledByDefault: true,
16+
description: "Unexpected character in url.",
17+
helpLinkUri: HelpLink.ForId(DiagnosticId));
18+
}
19+
}

AspNetCoreAnalyzers/Analyzers/AttributeAnalyzer.cs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ public class AttributeAnalyzer : DiagnosticAnalyzer
2323
ASP006ParameterRegex.Descriptor,
2424
ASP007MissingParameter.Descriptor,
2525
ASP008ValidRouteParameterName.Descriptor,
26-
ASP009KebabCaseUrl.Descriptor);
26+
ASP009KebabCaseUrl.Descriptor,
27+
ASP010UrlSyntax.Descriptor);
2728

2829
public override void Initialize(AnalysisContext context)
2930
{
@@ -146,6 +147,15 @@ context.Node is AttributeSyntax attribute &&
146147
segment.Span.GetLocation(),
147148
ImmutableDictionary<string, string>.Empty.Add(nameof(Text), kebabCase)));
148149
}
150+
151+
if (ContainsReservedCharacter(segment, out location))
152+
{
153+
context.ReportDiagnostic(
154+
Diagnostic.Create(
155+
ASP010UrlSyntax.Descriptor,
156+
location,
157+
segment.Span.ToString(location)));
158+
}
149159
}
150160
}
151161
}
@@ -587,5 +597,33 @@ bool IsHumpOrSnakeCased(Span span)
587597
return false;
588598
}
589599
}
600+
601+
/// <summary>
602+
/// https://tools.ietf.org/html/rfc3986#section-2.2.
603+
/// </summary>
604+
private static bool ContainsReservedCharacter(PathSegment segment, out Location location)
605+
{
606+
if (segment.Parameter == null)
607+
{
608+
for (var i = 0; i < segment.Span.Length; i++)
609+
{
610+
switch (segment.Span[i])
611+
{
612+
//case ':':
613+
//case '/':
614+
case '?':
615+
//case '#':
616+
//case '[':
617+
//case ']':
618+
//case '@':
619+
location = segment.Span.GetLocation(i, 1);
620+
return true;
621+
}
622+
}
623+
}
624+
625+
location = null;
626+
return false;
627+
}
590628
}
591629
}

AspNetCoreAnalyzers/Helpers/Span.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ public override string ToString() => this.TextSpan.Length == 0
5555
? string.Empty
5656
: this.Literal.ValueText.Substring(this.TextSpan.Start, this.TextSpan.Length);
5757

58+
public string ToString(Location location) => this.Literal.ToString(location);
59+
5860
public Location GetLocation() => this.Literal.GetLocation(this.TextSpan);
5961

6062
public Location GetLocation(int start, int length) => this.Literal.GetLocation(new TextSpan(this.TextSpan.Start + start, length));

AspNetCoreAnalyzers/Helpers/StringLiteral.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,17 +105,17 @@ public bool Equals(StringLiteral other)
105105

106106
public override bool Equals(object obj)
107107
{
108-
if (ReferenceEquals(null, obj))
109-
{
110-
return false;
111-
}
112-
113-
return obj is StringLiteral other && this.Equals(other);
108+
return obj is StringLiteral other &&
109+
this.Equals(other);
114110
}
115111

116112
public override int GetHashCode()
117113
{
118114
return this.literalExpression.GetHashCode();
119115
}
116+
117+
public string ToString(Location location) => location.SourceSpan.Length == 0
118+
? string.Empty
119+
: this.Text.Substring(location.SourceSpan.Start - this.literalExpression.SpanStart, location.SourceSpan.Length);
120120
}
121121
}

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ Roslyn analyzers for ASP.NET.Core.
4747
<td><a href="https://github.com/DotNetAnalyzers/AspNetCoreAnalyzers/tree/master/documentation/ASP009.md">ASP009</a></td>
4848
<td>Use kebab-cased urls.</td>
4949
</tr>
50+
<tr>
51+
<td><a href="https://github.com/DotNetAnalyzers/AspNetCoreAnalyzers/tree/master/documentation/ASP010.md">ASP010</a></td>
52+
<td>Unexpected character in url.</td>
53+
</tr>
5054
<table>
5155
<!-- end generated table -->
5256

documentation/ASP010.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# ASP010
2+
## Unexpected character in url.
3+
4+
<!-- start generated table -->
5+
<table>
6+
<tr>
7+
<td>CheckId</td>
8+
<td>ASP010</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+
Unexpected character in url.
32+
33+
## Motivation
34+
35+
```cs
36+
[HttpGet(""api/a?b"")]
37+
public IActionResult GetId(string id)
38+
{
39+
...
40+
}
41+
```
42+
43+
Throws an exception at runtime.
44+
45+
## How to fix violations
46+
47+
Fix the url template.
48+
49+
<!-- start generated config severity -->
50+
## Configure severity
51+
52+
### Via ruleset file.
53+
54+
Configure the severity per project, for more info see [MSDN](https://msdn.microsoft.com/en-us/library/dd264949.aspx).
55+
56+
### Via #pragma directive.
57+
```C#
58+
#pragma warning disable ASP010 // Unexpected character in url.
59+
Code violating the rule here
60+
#pragma warning restore ASP010 // Unexpected character in url.
61+
```
62+
63+
Or put this at the top of the file to disable all instances.
64+
```C#
65+
#pragma warning disable ASP010 // Unexpected character in url.
66+
```
67+
68+
### Via attribute `[SuppressMessage]`.
69+
70+
```C#
71+
[System.Diagnostics.CodeAnalysis.SuppressMessage("AspNetCoreAnalyzers.Routing",
72+
"ASP010:Unexpected character in url.",
73+
Justification = "Reason...")]
74+
```
75+
<!-- end generated config severity -->

0 commit comments

Comments
 (0)