Skip to content

Commit 82dd4a2

Browse files
committed
Refactor parsing.
1 parent 20efb19 commit 82dd4a2

9 files changed

Lines changed: 305 additions & 208 deletions

File tree

AspNetCoreAnalyzers.Tests/Helpers/UrlTemplateTests.cs

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public async Task<IActionResult> GetOrder([FromRoute]int id)
2828
}".AssertReplace("api/orders/{id}", text));
2929
var literal = syntaxTree.FindLiteralExpression(text);
3030
Assert.AreEqual(true, UrlTemplate.TryParse(literal, out var template));
31-
CollectionAssert.AreEqual(expected, template.Path.Select(x => x.Text.Text));
31+
CollectionAssert.AreEqual(expected, template.Path.Select(x => x.Span.Text));
3232
}
3333

3434
[TestCase("{id}", new[] { "{id}" })]
@@ -57,20 +57,21 @@ public async Task<IActionResult> GetOrder([FromRoute]int id)
5757
}".AssertReplace("api/orders/{id}", text));
5858
var literal = syntaxTree.FindLiteralExpression(text);
5959
Assert.AreEqual(true, UrlTemplate.TryParse(literal, out var template));
60-
CollectionAssert.AreEqual(expected, template.Path.Select(x => x.Text.Text));
60+
CollectionAssert.AreEqual(expected, template.Path.Select(x => x.Span.Text));
6161

6262
// ReSharper disable once PossibleInvalidOperationException
6363
var parameter = template.Path.Single(x => x.Parameter.HasValue)
6464
.Parameter.Value;
6565
Assert.AreEqual("id", parameter.Name.Text);
6666
}
6767

68-
[TestCase("orders/{id:min(1)}", new[] { "orders", "{id:min(1)}" })]
69-
[TestCase("orders/{id:int:min(1):max(2)}", new[] { "orders", "{id:int:min(1):max(2)}" })]
70-
[TestCase("orders/{id:max(2)}", new[] { "orders", "{id:max(2)}" })]
71-
[TestCase("orders/{id:int:max(2)}", new[] { "orders", "{id:int:max(2)}" })]
72-
[TestCase("orders/{id:range(1,23)}", new[] { "orders", "{id:range(1,23)}" })]
73-
public void TryParseWhenIntParameter(string text, string[] expected)
68+
[TestCase("orders/{id?}", new[] { "orders", "{id?}" }, new[] { "?" })]
69+
[TestCase("orders/{id:min(1)}", new[] { "orders", "{id:min(1)}" }, new[] { "min(1)" })]
70+
[TestCase("orders/{id:int:min(1):max(2)}", new[] { "orders", "{id:int:min(1):max(2)}" }, new[] { "int", "min(1)", "max(2)" })]
71+
[TestCase("orders/{id:max(2)}", new[] { "orders", "{id:max(2)}" }, new[] { "max(2)" })]
72+
[TestCase("orders/{id:int:max(2)}", new[] { "orders", "{id:int:max(2)}" }, new[] { "int", "max(2)" })]
73+
[TestCase("orders/{id:range(1,23)}", new[] { "orders", "{id:range(1,23)}" }, new[] { "range(1,23)" })]
74+
public void TryParseWhenIntParameter(string text, string[] segments, string[] constraints)
7475
{
7576
var syntaxTree = CSharpSyntaxTree.ParseText(@"
7677
namespace ValidCode
@@ -89,25 +90,28 @@ public async Task<IActionResult> GetOrder([FromRoute]int id)
8990
}".AssertReplace("orders/{id}", text));
9091
var literal = syntaxTree.FindLiteralExpression(text);
9192
Assert.AreEqual(true, UrlTemplate.TryParse(literal, out var template));
92-
CollectionAssert.AreEqual(expected, template.Path.Select(x => x.Text.Text));
93+
CollectionAssert.AreEqual(segments, template.Path.Select(x => x.Span.Text));
9394

9495
// ReSharper disable once PossibleInvalidOperationException
9596
var parameter = template.Path.Single(x => x.Parameter.HasValue)
9697
.Parameter.Value;
9798
Assert.AreEqual("id", parameter.Name.Text);
99+
CollectionAssert.AreEqual(constraints, parameter.Constraints.Select(x => x.Span.Text));
98100
}
99101

100-
[TestCase("orders/{id?}", new[] { "orders", "{id?}" })]
101-
[TestCase("orders/{id:alpha}", new[] { "orders", "{id:alpha}" })]
102-
[TestCase("orders/{id:alpha:minlength(1)}", new[] { "orders", "{id:alpha:minlength(1)}" })]
103-
[TestCase("orders/{id:minlength(1)}", new[] { "orders", "{id:minlength(1)}" })]
104-
[TestCase("orders/{id:maxlength(1)}", new[] { "orders", "{id:maxlength(1)}" })]
105-
[TestCase("orders/{id:length(1)}", new[] { "orders", "{id:length(1)}" })]
106-
[TestCase("orders/{id:length(1,2)}", new[] { "orders", "{id:length(1,2)}" })]
107-
[TestCase("orders/{id:regex(^\\\\d{{3}}-\\\\d{{2}}-\\\\d{{4}}$)}", new[] { "orders", "{id:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}" })]
108-
[TestCase("orders/{id:regex(a/b)}", new[] { "orders", "{id:regex(a/b)}" })]
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)" })]
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}$)" })]
112+
[TestCase("orders/{id:regex(a/b)}", new[] { "orders", "{id:regex(a/b)}" }, new[] { "regex(a/b)" })]
109113
////[TestCase("orders/{id:regex(a[)}/]b)}", new[] { "orders", "{id:regex(a[)}/]b)}" })]
110-
public void TryParseWhenStringParameter(string text, string[] expected)
114+
public void TryParseWhenStringParameter(string text, string[] segments, string[] constraints)
111115
{
112116
var syntaxTree = CSharpSyntaxTree.ParseText(@"
113117
namespace ValidCode
@@ -126,12 +130,14 @@ public async Task<IActionResult> GetOrder(string id)
126130
}".AssertReplace("orders/{id}", text));
127131
var literal = syntaxTree.FindLiteralExpression(text);
128132
Assert.AreEqual(true, UrlTemplate.TryParse(literal, out var template));
129-
CollectionAssert.AreEqual(expected, template.Path.Select(x => x.Text.Text));
133+
CollectionAssert.AreEqual(segments, template.Path.Select(x => x.Span.Text));
130134

131135
// ReSharper disable once PossibleInvalidOperationException
132136
var parameter = template.Path.Single(x => x.Parameter.HasValue)
133137
.Parameter.Value;
134138
Assert.AreEqual("id", parameter.Name.Text);
139+
CollectionAssert.AreEqual(constraints, parameter.Constraints.Select(x => x.Span.Text)
140+
.ToArray());
135141
}
136142
}
137143
}

AspNetCoreAnalyzers.sln.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
4242
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
4343
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpRenamePlacementToArrangementMigration/@EntryIndexedValue">True</s:Boolean>
44+
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
4445
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
4546
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
4647
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>

AspNetCoreAnalyzers/Analyzers/AttributeAnalyzer.cs

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -141,29 +141,32 @@ private static PooledList<ParameterPair> GetPairs(UrlTemplate template, IMethodS
141141

142142
private static bool TryGetParameterType(ParameterPair pair, out string typeName)
143143
{
144-
if (pair.Template?.Type is TextAndLocation templateType &&
144+
if (pair.Template?.Constraints is ImmutableArray<RouteConstraint> constraints &&
145145
pair.Method is IParameterSymbol parameter)
146146
{
147-
switch (templateType.Text)
147+
foreach (var constraint in constraints)
148148
{
149-
// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-2.2#route-constraint-reference
150-
case "bool" when parameter.Type != KnownSymbol.Boolean:
151-
case "decimal" when parameter.Type != KnownSymbol.Decimal:
152-
case "double" when parameter.Type != KnownSymbol.Float:
153-
case "float" when parameter.Type != KnownSymbol.Double:
154-
case "int" when parameter.Type != KnownSymbol.Int32:
155-
case "long" when parameter.Type != KnownSymbol.Int64:
156-
typeName = templateType.Text;
157-
return true;
158-
case "datetime" when parameter.Type != KnownSymbol.DateTime:
159-
typeName = "System.DateTime";
160-
return true;
161-
case "guid" when parameter.Type != KnownSymbol.Guid:
162-
typeName = "System.Guid";
163-
return true;
164-
case "alpha" when parameter.Type != KnownSymbol.String:
165-
typeName = "string";
166-
return true;
149+
switch (constraint.Span.Text)
150+
{
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:
158+
typeName = constraint.Span.Text;
159+
return true;
160+
case "datetime" when parameter.Type != KnownSymbol.DateTime:
161+
typeName = "System.DateTime";
162+
return true;
163+
case "guid" when parameter.Type != KnownSymbol.Guid:
164+
typeName = "System.Guid";
165+
return true;
166+
case "alpha" when parameter.Type != KnownSymbol.String:
167+
typeName = "string";
168+
return true;
169+
}
167170
}
168171
}
169172

AspNetCoreAnalyzers/Helpers/PathSegment.cs

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,81 @@ namespace AspNetCoreAnalyzers
33
using System.Diagnostics;
44
using Microsoft.CodeAnalysis.CSharp.Syntax;
55

6-
[DebuggerDisplay("{this.Text.Text}")]
6+
[DebuggerDisplay("{this.Span.Text}")]
77
public struct PathSegment
88
{
99
public PathSegment(LiteralExpressionSyntax literal, int start, int end)
1010
{
11-
this.Text = new TextAndLocation(literal, start, end);
12-
this.Parameter = TemplateParameter.TryParse(this.Text, out var parameter)
11+
this.Span = new Span(literal, start, end);
12+
this.Parameter = TemplateParameter.TryParse(this.Span, out var parameter)
1313
? parameter
1414
: (TemplateParameter?)null;
1515
}
1616

17-
public TextAndLocation Text { get; }
17+
public Span Span { get; }
1818

1919
public TemplateParameter? Parameter { get; }
20+
21+
public static bool TryRead(LiteralExpressionSyntax literal, int start, out PathSegment segment)
22+
{
23+
// https://tools.ietf.org/html/rfc3986
24+
var text = literal.Token.ValueText;
25+
var pos = start;
26+
if (pos < text.Length - 1)
27+
{
28+
if (pos == 0)
29+
{
30+
pos++;
31+
}
32+
else if (text[pos] == '/')
33+
{
34+
pos++;
35+
start++;
36+
}
37+
else
38+
{
39+
segment = default(PathSegment);
40+
return false;
41+
}
42+
43+
while (pos < text.Length)
44+
{
45+
if (text[pos] == '/')
46+
{
47+
segment = new PathSegment(literal, start, pos);
48+
return true;
49+
}
50+
51+
if (text[pos] == '(')
52+
{
53+
pos++;
54+
while (Text.TrySkipPast(text, ref pos, ")"))
55+
{
56+
Text.SkipWhiteSpace(text, ref pos);
57+
switch (text[pos])
58+
{
59+
case ':':
60+
break;
61+
case '}':
62+
pos++;
63+
segment = new PathSegment(literal, start, pos);
64+
return true;
65+
}
66+
}
67+
}
68+
69+
pos++;
70+
}
71+
72+
if (pos == text.Length)
73+
{
74+
segment = new PathSegment(literal, start, pos);
75+
return true;
76+
}
77+
}
78+
79+
segment = default(PathSegment);
80+
return false;
81+
}
2082
}
2183
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
namespace AspNetCoreAnalyzers
2+
{
3+
using System;
4+
using System.Diagnostics;
5+
6+
[DebuggerDisplay("{this.Span.Text}")]
7+
public struct RouteConstraint : IEquatable<RouteConstraint>
8+
{
9+
public RouteConstraint(Span span)
10+
{
11+
this.Span = span;
12+
}
13+
14+
public Span Span { get; }
15+
16+
public static bool operator ==(RouteConstraint left, RouteConstraint right)
17+
{
18+
return left.Equals(right);
19+
}
20+
21+
public static bool operator !=(RouteConstraint left, RouteConstraint right)
22+
{
23+
return !left.Equals(right);
24+
}
25+
26+
public static bool TryRead(Span span, int pos, out RouteConstraint constraint)
27+
{
28+
if (pos >= span.TextSpan.End ||
29+
span.Text[pos] != ':')
30+
{
31+
constraint = default(RouteConstraint);
32+
return false;
33+
}
34+
35+
pos++;
36+
for (var i = pos; i < span.TextSpan.Length; i++)
37+
{
38+
switch (span.Text[i])
39+
{
40+
case '(' when Text.TrySkipPast(span.Text, ref i, "):") ||
41+
Text.TrySkipPast(span.Text, ref i, ")}"):
42+
constraint = new RouteConstraint(span.Slice(pos, i - 1));
43+
return true;
44+
case '}':
45+
case ':':
46+
constraint = new RouteConstraint(span.Slice(pos, i));
47+
return true;
48+
}
49+
}
50+
51+
constraint = default(RouteConstraint);
52+
return false;
53+
}
54+
55+
public bool Equals(RouteConstraint other)
56+
{
57+
return this.Span.Equals(other.Span);
58+
}
59+
60+
public override bool Equals(object obj)
61+
{
62+
return obj is RouteConstraint other &&
63+
this.Equals(other);
64+
}
65+
66+
public override int GetHashCode()
67+
{
68+
return this.Span.GetHashCode();
69+
}
70+
}
71+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
namespace AspNetCoreAnalyzers
2+
{
3+
using System;
4+
using System.Diagnostics;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp.Syntax;
7+
using Microsoft.CodeAnalysis.Text;
8+
9+
[DebuggerDisplay("{this.Text}")]
10+
public struct Span : IEquatable<Span>
11+
{
12+
private readonly LiteralExpressionSyntax literal;
13+
14+
public Span(LiteralExpressionSyntax literal, int start, int end)
15+
{
16+
this.literal = literal;
17+
this.TextSpan = new TextSpan(start, end - start);
18+
this.Text = literal.Token.ValueText.Substring(start, end - start);
19+
}
20+
21+
public TextSpan TextSpan { get; }
22+
23+
public string Text { get; }
24+
25+
public Location Location => Location.Create(this.literal.SyntaxTree, this.TextSpan);
26+
27+
public static bool operator ==(Span left, Span right)
28+
{
29+
return left.Equals(right);
30+
}
31+
32+
public static bool operator !=(Span left, Span right)
33+
{
34+
return !left.Equals(right);
35+
}
36+
37+
public bool Equals(Span other)
38+
{
39+
return this.literal.Equals(other.literal) && this.TextSpan == other.TextSpan;
40+
}
41+
42+
public override bool Equals(object obj)
43+
{
44+
return obj is Span other &&
45+
this.Equals(other);
46+
}
47+
48+
public override int GetHashCode()
49+
{
50+
unchecked
51+
{
52+
var hashCode = this.literal.GetHashCode();
53+
hashCode = (hashCode * 397) ^ this.TextSpan.GetHashCode();
54+
return hashCode;
55+
}
56+
}
57+
58+
internal Span Slice(int start, int end)
59+
{
60+
if (start >= end)
61+
{
62+
throw new InvalidOperationException("Expected start to be less than end.");
63+
}
64+
65+
if (end > this.TextSpan.End)
66+
{
67+
throw new InvalidOperationException("Expected end to be less than TextSpan.End.");
68+
}
69+
70+
return new Span(this.literal, this.TextSpan.Start + start, this.TextSpan.Start + end);
71+
}
72+
73+
internal Span Substring(int index, int length)
74+
{
75+
return new Span(this.literal, this.TextSpan.Start + index, this.TextSpan.Start + index + length);
76+
}
77+
78+
internal Span Substring(int index)
79+
{
80+
return new Span(this.literal, this.TextSpan.Start + index, this.TextSpan.Start + index + this.TextSpan.Length);
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)