Skip to content

Commit 3ff564b

Browse files
committed
Refactor template parsing.
1 parent 44a0106 commit 3ff564b

7 files changed

Lines changed: 177 additions & 99 deletions

File tree

AspNetCoreAnalyzers.Tests/Helpers/UrlTemplateTests.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ public async Task<IActionResult> GetOrder([FromRoute]int id)
2929
}".AssertReplace("api/orders/{id}", text));
3030
var literal = syntaxTree.FindLiteralExpression(text);
3131
Assert.AreEqual(true, UrlTemplate.TryParse(literal, out var template));
32-
CollectionAssert.AreEqual(expected, template.Path.Select(x => x.Text));
32+
CollectionAssert.AreEqual(expected, template.Path.Select(x => x.Text.Text));
33+
34+
// ReSharper disable once PossibleInvalidOperationException
35+
var parameter = template.Path.Single(x => x.Parameter.HasValue).Parameter.Value;
36+
Assert.AreEqual("id", parameter.Name.Text);
3337
}
3438
}
3539
}

AspNetCoreAnalyzers/Analyzers/AttributeAnalyzer.cs

Lines changed: 4 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,20 @@ private static void Handle(SyntaxNodeAnalysisContext context)
2525
if (!context.IsExcludedFromAnalysis() &&
2626
context.Node is AttributeSyntax attribute &&
2727
context.ContainingSymbol is IMethodSymbol method &&
28-
attribute.TryFirstAncestor<MethodDeclarationSyntax>(out var methodDeclaration) &&
28+
attribute.TryFirstAncestor(out MethodDeclarationSyntax methodDeclaration) &&
2929
TryGetTemplate(attribute, context, out var template))
3030
{
3131
foreach (var component in template.Path)
3232
{
33-
if (TryGetParameterName(component.Text, out var name))
33+
if (component.Parameter is TemplateParameter parameter)
3434
{
35-
if (method.Parameters.TrySingle(x => IsFromRoute(x), out var single) &&
36-
single.Name != name)
35+
if (method.Parameters.TryFirst(x => IsFromRoute(x) && x.Name != parameter.Name.Text, out var single))
3736
{
3837
context.ReportDiagnostic(
3938
Diagnostic.Create(
4039
ASP001ParameterName.Descriptor,
4140
single.Locations.Single(),
42-
ImmutableDictionary<string, string>.Empty.Add(nameof(NameSyntax), name)));
41+
ImmutableDictionary<string, string>.Empty.Add(nameof(NameSyntax), parameter.Name.Text)));
4342
}
4443

4544
if (!method.Parameters.TryFirst(x => IsFromRoute(x), out _))
@@ -54,51 +53,6 @@ context.ContainingSymbol is IMethodSymbol method &&
5453
}
5554
}
5655

57-
private static bool TryGetParameterName(string text, out string name)
58-
{
59-
var start = text.IndexOf('{');
60-
if (start < 0 ||
61-
text.IndexOf('}') < start ||
62-
text.IndexOf('{', start) > 0)
63-
{
64-
name = null;
65-
return false;
66-
}
67-
68-
start++;
69-
while (start < text.Length &&
70-
text[start] == ' ')
71-
{
72-
start++;
73-
}
74-
75-
var end = text.IndexOf('}', start);
76-
if (end < 0)
77-
{
78-
name = null;
79-
return false;
80-
}
81-
82-
while (text[end] == ' ')
83-
{
84-
end--;
85-
}
86-
87-
for (var i = start; i < end; i++)
88-
{
89-
switch (text[i])
90-
{
91-
case '?':
92-
case ':':
93-
end = i;
94-
break;
95-
}
96-
}
97-
98-
name = text.Substring(start, end - start);
99-
return true;
100-
}
101-
10256
private static bool TryGetTemplate(AttributeSyntax attribute, SyntaxNodeAnalysisContext context, out UrlTemplate template)
10357
{
10458
if (attribute.ArgumentList is AttributeArgumentListSyntax argumentList &&
Lines changed: 7 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,19 @@
11
namespace AspNetCoreAnalyzers
22
{
3-
using System;
4-
using Microsoft.CodeAnalysis;
53
using Microsoft.CodeAnalysis.CSharp.Syntax;
6-
using Microsoft.CodeAnalysis.Text;
74

8-
public struct Component : IEquatable<Component>
5+
public struct Component
96
{
10-
private readonly LiteralExpressionSyntax literal;
11-
private readonly int start;
12-
private readonly int end;
13-
147
public Component(LiteralExpressionSyntax literal, int start, int end)
158
{
16-
this.literal = literal;
17-
this.start = start;
18-
this.end = end;
19-
this.Text = literal.Token.ValueText.Substring(start, end - start);
20-
}
21-
22-
public string Text { get; }
23-
24-
public Location Location => Location.Create(this.literal.SyntaxTree, TextSpan.FromBounds(this.literal.SpanStart + this.start, this.literal.SpanStart + this.end));
25-
26-
public static bool operator ==(Component left, Component right)
27-
{
28-
return left.Equals(right);
29-
}
30-
31-
public static bool operator !=(Component left, Component right)
32-
{
33-
return !left.Equals(right);
34-
}
35-
36-
public bool Equals(Component other)
37-
{
38-
return this.literal.Equals(other.literal) && this.start == other.start && this.end == other.end;
9+
this.Text = new TextAndLocation(literal, start, end);
10+
this.Parameter = TemplateParameter.TryParse(this.Text, out var parameter)
11+
? parameter
12+
: (TemplateParameter?)null;
3913
}
4014

41-
public override bool Equals(object obj)
42-
{
43-
return obj is Component other &&
44-
this.Equals(other);
45-
}
15+
public TextAndLocation Text { get; }
4616

47-
public override int GetHashCode()
48-
{
49-
unchecked
50-
{
51-
var hashCode = this.literal.GetHashCode();
52-
hashCode = (hashCode * 397) ^ this.start;
53-
hashCode = (hashCode * 397) ^ this.end;
54-
return hashCode;
55-
}
56-
}
17+
public TemplateParameter? Parameter { get; }
5718
}
5819
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
namespace AspNetCoreAnalyzers
2+
{
3+
public struct TemplateParameter
4+
{
5+
public TemplateParameter(TextAndLocation name, bool isOptional, TextAndLocation? type)
6+
{
7+
this.Name = name;
8+
this.IsOptional = isOptional;
9+
this.Type = type;
10+
}
11+
12+
public TextAndLocation Name { get; }
13+
14+
public bool IsOptional { get; }
15+
16+
public TextAndLocation? Type { get; }
17+
18+
public static bool TryParse(TextAndLocation textAndLocation, out TemplateParameter result)
19+
{
20+
var text = textAndLocation.Text;
21+
var start = text.IndexOf('{');
22+
if (start < 0 ||
23+
text.IndexOf('}') < start ||
24+
text.IndexOf('{', start) > 0)
25+
{
26+
result = default(TemplateParameter);
27+
return false;
28+
}
29+
30+
start++;
31+
while (start < text.Length &&
32+
text[start] == ' ')
33+
{
34+
start++;
35+
}
36+
37+
var end = text.IndexOf('}', start);
38+
if (end < 0)
39+
{
40+
result = default(TemplateParameter);
41+
return false;
42+
}
43+
44+
while (text[end] == ' ')
45+
{
46+
end--;
47+
}
48+
49+
for (var i = start; i < end; i++)
50+
{
51+
switch (text[i])
52+
{
53+
case '?':
54+
case ':':
55+
end = i;
56+
break;
57+
}
58+
}
59+
60+
result = new TemplateParameter(textAndLocation.Substring(start, end - start), text.Contains("?"), Type());
61+
return true;
62+
63+
TextAndLocation? Type()
64+
{
65+
var typeStart = text.IndexOf(':', start);
66+
if (typeStart < 0)
67+
{
68+
return null;
69+
}
70+
71+
var typeEnd = text.IndexOf('}', typeStart);
72+
if (typeEnd < 0)
73+
{
74+
return null;
75+
}
76+
77+
while (text[typeEnd] == ' ')
78+
{
79+
typeEnd--;
80+
}
81+
82+
return textAndLocation.Substring(typeStart, typeEnd - typeStart);
83+
}
84+
}
85+
}
86+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
namespace AspNetCoreAnalyzers
2+
{
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Text;
6+
7+
public struct TextAndLocation
8+
{
9+
private readonly LiteralExpressionSyntax literal;
10+
private readonly int start;
11+
private readonly int end;
12+
13+
public TextAndLocation(LiteralExpressionSyntax literal, int start, int end)
14+
{
15+
this.literal = literal;
16+
this.start = start;
17+
this.end = end;
18+
this.Text = literal.Token.ValueText.Substring(start, end - start);
19+
}
20+
21+
public string Text { get; }
22+
23+
public Location Location => Location.Create(this.literal.SyntaxTree, TextSpan.FromBounds(this.literal.SpanStart + this.start, this.literal.SpanStart + this.end));
24+
25+
public static bool operator ==(TextAndLocation left, TextAndLocation right)
26+
{
27+
return left.Equals(right);
28+
}
29+
30+
public static bool operator !=(TextAndLocation left, TextAndLocation right)
31+
{
32+
return !left.Equals(right);
33+
}
34+
35+
public bool Equals(TextAndLocation other)
36+
{
37+
return this.literal.Equals(other.literal) && this.start == other.start && this.end == other.end;
38+
}
39+
40+
public override bool Equals(object obj)
41+
{
42+
return obj is TextAndLocation other &&
43+
this.Equals(other);
44+
}
45+
46+
public override int GetHashCode()
47+
{
48+
unchecked
49+
{
50+
var hashCode = this.literal.GetHashCode();
51+
hashCode = (hashCode * 397) ^ this.start;
52+
hashCode = (hashCode * 397) ^ this.end;
53+
return hashCode;
54+
}
55+
}
56+
57+
internal TextAndLocation Substring(int index, int length)
58+
{
59+
return new TextAndLocation(this.literal, this.start + index, this.start + index + length);
60+
}
61+
}
62+
}

ValidCode/Order.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
namespace ValidCode
1+
namespace ValidCode
22
{
3+
using System.Collections.Generic;
4+
35
public class Order
46
{
57
public int Id { get; set; }
8+
9+
public IEnumerable<OrderItem> Items { get; set; }
610
}
7-
}
11+
}

ValidCode/OrderItem.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace ValidCode
2+
{
3+
public class OrderItem
4+
{
5+
public int Id { get; set; }
6+
}
7+
}

0 commit comments

Comments
 (0)