Skip to content

Commit 642c906

Browse files
committed
Escape regexes in parameter constraints. Close #24.
1 parent a78a301 commit 642c906

7 files changed

Lines changed: 152 additions & 6 deletions

File tree

AspNetCoreAnalyzers.Tests/ASP003ParameterTypeTests/ValidCode.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,6 @@ public async Task<IActionResult> GetOrder(long id)
218218
[TestCase("api/orders/{id:length(1,3)}")]
219219
[TestCase("api/orders/{id:alpha}")]
220220
[TestCase("api/orders/{id:regex(a-(0|1))}")]
221-
[TestCase("api/orders/{id:regex(^\\\\d{{3}}-\\\\d{{2}}-\\\\d{{4}}$)}")]
222221
[TestCase("api/orders/{id:required}")]
223222
public void ExplicitString(string template)
224223
{

AspNetCoreAnalyzers.Tests/ASP004ParameterSyntaxTests/ValidCode.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ public class ValidCode
3232
[TestCase("api/orders/{value:length(1,3)}", "string")]
3333
[TestCase("api/orders/{value:alpha}", "string")]
3434
[TestCase("api/orders/{value:regex(a-(0|1))}", "string")]
35-
[TestCase("api/orders/{value:regex(^\\\\d{{3}}-\\\\d{{2}}-\\\\d{{4}}$)}", "string")]
3635
[TestCase("api/orders/{value:regex(a{{0,1}})}", "string")]
3736
[TestCase("api/orders/{value:minlength(1):maxlength(2):required:alpha:regex(a{{0,1}})}", "string")]
3837
[TestCase("api/orders/{value:required}", "string")]
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
namespace AspNetCoreAnalyzers.Tests.ASP005ParameterRegexTests
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(ASP005ParameterRegex.Descriptor);
12+
private static readonly CodeFixProvider Fix = new ParameterSyntaxFix();
13+
14+
[TestCase("api/orders/{id:regex(↓a{1})}", "api/orders/{id:regex(a{{1}})}")]
15+
[TestCase("api/orders/{id:regex(↓^[a-z]{2}$)}", "api/orders/{id:regex(^[[a-z]]{{2}}$)}")]
16+
public void When(string before, string after)
17+
{
18+
var code = @"
19+
namespace ValidCode
20+
{
21+
using Microsoft.AspNetCore.Mvc;
22+
23+
[ApiController]
24+
public class OrdersController : Controller
25+
{
26+
[HttpGet(""api/orders/{id}"")]
27+
public IActionResult GetId(string id)
28+
{
29+
return this.Ok(id);
30+
}
31+
}
32+
}".AssertReplace("api/orders/{id}", before);
33+
34+
var fixedCode = @"
35+
namespace ValidCode
36+
{
37+
using Microsoft.AspNetCore.Mvc;
38+
39+
[ApiController]
40+
public class OrdersController : Controller
41+
{
42+
[HttpGet(""api/orders/{id}"")]
43+
public IActionResult GetId(string id)
44+
{
45+
return this.Ok(id);
46+
}
47+
}
48+
}".AssertReplace("api/orders/{id}", after);
49+
AnalyzerAssert.CodeFix(Analyzer, Fix, ExpectedDiagnostic, code, fixedCode);
50+
}
51+
}
52+
}

AspNetCoreAnalyzers/Analyzers/AttributeAnalyzer.cs

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace AspNetCoreAnalyzers
22
{
33
using System;
4+
using System.Collections.Generic;
45
using System.Collections.Immutable;
56
using System.Linq;
67
using Gu.Roslyn.AnalyzerExtensions;
@@ -89,10 +90,23 @@ context.ContainingSymbol is IMethodSymbol method &&
8990
ASP004ParameterSyntax.Descriptor,
9091
location,
9192
syntax == null
92-
? ImmutableDictionary<string, string>.Empty
93-
: ImmutableDictionary<string, string>.Empty.Add(
94-
nameof(Text),
95-
syntax)));
93+
? ImmutableDictionary<string, string>.Empty
94+
: ImmutableDictionary<string, string>.Empty.Add(
95+
nameof(Text),
96+
syntax)));
97+
}
98+
99+
if (HasWrongRegexSyntax(segment, out location, out syntax))
100+
{
101+
context.ReportDiagnostic(
102+
Diagnostic.Create(
103+
ASP005ParameterRegex.Descriptor,
104+
location,
105+
syntax == null
106+
? ImmutableDictionary<string, string>.Empty
107+
: ImmutableDictionary<string, string>.Empty.Add(
108+
nameof(Text),
109+
syntax)));
96110
}
97111
}
98112
}
@@ -322,5 +336,59 @@ bool HasWrongIntArgumentSyntax(RouteConstraint constraint, string methodName, ou
322336
return false;
323337
}
324338
}
339+
340+
private static bool HasWrongRegexSyntax(PathSegment segment, out Location location, out string correctSyntax)
341+
{
342+
if (segment.Parameter is TemplateParameter parameter)
343+
{
344+
foreach (var constraint in parameter.Constraints)
345+
{
346+
var text = constraint.Span.Text;
347+
if (text.StartsWith("regex(", StringComparison.OrdinalIgnoreCase))
348+
{
349+
for (var i = 6; i < text.Length - 1; i++)
350+
{
351+
if (NotEscaped())
352+
{
353+
var escaped = new List<char>(text.Length - 7);
354+
for (i = 6; i < text.Length - 1; i++)
355+
{
356+
escaped.Add(text[i]);
357+
if (NotEscaped())
358+
{
359+
escaped.Add(text[i]);
360+
}
361+
}
362+
363+
location = constraint.Span.GetLocation(6, text.Length - 7);
364+
correctSyntax = new string(escaped.ToArray());
365+
return true;
366+
}
367+
368+
bool NotEscaped()
369+
{
370+
return NotEscapedChar('\\') ||
371+
NotEscapedChar('{') ||
372+
NotEscapedChar('}') ||
373+
NotEscapedChar('[') ||
374+
NotEscapedChar(']');
375+
376+
bool NotEscapedChar(char c)
377+
{
378+
return text[i] == c &&
379+
text[i - 1] != c &&
380+
text[i - 1] != '\\' &&
381+
text[i + 1] != c;
382+
}
383+
}
384+
}
385+
}
386+
}
387+
}
388+
389+
location = null;
390+
correctSyntax = null;
391+
return false;
392+
}
325393
}
326394
}

AspNetCoreAnalyzers/Helpers/Span.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ public override int GetHashCode()
5757

5858
public Location GetLocation(int start) => Location.Create(this.literal.SyntaxTree, new TextSpan(this.literal.SpanStart + this.TextSpan.Start + start + 1, this.TextSpan.Length));
5959

60+
public Location GetLocation(int start, int length) => Location.Create(this.literal.SyntaxTree, new TextSpan(this.literal.SpanStart + this.TextSpan.Start + start + 1, length));
61+
6062
internal Span Slice(int start, int end)
6163
{
6264
if (start > end)
File renamed without changes.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
namespace ValidCode
2+
{
3+
using Microsoft.AspNetCore.Mvc;
4+
5+
[ApiController]
6+
public class RegexController : Controller
7+
{
8+
[HttpGet("string1/{value:regex(a{{0,1}})}")]
9+
public IActionResult Get1(string value)
10+
{
11+
return this.Ok(value);
12+
}
13+
14+
[HttpGet("string2/{value:regex(\\\\d+)}")]
15+
public IActionResult Get2(string value)
16+
{
17+
return this.Ok(value);
18+
}
19+
20+
[HttpGet("string3/{value:regex(^\\\\d{{3}}-\\\\d{{2}}-\\\\d{{4}}$)}")]
21+
public IActionResult Get3(string value)
22+
{
23+
return this.Ok(value);
24+
}
25+
}
26+
}

0 commit comments

Comments
 (0)