Skip to content

Commit 42070a9

Browse files
committed
Refactor UrlTemplate.TryParse
1 parent 30bead5 commit 42070a9

5 files changed

Lines changed: 145 additions & 39 deletions

File tree

AspNetCoreAnalyzers.Tests/ASP001ParameterNameTests/CodeFix.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace AspNetCoreAnalyzers.Tests.ASP001ParameterNameTests
88
public class CodeFix
99
{
1010
private static readonly DiagnosticAnalyzer Analyzer = new AttributeAnalyzer();
11-
private static readonly ExpectedDiagnostic ExpectedDiagnostic = Gu.Roslyn.Asserts.ExpectedDiagnostic.Create(ASP001ParameterName.Descriptor);
11+
private static readonly ExpectedDiagnostic ExpectedDiagnostic = ExpectedDiagnostic.Create(ASP001ParameterName.Descriptor);
1212
private static readonly CodeFixProvider Fix = new ParameterNameFix();
1313

1414
[Test]

AspNetCoreAnalyzers.Tests/Helpers/UrlTemplateTests.cs

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,38 @@ namespace AspNetCoreAnalyzers.Tests.Helpers
77

88
public class UrlTemplateTests
99
{
10-
[TestCase("{id}/info", new[] { "{id}", "info" })]
11-
[TestCase("{id?}/info", new[] { "{id?}", "info" })]
12-
[TestCase("{id:int}/info", new[] { "{id:int}", "info" })]
13-
[TestCase("api/orders/{id}", new[] { "api", "orders", "{id}" })]
10+
[TestCase("foo", new[] { "foo" })]
11+
[TestCase("foo/bar", new[] { "foo", "bar" })]
12+
public void TryParse(string text, string[] expected)
13+
{
14+
var syntaxTree = CSharpSyntaxTree.ParseText(@"
15+
namespace ValidCode
16+
{
17+
using System.Threading.Tasks;
18+
using Microsoft.AspNetCore.Mvc;
19+
20+
[ApiController]
21+
public class OrdersController : Controller
22+
{
23+
[HttpGet(""api/orders/{id}"")]
24+
public async Task<IActionResult> GetOrder([FromRoute]int id)
25+
{
26+
}
27+
}
28+
}".AssertReplace("api/orders/{id}", text));
29+
var literal = syntaxTree.FindLiteralExpression(text);
30+
Assert.AreEqual(true, UrlTemplate.TryParse(literal, out var template));
31+
CollectionAssert.AreEqual(expected, template.Path.Select(x => x.Text.Text));
32+
}
33+
34+
[TestCase("{id}", new[] { "{id}" })]
35+
[TestCase("{id}/info", new[] { "{id}", "info" })]
36+
[TestCase("{id?}/info", new[] { "{id?}", "info" })]
37+
[TestCase("{id:int}/info", new[] { "{id:int}", "info" })]
38+
[TestCase("{id:required}/info", new[] { "{id:required}", "info" })]
39+
[TestCase("api/orders/{id}", new[] { "api", "orders", "{id}" })]
1440
[TestCase("api/orders/{id}/info", new[] { "api", "orders", "{id}", "info" })]
15-
public void TryParseWhenInt(string text, string[] expected)
41+
public void TryParseWithParameter(string text, string[] expected)
1642
{
1743
var syntaxTree = CSharpSyntaxTree.ParseText(@"
1844
namespace ValidCode
@@ -34,12 +60,49 @@ public async Task<IActionResult> GetOrder([FromRoute]int id)
3460
CollectionAssert.AreEqual(expected, template.Path.Select(x => x.Text.Text));
3561

3662
// ReSharper disable once PossibleInvalidOperationException
37-
var parameter = template.Path.Single(x => x.Parameter.HasValue).Parameter.Value;
63+
var parameter = template.Path.Single(x => x.Parameter.HasValue)
64+
.Parameter.Value;
65+
Assert.AreEqual("id", parameter.Name.Text);
66+
}
67+
68+
[TestCase("orders/{id:min(1)}", new[] { "orders", "{id:min(1)}" })]
69+
[TestCase("orders/{id:max(2)}", new[] { "orders", "{id:max(2)}" })]
70+
[TestCase("orders/{id:int:max(2)}", new[] { "orders", "{id:int:max(2)}" })]
71+
[TestCase("orders/{id:range(1,23)}", new[] { "orders", "{id:range(1,23)}" })]
72+
public void TryParseWhenIntParameter(string text, string[] expected)
73+
{
74+
var syntaxTree = CSharpSyntaxTree.ParseText(@"
75+
namespace ValidCode
76+
{
77+
using System.Threading.Tasks;
78+
using Microsoft.AspNetCore.Mvc;
79+
80+
[ApiController]
81+
public class OrdersController : Controller
82+
{
83+
[HttpGet(""orders/{id}"")]
84+
public async Task<IActionResult> GetOrder([FromRoute]int id)
85+
{
86+
}
87+
}
88+
}".AssertReplace("orders/{id}", text));
89+
var literal = syntaxTree.FindLiteralExpression(text);
90+
Assert.AreEqual(true, UrlTemplate.TryParse(literal, out var template));
91+
CollectionAssert.AreEqual(expected, template.Path.Select(x => x.Text.Text));
92+
93+
// ReSharper disable once PossibleInvalidOperationException
94+
var parameter = template.Path.Single(x => x.Parameter.HasValue)
95+
.Parameter.Value;
3896
Assert.AreEqual("id", parameter.Name.Text);
3997
}
4098

41-
[TestCase("orders/{id:alpha}", new[] { "orders", "{id:alpha}" })]
42-
public void TryParseWhenString(string text, string[] expected)
99+
[TestCase("orders/{id:alpha}", new[] { "orders", "{id:alpha}" })]
100+
[TestCase("orders/{id:alpha:minlength(1)}", new[] { "orders", "{id:alpha:minlength(1)}" })]
101+
[TestCase("orders/{id:minlength(1)}", new[] { "orders", "{id:minlength(1)}" })]
102+
[TestCase("orders/{id:maxlength(1)}", new[] { "orders", "{id:maxlength(1)}" })]
103+
[TestCase("orders/{id:length(1)}", new[] { "orders", "{id:length(1)}" })]
104+
[TestCase("orders/{id:length(1,2)}", new[] { "orders", "{id:length(1,2)}" })]
105+
public void TryParseWhenStringParameter(string text, string[] expected)
43106
{
44107
var syntaxTree = CSharpSyntaxTree.ParseText(@"
45108
namespace ValidCode
@@ -50,18 +113,19 @@ namespace ValidCode
50113
[ApiController]
51114
public class OrdersController : Controller
52115
{
53-
[HttpGet(""orders/{id:alpha}"")]
116+
[HttpGet(""orders/{id}"")]
54117
public async Task<IActionResult> GetOrder(string id)
55118
{
56119
}
57120
}
58-
}".AssertReplace("api/orders/{id}", text));
121+
}".AssertReplace("orders/{id}", text));
59122
var literal = syntaxTree.FindLiteralExpression(text);
60123
Assert.AreEqual(true, UrlTemplate.TryParse(literal, out var template));
61124
CollectionAssert.AreEqual(expected, template.Path.Select(x => x.Text.Text));
62125

63126
// ReSharper disable once PossibleInvalidOperationException
64-
var parameter = template.Path.Single(x => x.Parameter.HasValue).Parameter.Value;
127+
var parameter = template.Path.Single(x => x.Parameter.HasValue)
128+
.Parameter.Value;
65129
Assert.AreEqual("id", parameter.Name.Text);
66130
}
67131
}

AspNetCoreAnalyzers/Helpers/Component.cs renamed to AspNetCoreAnalyzers/Helpers/PathSegment.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
namespace AspNetCoreAnalyzers
22
{
3+
using System.Diagnostics;
34
using Microsoft.CodeAnalysis.CSharp.Syntax;
45

5-
public struct Component
6+
[DebuggerDisplay("{this.Text.Text}")]
7+
public struct PathSegment
68
{
7-
public Component(LiteralExpressionSyntax literal, int start, int end)
9+
public PathSegment(LiteralExpressionSyntax literal, int start, int end)
810
{
911
this.Text = new TextAndLocation(literal, start, end);
1012
this.Parameter = TemplateParameter.TryParse(this.Text, out var parameter)

AspNetCoreAnalyzers/Helpers/TextAndLocation.cs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,22 @@ namespace AspNetCoreAnalyzers
77
public struct TextAndLocation
88
{
99
private readonly LiteralExpressionSyntax literal;
10-
private readonly int start;
11-
private readonly int end;
1210

1311
public TextAndLocation(LiteralExpressionSyntax literal, int start, int end)
1412
{
1513
this.literal = literal;
16-
this.start = start;
17-
this.end = end;
14+
this.Start = start;
15+
this.End = end;
1816
this.Text = literal.Token.ValueText.Substring(start, end - start);
1917
}
2018

19+
public int Start { get; }
20+
21+
public int End { get; }
22+
2123
public string Text { get; }
2224

23-
public Location Location => Location.Create(this.literal.SyntaxTree, TextSpan.FromBounds(this.literal.SpanStart + this.start, this.literal.SpanStart + this.end));
25+
public Location Location => Location.Create(this.literal.SyntaxTree, TextSpan.FromBounds(this.literal.SpanStart + this.Start, this.literal.SpanStart + this.End));
2426

2527
public static bool operator ==(TextAndLocation left, TextAndLocation right)
2628
{
@@ -34,7 +36,7 @@ public TextAndLocation(LiteralExpressionSyntax literal, int start, int end)
3436

3537
public bool Equals(TextAndLocation other)
3638
{
37-
return this.literal.Equals(other.literal) && this.start == other.start && this.end == other.end;
39+
return this.literal.Equals(other.literal) && this.Start == other.Start && this.End == other.End;
3840
}
3941

4042
public override bool Equals(object obj)
@@ -48,15 +50,15 @@ public override int GetHashCode()
4850
unchecked
4951
{
5052
var hashCode = this.literal.GetHashCode();
51-
hashCode = (hashCode * 397) ^ this.start;
52-
hashCode = (hashCode * 397) ^ this.end;
53+
hashCode = (hashCode * 397) ^ this.Start;
54+
hashCode = (hashCode * 397) ^ this.End;
5355
return hashCode;
5456
}
5557
}
5658

5759
internal TextAndLocation Substring(int index, int length)
5860
{
59-
return new TextAndLocation(this.literal, this.start + index, this.start + index + length);
61+
return new TextAndLocation(this.literal, this.Start + index, this.Start + index + length);
6062
}
6163
}
6264
}

AspNetCoreAnalyzers/Helpers/UrlTemplate.cs

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ namespace AspNetCoreAnalyzers
22
{
33
using System;
44
using System.Collections.Immutable;
5-
using System.Linq;
65
using Microsoft.CodeAnalysis;
76
using Microsoft.CodeAnalysis.CSharp;
87
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -12,15 +11,15 @@ namespace AspNetCoreAnalyzers
1211
/// </summary>
1312
public struct UrlTemplate : IEquatable<UrlTemplate>
1413
{
15-
private UrlTemplate(LiteralExpressionSyntax literal, ImmutableArray<Component> path)
14+
private UrlTemplate(LiteralExpressionSyntax literal, ImmutableArray<PathSegment> path)
1615
{
1716
this.Literal = literal;
1817
this.Path = path;
1918
}
2019

2120
public LiteralExpressionSyntax Literal { get; }
2221

23-
public ImmutableArray<Component> Path { get; }
22+
public ImmutableArray<PathSegment> Path { get; }
2423

2524
public static bool operator ==(UrlTemplate left, UrlTemplate right)
2625
{
@@ -37,23 +36,19 @@ public static bool TryParse(LiteralExpressionSyntax literal, out UrlTemplate tem
3736
if (literal.IsKind(SyntaxKind.StringLiteralExpression))
3837
{
3938
var text = literal.Token.ValueText;
40-
var builder = ImmutableArray.CreateBuilder<Component>(text.Count(x => x == '/') + 1);
39+
var builder = ImmutableArray.CreateBuilder<PathSegment>();
4140
var start = 0;
42-
while (true)
41+
while (TryParse(literal, text, start, out var component))
4342
{
44-
var end = text.IndexOf('/', start);
45-
if (end < 0)
46-
{
47-
builder.Add(new Component(literal, start, text.Length));
48-
break;
49-
}
50-
51-
builder.Add(new Component(literal, start, end));
52-
start = end + 1;
43+
builder.Add(component);
44+
start = component.Text.End;
5345
}
5446

55-
template = new UrlTemplate(literal, builder.MoveToImmutable());
56-
return true;
47+
if (start == text.Length)
48+
{
49+
template = new UrlTemplate(literal, builder.Count == builder.Capacity ? builder.MoveToImmutable() : builder.ToImmutable());
50+
return true;
51+
}
5752
}
5853

5954
template = default(UrlTemplate);
@@ -75,5 +70,48 @@ public override int GetHashCode()
7570
{
7671
return this.Literal.GetHashCode();
7772
}
73+
74+
private static bool TryParse(LiteralExpressionSyntax literal, string text, int start, out PathSegment segment)
75+
{
76+
// https://tools.ietf.org/html/rfc3986
77+
var pos = start;
78+
if (pos < text.Length - 1)
79+
{
80+
if (pos == 0)
81+
{
82+
pos++;
83+
}
84+
else if (text[pos] == '/')
85+
{
86+
pos++;
87+
start++;
88+
}
89+
else
90+
{
91+
segment = default(PathSegment);
92+
return false;
93+
}
94+
95+
while (pos < text.Length)
96+
{
97+
if (text[pos] == '/')
98+
{
99+
segment = new PathSegment(literal, start, pos);
100+
return true;
101+
}
102+
103+
pos++;
104+
}
105+
106+
if (pos == text.Length)
107+
{
108+
segment = new PathSegment(literal, start, pos);
109+
return true;
110+
}
111+
}
112+
113+
segment = default(PathSegment);
114+
return false;
115+
}
78116
}
79117
}

0 commit comments

Comments
 (0)