Skip to content

Commit 2a0e07d

Browse files
committed
ASP009 Use lowercase urls. Close #13.
Only fixing single word segments. Multiword needs strategy for lower casing like snake_case or kebab-case.
1 parent 0b03e5d commit 2a0e07d

File tree

8 files changed

+219
-3
lines changed

8 files changed

+219
-3
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
namespace AspNetCoreAnalyzers.Tests.ASP009LowercaseUrlsTests
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(ASP009LowercaseUrl.Descriptor);
12+
private static readonly CodeFixProvider Fix = new TemplateTextFix();
13+
14+
[TestCase("\"api/↓Orders/{id}\"", "\"api/orders/{id}\"")]
15+
public void WhenMethodAttribute(string before, string after)
16+
{
17+
var code = @"
18+
namespace ValidCode
19+
{
20+
using Microsoft.AspNetCore.Mvc;
21+
22+
[ApiController]
23+
public class OrdersController : Controller
24+
{
25+
[HttpGet(""api/Orders/{id}"")]
26+
public IActionResult GetId(string id)
27+
{
28+
return this.Ok(id);
29+
}
30+
}
31+
}".AssertReplace("\"api/Orders/{id}\"", before);
32+
33+
var fixedCode = @"
34+
namespace ValidCode
35+
{
36+
using Microsoft.AspNetCore.Mvc;
37+
38+
[ApiController]
39+
public class OrdersController : Controller
40+
{
41+
[HttpGet(""api/orders/{id}"")]
42+
public IActionResult GetId(string id)
43+
{
44+
return this.Ok(id);
45+
}
46+
}
47+
}".AssertReplace("\"api/orders/{id}\"", after);
48+
AnalyzerAssert.CodeFix(Analyzer, Fix, ExpectedDiagnostic, code, fixedCode);
49+
}
50+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
namespace AspNetCoreAnalyzers.Tests.ASP009LowercaseUrlsTests
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/TwoWords/{value}\"")]
14+
public void WithParameter(string parameter)
15+
{
16+
var code = @"
17+
namespace ValidCode
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
@@ -36,6 +36,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".docs", ".docs", "{1C271AF2
3636
documentation\ASP005.md = documentation\ASP005.md
3737
documentation\ASP006.md = documentation\ASP006.md
3838
documentation\ASP008.md = documentation\ASP008.md
39+
documentation\ASP009.md = documentation\ASP009.md
3940
README.md = README.md
4041
RELEASE_NOTES.md = RELEASE_NOTES.md
4142
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 ASP009LowercaseUrl
6+
{
7+
public const string DiagnosticId = "ASP009";
8+
9+
internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor(
10+
id: DiagnosticId,
11+
title: "Use lowercase urls.",
12+
messageFormat: "Use lowercase urls.",
13+
category: AnalyzerCategory.Routing,
14+
defaultSeverity: DiagnosticSeverity.Warning,
15+
isEnabledByDefault: true,
16+
description: "Use lowercase urls.",
17+
helpLinkUri: HelpLink.ForId(DiagnosticId));
18+
}
19+
}

AspNetCoreAnalyzers/Analyzers/AttributeAnalyzer.cs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ public class AttributeAnalyzer : DiagnosticAnalyzer
2222
ASP005ParameterSyntax.Descriptor,
2323
ASP006ParameterRegex.Descriptor,
2424
ASP007MissingParameter.Descriptor,
25-
ASP008ValidRouteParameterName.Descriptor);
25+
ASP008ValidRouteParameterName.Descriptor,
26+
ASP009LowercaseUrl.Descriptor);
2627

2728
public override void Initialize(AnalysisContext context)
2829
{
@@ -133,6 +134,17 @@ context.ContainingSymbol is IMethodSymbol method &&
133134
? ImmutableDictionary<string, string>.Empty
134135
: ImmutableDictionary<string, string>.Empty.Add(nameof(Text), name)));
135136
}
137+
138+
if (IsUpperCase(segment, out var lowercase))
139+
{
140+
context.ReportDiagnostic(
141+
Diagnostic.Create(
142+
ASP009LowercaseUrl.Descriptor,
143+
segment.Span.GetLocation(),
144+
lowercase == null
145+
? ImmutableDictionary<string, string>.Empty
146+
: ImmutableDictionary<string, string>.Empty.Add(nameof(Text), lowercase)));
147+
}
136148
}
137149
}
138150
}
@@ -501,7 +513,8 @@ private static bool HasInvalidName(PathSegment segment, out Location location, o
501513
parameter.Name.EndsWith(" ", StringComparison.OrdinalIgnoreCase))
502514
{
503515
location = parameter.Name.GetLocation();
504-
correctName = parameter.Name.ToString().Trim();
516+
correctName = parameter.Name.ToString()
517+
.Trim();
505518
return true;
506519
}
507520

@@ -521,5 +534,28 @@ private static bool HasInvalidName(PathSegment segment, out Location location, o
521534
correctName = null;
522535
return false;
523536
}
537+
538+
private static bool IsUpperCase(PathSegment segment, out string lowercase)
539+
{
540+
if (segment.Parameter == null &&
541+
segment.Span.Length > 0 &&
542+
char.IsUpper(segment.Span[0]))
543+
{
544+
for (var i = 1; i < segment.Span.Length; i++)
545+
{
546+
if (char.IsUpper(segment.Span[i]))
547+
{
548+
lowercase = null;
549+
return false;
550+
}
551+
}
552+
553+
lowercase = segment.Span.ToString().ToLower();
554+
return true;
555+
}
556+
557+
lowercase = null;
558+
return false;
559+
}
524560
}
525561
}

AspNetCoreAnalyzers/CodeFixes/TemplateTextFix.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ public class TemplateTextFix : CodeFixProvider
2020
ASP004RouteParameterType.DiagnosticId,
2121
ASP005ParameterSyntax.DiagnosticId,
2222
ASP006ParameterRegex.DiagnosticId,
23-
ASP008ValidRouteParameterName.DiagnosticId);
23+
ASP008ValidRouteParameterName.DiagnosticId,
24+
ASP009LowercaseUrl.DiagnosticId);
2425

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

@@ -65,6 +66,8 @@ private static string GetTitle(Diagnostic diagnostic)
6566
return "Escape regex.";
6667
case ASP008ValidRouteParameterName.DiagnosticId:
6768
return "Fix name.";
69+
case ASP009LowercaseUrl.DiagnosticId:
70+
return "To lowercase.";
6871
default:
6972
throw new InvalidOperationException("Should never get here.");
7073
}

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ Roslyn analyzers for ASP.NET.Core.
4343
<td><a href="https://github.com/DotNetAnalyzers/AspNetCoreAnalyzers/tree/master/documentation/ASP008.md">ASP008</a></td>
4444
<td>Invalid route parameter name.</td>
4545
</tr>
46+
<tr>
47+
<td><a href="https://github.com/DotNetAnalyzers/AspNetCoreAnalyzers/tree/master/documentation/ASP009.md">ASP009</a></td>
48+
<td>Use lowercase urls.</td>
49+
</tr>
4650
<table>
4751
<!-- end generated table -->
4852

documentation/ASP009.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# ASP009
2+
## Use lowercase urls.
3+
4+
<!-- start generated table -->
5+
<table>
6+
<tr>
7+
<td>CheckId</td>
8+
<td>ASP009</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+
Use lowercase urls.
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 ASP009 // Use lowercase urls.
51+
Code violating the rule here
52+
#pragma warning restore ASP009 // Use lowercase urls.
53+
```
54+
55+
Or put this at the top of the file to disable all instances.
56+
```C#
57+
#pragma warning disable ASP009 // Use lowercase urls.
58+
```
59+
60+
### Via attribute `[SuppressMessage]`.
61+
62+
```C#
63+
[System.Diagnostics.CodeAnalysis.SuppressMessage("AspNetCoreAnalyzers.Routing",
64+
"ASP009:Use lowercase urls.",
65+
Justification = "Reason...")]
66+
```
67+
<!-- end generated config severity -->

0 commit comments

Comments
 (0)