Skip to content

Commit c882505

Browse files
committed
Check opening and closing curlies of route parameters. #22
1 parent 0966416 commit c882505

5 files changed

Lines changed: 92 additions & 33 deletions

File tree

AspNetCoreAnalyzers.Tests/ASP004ParameterSyntaxTests/CodeFix.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ namespace AspNetCoreAnalyzers.Tests.ASP004ParameterSyntaxTests
55
using Microsoft.CodeAnalysis.Diagnostics;
66
using NUnit.Framework;
77

8-
[Explicit("Failing tests.")]
98
public class CodeFix
109
{
1110
private static readonly DiagnosticAnalyzer Analyzer = new AttributeAnalyzer();
1211
private static readonly ExpectedDiagnostic ExpectedDiagnostic = ExpectedDiagnostic.Create(ASP004ParameterSyntax.Descriptor);
13-
private static readonly CodeFixProvider Fix = new ParameterTypeFix();
12+
private static readonly CodeFixProvider Fix = new ParameterSyntaxFix();
1413

1514
[TestCase("api/orders/↓id:long}", "api/orders/{id:long}")]
1615
[TestCase("api/orders/↓{id:long", "api/orders/{id:long}")]
@@ -40,7 +39,7 @@ namespace ValidCode
4039
[ApiController]
4140
public class OrdersController : Controller
4241
{
43-
[HttpGet(""api/orders/{id:ilongnt}"")]
42+
[HttpGet(""api/orders/{id:long}"")]
4443
public IActionResult GetId(long id)
4544
{
4645
return this.Ok(id);

AspNetCoreAnalyzers.Tests/Helpers/UrlTemplateTests.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -99,17 +99,17 @@ public async Task<IActionResult> GetOrder([FromRoute]int id)
9999
CollectionAssert.AreEqual(constraints, parameter.Constraints.Select(x => x.Span.Text));
100100
}
101101

102-
[TestCase("orders/{id}", new[] { "orders", "{id}" }, new string[0])]
103-
[TestCase("orders/{id?}", new[] { "orders", "{id?}" }, new[] { "?" })]
104-
[TestCase("orders/{id:alpha}", new[] { "orders", "{id:alpha}" }, new[] { "alpha" })]
105-
[TestCase("orders/{id:ALPHA}", new[] { "orders", "{id:ALPHA}" }, new[] { "ALPHA" })]
106-
[TestCase("orders/{id:alpha:minlength(1)}", new[] { "orders", "{id:alpha:minlength(1)}" }, new[] { "alpha", "minlength(1)" })]
107-
[TestCase("orders/{id:minlength(1)}", new[] { "orders", "{id:minlength(1)}" }, new[] { "minlength(1)" })]
108-
[TestCase("orders/{id:maxlength(1)}", new[] { "orders", "{id:maxlength(1)}" }, new[] { "maxlength(1)" })]
109-
[TestCase("orders/{id:length(1)}", new[] { "orders", "{id:length(1)}" }, new[] { "length(1)" })]
110-
[TestCase("orders/{id:length(1,2)}", new[] { "orders", "{id:length(1,2)}" }, new[] { "length(1,2)" })]
102+
[TestCase("orders/{id}", new[] { "orders", "{id}" }, new string[0])]
103+
[TestCase("orders/{id?}", new[] { "orders", "{id?}" }, new[] { "?" })]
104+
[TestCase("orders/{id:alpha}", new[] { "orders", "{id:alpha}" }, new[] { "alpha" })]
105+
[TestCase("orders/{id:ALPHA}", new[] { "orders", "{id:ALPHA}" }, new[] { "ALPHA" })]
106+
[TestCase("orders/{id:alpha:minlength(1)}", new[] { "orders", "{id:alpha:minlength(1)}" }, new[] { "alpha", "minlength(1)" })]
107+
[TestCase("orders/{id:minlength(1)}", new[] { "orders", "{id:minlength(1)}" }, new[] { "minlength(1)" })]
108+
[TestCase("orders/{id:maxlength(1)}", new[] { "orders", "{id:maxlength(1)}" }, new[] { "maxlength(1)" })]
109+
[TestCase("orders/{id:length(1)}", new[] { "orders", "{id:length(1)}" }, new[] { "length(1)" })]
110+
[TestCase("orders/{id:length(1,2)}", new[] { "orders", "{id:length(1,2)}" }, new[] { "length(1,2)" })]
111111
[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}$)" })]
112-
[TestCase("orders/{id:regex(a/b)}", new[] { "orders", "{id:regex(a/b)}" }, new[] { "regex(a/b)" })]
112+
[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)
115115
{
@@ -140,7 +140,7 @@ public async Task<IActionResult> GetOrder(string id)
140140
.ToArray());
141141
}
142142

143-
[TestCase("orders/{id:}", new[] { "orders", "{id:}" }, new[] { "" })]
143+
[TestCase("orders/{id:}", new[] { "orders", "{id:}" }, new[] { "" })]
144144
[TestCase("orders/{id:min(1}", new[] { "orders", "{id:min(1}" }, new[] { "min(1" })]
145145
[TestCase("orders/{id:min1)}", new[] { "orders", "{id:min1)}" }, new[] { "min1)" })]
146146
public void TryParseWhenSyntaxError(string text, string[] segments, string[] constraints)

AspNetCoreAnalyzers/Analyzers/AttributeAnalyzer.cs

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ context.ContainingSymbol is IMethodSymbol method &&
6666

6767
foreach (var pair in pairs)
6868
{
69-
if (TryGetCorrectParameterType(pair, out var typeName) &&
69+
if (HasWrongType(pair, out var typeName) &&
7070
methodDeclaration.TryFindParameter(pair.Method?.Name, out parameterSyntax))
7171
{
7272
context.ReportDiagnostic(
@@ -78,6 +78,22 @@ context.ContainingSymbol is IMethodSymbol method &&
7878
typeName)));
7979
}
8080
}
81+
82+
foreach (var segment in template.Path)
83+
{
84+
if (HasWrongSyntax(segment, out var location, out var syntax))
85+
{
86+
context.ReportDiagnostic(
87+
Diagnostic.Create(
88+
ASP004ParameterSyntax.Descriptor,
89+
location,
90+
syntax == null
91+
? ImmutableDictionary<string, string>.Empty
92+
: ImmutableDictionary<string, string>.Empty.Add(
93+
nameof(Text),
94+
syntax)));
95+
}
96+
}
8197
}
8298
}
8399
}
@@ -142,7 +158,7 @@ private static PooledList<ParameterPair> GetPairs(UrlTemplate template, IMethodS
142158
return list;
143159
}
144160

145-
private static bool TryGetCorrectParameterType(ParameterPair pair, out string typeName)
161+
private static bool HasWrongType(ParameterPair pair, out string correctType)
146162
{
147163
if (pair.Template?.Constraints is ImmutableArray<RouteConstraint> constraints &&
148164
pair.Method is IParameterSymbol parameter)
@@ -153,31 +169,31 @@ private static bool TryGetCorrectParameterType(ParameterPair pair, out string ty
153169
switch (constraint.Span.Text)
154170
{
155171
case "bool":
156-
typeName = constraint.Span.Text;
172+
correctType = constraint.Span.Text;
157173
return parameter.Type != KnownSymbol.Boolean;
158174
case "decimal":
159-
typeName = constraint.Span.Text;
175+
correctType = constraint.Span.Text;
160176
return parameter.Type != KnownSymbol.Decimal;
161177
case "double":
162-
typeName = constraint.Span.Text;
178+
correctType = constraint.Span.Text;
163179
return parameter.Type != KnownSymbol.Double;
164180
case "float":
165-
typeName = constraint.Span.Text;
181+
correctType = constraint.Span.Text;
166182
return parameter.Type != KnownSymbol.Float;
167183
case "int":
168-
typeName = constraint.Span.Text;
184+
correctType = constraint.Span.Text;
169185
return parameter.Type != KnownSymbol.Int32;
170186
case "long":
171-
typeName = constraint.Span.Text;
187+
correctType = constraint.Span.Text;
172188
return parameter.Type != KnownSymbol.Int64;
173189
case "datetime" when parameter.Type != KnownSymbol.DateTime:
174-
typeName = "System.DateTime";
190+
correctType = "System.DateTime";
175191
return true;
176192
case "guid" when parameter.Type != KnownSymbol.Guid:
177-
typeName = "System.Guid";
193+
correctType = "System.Guid";
178194
return true;
179195
case "alpha" when parameter.Type != KnownSymbol.String:
180-
typeName = "string";
196+
correctType = "string";
181197
return true;
182198
case "required":
183199
continue;
@@ -186,19 +202,43 @@ private static bool TryGetCorrectParameterType(ParameterPair pair, out string ty
186202
text.StartsWith("length(", StringComparison.OrdinalIgnoreCase) ||
187203
text.StartsWith("minlength(", StringComparison.OrdinalIgnoreCase) ||
188204
text.StartsWith("maxlength(", StringComparison.OrdinalIgnoreCase)):
189-
typeName = "string";
205+
correctType = "string";
190206
return true;
191207
case string text when parameter.Type != KnownSymbol.Int64 &&
192208
(text.StartsWith("min(", StringComparison.OrdinalIgnoreCase) ||
193209
text.StartsWith("max(", StringComparison.OrdinalIgnoreCase) ||
194210
text.StartsWith("range(", StringComparison.OrdinalIgnoreCase)):
195-
typeName = "long";
211+
correctType = "long";
196212
return true;
197213
}
198214
}
199215
}
200216

201-
typeName = null;
217+
correctType = null;
218+
return false;
219+
}
220+
221+
private static bool HasWrongSyntax(PathSegment segment, out Location location, out string correctSyntax)
222+
{
223+
var text = segment.Span.Text;
224+
if (text.StartsWith("{", StringComparison.Ordinal) &&
225+
!text.EndsWith("}", StringComparison.Ordinal))
226+
{
227+
location = segment.Span.GetLocation();
228+
correctSyntax = text + "}";
229+
return true;
230+
}
231+
232+
if (!text.StartsWith("{", StringComparison.Ordinal) &&
233+
text.EndsWith("}", StringComparison.Ordinal))
234+
{
235+
location = segment.Span.GetLocation();
236+
correctSyntax = "{" + text;
237+
return true;
238+
}
239+
240+
location = null;
241+
correctSyntax = null;
202242
return false;
203243
}
204244
}
Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,43 @@
11
namespace AspNetCoreAnalyzers
22
{
33
using System.Collections.Immutable;
4+
using System.Threading;
45
using System.Threading.Tasks;
56
using Gu.Roslyn.CodeFixExtensions;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.CodeActions;
9+
using Microsoft.CodeAnalysis.CodeFixes;
10+
using Microsoft.CodeAnalysis.CSharp.Syntax;
611

7-
public class ParameterSyntaxFix : DocumentEditorCodeFixProvider
12+
public class ParameterSyntaxFix : CodeFixProvider
813
{
914
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(
1015
ASP004ParameterSyntax.DiagnosticId);
1116

12-
protected override async Task RegisterCodeFixesAsync(DocumentEditorCodeFixContext context)
17+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
1318
{
1419
var syntaxRoot = await context.Document.GetSyntaxRootAsync(context.CancellationToken)
1520
.ConfigureAwait(false);
1621

1722
foreach (var diagnostic in context.Diagnostics)
1823
{
24+
if (syntaxRoot.TryFindNodeOrAncestor(diagnostic, out LiteralExpressionSyntax literal) &&
25+
diagnostic.Properties.TryGetValue(nameof(Text), out var text))
26+
{
27+
context.RegisterCodeFix(
28+
CodeAction.Create(
29+
"Fix syntax error.",
30+
_ => Fix(_)),
31+
diagnostic);
1932

33+
async Task<Document> Fix(CancellationToken cancellationToken)
34+
{
35+
var sourceText = await context.Document.GetTextAsync(cancellationToken)
36+
.ConfigureAwait(false);
37+
return context.Document.WithText(sourceText.Replace(diagnostic.Location.SourceSpan, text));
38+
}
39+
}
2040
}
2141
}
2242
}
23-
}
43+
}

AspNetCoreAnalyzers/Helpers/Span.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ public Span(LiteralExpressionSyntax literal, int start, int end)
2222

2323
public string Text { get; }
2424

25-
public Location Location => Location.Create(this.literal.SyntaxTree, this.TextSpan);
26-
2725
public static bool operator ==(Span left, Span right)
2826
{
2927
return left.Equals(right);
@@ -55,6 +53,8 @@ public override int GetHashCode()
5553
}
5654
}
5755

56+
public Location GetLocation() => Location.Create(this.literal.SyntaxTree, new TextSpan(this.literal.SpanStart + this.TextSpan.Start + 1, this.TextSpan.Length));
57+
5858
internal Span Slice(int start, int end)
5959
{
6060
if (start > end)

0 commit comments

Comments
 (0)