Skip to content

Commit 68c6824

Browse files
committed
More tests.
1 parent 82dd4a2 commit 68c6824

5 files changed

Lines changed: 181 additions & 23 deletions

File tree

AspNetCoreAnalyzers.Tests/ASP003ParameterTypeTests/CodeFix.cs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,25 @@ public class CodeFix
1111
private static readonly ExpectedDiagnostic ExpectedDiagnostic = ExpectedDiagnostic.Create(ASP003ParameterType.Descriptor);
1212
private static readonly CodeFixProvider Fix = new ParameterTypeFix();
1313

14-
[TestCase("api/orders/{id:int}", "int id")]
15-
[TestCase("api/orders/{id:bool}", "bool id")]
16-
[TestCase("api/orders/{id:datetime}", "System.DateTime id")]
17-
[TestCase("api/orders/{id:decimal}", "decimal id")]
18-
[TestCase("api/orders/{id:double}", "double id")]
19-
[TestCase("api/orders/{id:float}", "float id")]
20-
[TestCase("api/orders/{id:guid}", "System.Guid id")]
21-
[TestCase("api/orders/{id:long}", "long id")]
22-
[TestCase("api/orders/{id:alpha}", "string id")]
14+
[TestCase("api/orders/{id:int}", "int id")]
15+
[TestCase("api/orders/{id:int:min(1)}", "int id")]
16+
[TestCase("api/orders/{id:bool}", "bool id")]
17+
[TestCase("api/orders/{id:datetime}", "System.DateTime id")]
18+
[TestCase("api/orders/{id:decimal}", "decimal id")]
19+
[TestCase("api/orders/{id:double}", "double id")]
20+
[TestCase("api/orders/{id:float}", "float id")]
21+
[TestCase("api/orders/{id:guid}", "System.Guid id")]
22+
[TestCase("api/orders/{id:long}", "long id")]
23+
[TestCase("api/orders/{id:minlength(1)}", "string id")]
24+
[TestCase("api/orders/{id:maxlength(1)}", "string id")]
25+
[TestCase("api/orders/{id:length(1)}", "string id")]
26+
[TestCase("api/orders/{id:length(1,3)}", "string id")]
27+
[TestCase("api/orders/{id:min(1)}", "long id")]
28+
[TestCase("api/orders/{id:max(10)}", "long id")]
29+
[TestCase("api/orders/{id:range(0,10)}", "long id")]
30+
[TestCase("api/orders/{id:alpha}", "string id")]
31+
[TestCase("api/orders/{id:regex(a-(0|1))}", "string id")]
32+
[TestCase("api/orders/{id:regex(^\\\\d{{3}}-\\\\d{{2}}-\\\\d{4}$)}", "string id")]
2333
public void ExplicitType(string template, string parameter)
2434
{
2535
var code = @"

AspNetCoreAnalyzers.Tests/ASP003ParameterTypeTests/ValidCode.cs

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,102 @@ public async Task<IActionResult> GetOrder(int id)
6363
AnalyzerAssert.Valid(Analyzer, order, db, code);
6464
}
6565

66+
[TestCase("{value:bool}", "bool")]
67+
[TestCase("{value:datetime}", "System.DateTime")]
68+
[TestCase("{value:decimal}", "decimal")]
69+
[TestCase("{value:double}", "double")]
70+
[TestCase("{value:float}", "float")]
71+
[TestCase("{value:int}", "int")]
72+
[TestCase("{value:long}", "long")]
73+
[TestCase("{value:guid}", "System.Guid")]
74+
public void ExplicitType(string constraint, string type)
75+
{
76+
var code = @"
77+
namespace ValidCode
78+
{
79+
using System.Threading.Tasks;
80+
using Microsoft.AspNetCore.Mvc;
81+
using Microsoft.EntityFrameworkCore;
82+
83+
[ApiController]
84+
public class OrdersController : Controller
85+
{
86+
[HttpGet(""api/{value}"")]
87+
public async Task<int> GetOrder(int value)
88+
{
89+
return value;
90+
}
91+
}
92+
}".AssertReplace("int", type)
93+
.AssertReplace("{value}", constraint)
94+
;
95+
AnalyzerAssert.Valid(Analyzer, code);
96+
}
97+
6698
[TestCase("api/orders/{id:int}")]
99+
[TestCase("api/orders/{id:int:min(1)}")]
100+
[TestCase("api/orders/{id:int:max(1)}")]
101+
[TestCase("api/orders/{id:int:range(1,10)}")]
102+
[TestCase("api/orders/{id:int:required}")]
103+
public void ExplicitInt(string template)
104+
{
105+
var order = @"
106+
namespace ValidCode
107+
{
108+
public class Order
109+
{
110+
public int Id { get; set; }
111+
}
112+
}";
113+
114+
var db = @"
115+
namespace ValidCode
116+
{
117+
using Microsoft.EntityFrameworkCore;
118+
119+
public class Db : DbContext
120+
{
121+
public DbSet<Order> Orders { get; set; }
122+
}
123+
}";
124+
var code = @"
125+
namespace ValidCode
126+
{
127+
using System.Threading.Tasks;
128+
using Microsoft.AspNetCore.Mvc;
129+
using Microsoft.EntityFrameworkCore;
130+
131+
[ApiController]
132+
public class OrdersController : Controller
133+
{
134+
private readonly Db db;
135+
136+
public OrdersController(Db db)
137+
{
138+
this.db = db;
139+
}
140+
141+
[HttpGet(""api/orders/{id:int}"")]
142+
public async Task<IActionResult> GetOrder(int id)
143+
{
144+
var match = await this.db.Orders.FirstOrDefaultAsync(x => x.Id == id);
145+
if (match == null)
146+
{
147+
return this.NotFound();
148+
}
149+
150+
return this.Ok(match);
151+
}
152+
}
153+
}".AssertReplace("api/orders/{id:int}", template);
154+
AnalyzerAssert.Valid(Analyzer, order, db, code);
155+
}
156+
67157
[TestCase("api/orders/{id:min(1)}")]
68158
[TestCase("api/orders/{id:max(1)}")]
69159
[TestCase("api/orders/{id:range(1,10)}")]
70160
[TestCase("api/orders/{id:required}")]
71-
public void ExplicitInteger(string template)
161+
public void ImplicitLong(string template)
72162
{
73163
var order = @"
74164
namespace ValidCode
@@ -107,7 +197,7 @@ public OrdersController(Db db)
107197
}
108198
109199
[HttpGet(""api/orders/{id:int}"")]
110-
public async Task<IActionResult> GetOrder(int id)
200+
public async Task<IActionResult> GetOrder(long id)
111201
{
112202
var match = await this.db.Orders.FirstOrDefaultAsync(x => x.Id == id);
113203
if (match == null)

AspNetCoreAnalyzers.Tests/Helpers/UrlTemplateTests.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ public async Task<IActionResult> GetOrder([FromRoute]int id)
108108
[TestCase("orders/{id:maxlength(1)}", new[] { "orders", "{id:maxlength(1)}" }, new[] { "maxlength(1)" })]
109109
[TestCase("orders/{id:length(1)}", new[] { "orders", "{id:length(1)}" }, new[] { "length(1)" })]
110110
[TestCase("orders/{id:length(1,2)}", new[] { "orders", "{id:length(1,2)}" }, new[] { "length(1,2)" })]
111-
[TestCase("orders/{id:regex(^\\\\d{3}-\\\\d{2}-\\\\d{4}$)}", new[] { "orders", "{id:regex(^\\d{3}-\\d{2}-\\d{4}$)}" }, new[] { "regex(^\\d{3}-\\d{2}-\\d{4}$)" })]
111+
[TestCase("orders/{id:regex(^\\\\d{{3}}-\\\\d{{2}}-\\\\d{4}$)}", new[] { "orders", "{id:regex(^\\d{{3}}-\\d{{2}}-\\d{4}$)}" }, new[] { "regex(^\\d{{3}}-\\d{{2}}-\\d{4}$)" })]
112112
[TestCase("orders/{id:regex(a/b)}", new[] { "orders", "{id:regex(a/b)}" }, new[] { "regex(a/b)" })]
113113
////[TestCase("orders/{id:regex(a[)}/]b)}", new[] { "orders", "{id:regex(a[)}/]b)}" })]
114114
public void TryParseWhenStringParameter(string text, string[] segments, string[] constraints)
@@ -139,5 +139,36 @@ public async Task<IActionResult> GetOrder(string id)
139139
CollectionAssert.AreEqual(constraints, parameter.Constraints.Select(x => x.Span.Text)
140140
.ToArray());
141141
}
142+
143+
[TestCase("orders/{id:}", new[] { "orders", "{id:}" }, new[] { "" })]
144+
[TestCase("orders/{id:min(1}", new[] { "orders", "{id:min(1}" }, new[] { "min(1" })]
145+
[TestCase("orders/{id:min1)}", new[] { "orders", "{id:min1)}" }, new[] { "min1)" })]
146+
public void TryParseWhenSyntaxError(string text, string[] segments, string[] constraints)
147+
{
148+
var syntaxTree = CSharpSyntaxTree.ParseText(@"
149+
namespace ValidCode
150+
{
151+
using System.Threading.Tasks;
152+
using Microsoft.AspNetCore.Mvc;
153+
154+
[ApiController]
155+
public class OrdersController : Controller
156+
{
157+
[HttpGet(""orders/{id}"")]
158+
public async Task<IActionResult> GetOrder([FromRoute]int id)
159+
{
160+
}
161+
}
162+
}".AssertReplace("orders/{id}", text));
163+
var literal = syntaxTree.FindLiteralExpression(text);
164+
Assert.AreEqual(true, UrlTemplate.TryParse(literal, out var template));
165+
CollectionAssert.AreEqual(segments, template.Path.Select(x => x.Span.Text));
166+
167+
// ReSharper disable once PossibleInvalidOperationException
168+
var parameter = template.Path.Single(x => x.Parameter.HasValue)
169+
.Parameter.Value;
170+
Assert.AreEqual("id", parameter.Name.Text);
171+
CollectionAssert.AreEqual(constraints, parameter.Constraints.Select(x => x.Span.Text));
172+
}
142173
}
143174
}

AspNetCoreAnalyzers/Analyzers/AttributeAnalyzer.cs

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
namespace AspNetCoreAnalyzers
22
{
3+
using System;
34
using System.Collections.Immutable;
45
using System.Linq;
56
using Gu.Roslyn.AnalyzerExtensions;
67
using Microsoft.CodeAnalysis;
78
using Microsoft.CodeAnalysis.CSharp;
89
using Microsoft.CodeAnalysis.CSharp.Syntax;
910
using Microsoft.CodeAnalysis.Diagnostics;
11+
using Attribute = Gu.Roslyn.AnalyzerExtensions.Attribute;
1012

1113
[DiagnosticAnalyzer(LanguageNames.CSharp)]
1214
public class AttributeAnalyzer : DiagnosticAnalyzer
@@ -63,7 +65,7 @@ context.ContainingSymbol is IMethodSymbol method &&
6365

6466
foreach (var pair in pairs)
6567
{
66-
if (TryGetParameterType(pair, out var typeName) &&
68+
if (TryGetCorrectParameterType(pair, out var typeName) &&
6769
methodDeclaration.TryFindParameter(pair.Method?.Name, out parameterSyntax))
6870
{
6971
context.ReportDiagnostic(
@@ -139,24 +141,34 @@ private static PooledList<ParameterPair> GetPairs(UrlTemplate template, IMethodS
139141
return list;
140142
}
141143

142-
private static bool TryGetParameterType(ParameterPair pair, out string typeName)
144+
private static bool TryGetCorrectParameterType(ParameterPair pair, out string typeName)
143145
{
144146
if (pair.Template?.Constraints is ImmutableArray<RouteConstraint> constraints &&
145147
pair.Method is IParameterSymbol parameter)
146148
{
147149
foreach (var constraint in constraints)
148150
{
151+
// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-2.2#route-constraint-reference
149152
switch (constraint.Span.Text)
150153
{
151-
// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-2.2#route-constraint-reference
152-
case "bool" when parameter.Type != KnownSymbol.Boolean:
153-
case "decimal" when parameter.Type != KnownSymbol.Decimal:
154-
case "double" when parameter.Type != KnownSymbol.Float:
155-
case "float" when parameter.Type != KnownSymbol.Double:
156-
case "int" when parameter.Type != KnownSymbol.Int32:
157-
case "long" when parameter.Type != KnownSymbol.Int64:
154+
case "bool":
158155
typeName = constraint.Span.Text;
159-
return true;
156+
return parameter.Type != KnownSymbol.Boolean;
157+
case "decimal":
158+
typeName = constraint.Span.Text;
159+
return parameter.Type != KnownSymbol.Decimal;
160+
case "double":
161+
typeName = constraint.Span.Text;
162+
return parameter.Type != KnownSymbol.Double;
163+
case "float":
164+
typeName = constraint.Span.Text;
165+
return parameter.Type != KnownSymbol.Float;
166+
case "int":
167+
typeName = constraint.Span.Text;
168+
return parameter.Type != KnownSymbol.Int32;
169+
case "long":
170+
typeName = constraint.Span.Text;
171+
return parameter.Type != KnownSymbol.Int64;
160172
case "datetime" when parameter.Type != KnownSymbol.DateTime:
161173
typeName = "System.DateTime";
162174
return true;
@@ -166,6 +178,21 @@ private static bool TryGetParameterType(ParameterPair pair, out string typeName)
166178
case "alpha" when parameter.Type != KnownSymbol.String:
167179
typeName = "string";
168180
return true;
181+
case "required":
182+
continue;
183+
case string text when parameter.Type != KnownSymbol.String &&
184+
(text.StartsWith("regex(", StringComparison.OrdinalIgnoreCase) ||
185+
text.StartsWith("length(", StringComparison.OrdinalIgnoreCase) ||
186+
text.StartsWith("minlength(", StringComparison.OrdinalIgnoreCase) ||
187+
text.StartsWith("maxlength(", StringComparison.OrdinalIgnoreCase)):
188+
typeName = "string";
189+
return true;
190+
case string text when parameter.Type != KnownSymbol.Int64 &&
191+
(text.StartsWith("min(", StringComparison.OrdinalIgnoreCase) ||
192+
text.StartsWith("max(", StringComparison.OrdinalIgnoreCase) ||
193+
text.StartsWith("range(", StringComparison.OrdinalIgnoreCase)):
194+
typeName = "long";
195+
return true;
169196
}
170197
}
171198
}

AspNetCoreAnalyzers/Helpers/Span.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public override int GetHashCode()
5757

5858
internal Span Slice(int start, int end)
5959
{
60-
if (start >= end)
60+
if (start > end)
6161
{
6262
throw new InvalidOperationException("Expected start to be less than end.");
6363
}

0 commit comments

Comments
 (0)