Skip to content

Commit 1d8ffe5

Browse files
committed
UrlTemplate
1 parent e9b5e9b commit 1d8ffe5

12 files changed

Lines changed: 238 additions & 40 deletions

File tree

AspNetCoreAnalyzers.Tests/ASP001ParameterNameTests/CodeFix.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace AspNetCoreAnalyzers.Tests.ASP001ParameterNameTests
55
using Microsoft.CodeAnalysis.Diagnostics;
66
using NUnit.Framework;
77

8-
internal class CodeFix
8+
public class CodeFix
99
{
1010
private static readonly DiagnosticAnalyzer Analyzer = new AttributeAnalyzer();
1111
private static readonly ExpectedDiagnostic ExpectedDiagnostic = Gu.Roslyn.Asserts.ExpectedDiagnostic.Create(ASP001ParameterName.Descriptor);

AspNetCoreAnalyzers.Tests/ASP001ParameterNameTests/ValidCode.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ namespace AspNetCoreAnalyzers.Tests.ASP001ParameterNameTests
44
using Microsoft.CodeAnalysis.Diagnostics;
55
using NUnit.Framework;
66

7-
internal class ValidCode
7+
public class ValidCode
88
{
99
private static readonly DiagnosticAnalyzer Analyzer = new AttributeAnalyzer();
1010

AspNetCoreAnalyzers.Tests/Documentation/Tests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace AspNetCoreAnalyzers.Tests.Documentation
1313
using Microsoft.CodeAnalysis.Diagnostics;
1414
using NUnit.Framework;
1515

16-
internal class Tests
16+
public class Tests
1717
{
1818
private static readonly IReadOnlyList<DiagnosticAnalyzer> Analyzers = typeof(AnalyzerCategory)
1919
.Assembly
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
namespace AspNetCoreAnalyzers.Tests.Helpers
2+
{
3+
using System.Linq;
4+
using Gu.Roslyn.Asserts;
5+
using Microsoft.CodeAnalysis.CSharp;
6+
using NUnit.Framework;
7+
8+
public class UrlTemplateTests
9+
{
10+
[TestCase("{id}/info", new[] { "{id}", "info" })]
11+
[TestCase("api/orders/{id}", new[] { "api", "orders", "{id}" })]
12+
[TestCase("api/orders/{id}/info", new[] { "api", "orders", "{id}", "info" })]
13+
public void TryParse(string text, string[] expected)
14+
{
15+
var syntaxTree = CSharpSyntaxTree.ParseText(@"
16+
namespace ValidCode
17+
{
18+
using System.Threading.Tasks;
19+
using Microsoft.AspNetCore.Mvc;
20+
21+
[ApiController]
22+
public class OrdersController : Controller
23+
{
24+
[HttpGet(""api/orders/{id}"")]
25+
public async Task<IActionResult> GetOrder([FromRoute]int id)
26+
{
27+
}
28+
}
29+
}".AssertReplace("api/orders/{id}", text));
30+
var literal = syntaxTree.FindLiteralExpression(text);
31+
Assert.AreEqual(true, UrlTemplate.TryParse(literal, out var template));
32+
CollectionAssert.AreEqual(expected, template.Path.Select(x => x.Text));
33+
}
34+
}
35+
}

AspNetCoreAnalyzers.Tests/Repro.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace AspNetCoreAnalyzers.Tests
1010
using NUnit.Framework;
1111

1212
[Explicit("Only for digging out test cases.")]
13-
internal class Repro
13+
public class Repro
1414
{
1515
////ReSharper disable once UnusedMember.Local
1616
private static readonly IReadOnlyList<DiagnosticAnalyzer> AllAnalyzers = typeof(HelpLink)

AspNetCoreAnalyzers/Analyzers/AttributeAnalyzer.cs

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ namespace AspNetCoreAnalyzers
22
{
33
using System.Collections.Immutable;
44
using System.Linq;
5-
using System.Text.RegularExpressions;
6-
using AspNetCoreAnalyzers.Helpers;
75
using Gu.Roslyn.AnalyzerExtensions;
86
using Microsoft.CodeAnalysis;
97
using Microsoft.CodeAnalysis.CSharp;
@@ -24,58 +22,84 @@ public override void Initialize(AnalysisContext context)
2422
private static void Handle(SyntaxNodeAnalysisContext context)
2523
{
2624
if (!context.IsExcludedFromAnalysis() &&
27-
context.Node is AttributeSyntax attribute)
25+
context.Node is AttributeSyntax attribute &&
26+
context.ContainingSymbol is IMethodSymbol method &&
27+
TryGetTemplate(attribute, context, out var template))
2828
{
29-
if (context.ContainingSymbol is IMethodSymbol method &&
30-
TryGetTemplate(attribute, context, out var template) &&
31-
TryGetRouteParameter(template, out var name) &&
32-
method.Parameters.TrySingle(x => IsFromRoute(x), out var parameter) &&
33-
parameter.Name != name)
29+
foreach (var component in template.Path)
3430
{
35-
context.ReportDiagnostic(
36-
Diagnostic.Create(
37-
ASP001ParameterName.Descriptor,
38-
parameter.Locations.Single(),
39-
ImmutableDictionary<string, string>.Empty.Add(nameof(NameSyntax), name)));
31+
if (TryGetParameterName(component.Text, out var name))
32+
{
33+
if (method.Parameters.TrySingle(x => IsFromRoute(x), out var single) &&
34+
single.Name != name)
35+
{
36+
context.ReportDiagnostic(
37+
Diagnostic.Create(
38+
ASP001ParameterName.Descriptor,
39+
single.Locations.Single(),
40+
ImmutableDictionary<string, string>.Empty.Add(nameof(NameSyntax), name)));
41+
}
42+
}
4043
}
4144
}
4245
}
4346

44-
private static bool TryGetTemplate(AttributeSyntax attribute, SyntaxNodeAnalysisContext context, out LiteralExpressionSyntax literal)
47+
private static bool TryGetParameterName(string text, out string name)
4548
{
46-
if (attribute.ArgumentList is AttributeArgumentListSyntax argumentList &&
47-
argumentList.Arguments.TrySingle(out var argument) &&
48-
argument.Expression is LiteralExpressionSyntax candidate &&
49-
candidate.IsKind(SyntaxKind.StringLiteralExpression) &&
50-
(Attribute.IsType(attribute, KnownSymbol.HttpGetAttribute, context.SemanticModel, context.CancellationToken) ||
51-
Attribute.IsType(attribute, KnownSymbol.HttpPostAttribute, context.SemanticModel, context.CancellationToken) ||
52-
Attribute.IsType(attribute, KnownSymbol.HttpPutAttribute, context.SemanticModel, context.CancellationToken) ||
53-
Attribute.IsType(attribute, KnownSymbol.HttpDeleteAttribute, context.SemanticModel, context.CancellationToken)))
49+
var start = text.IndexOf('{');
50+
if (start < 0 ||
51+
text.IndexOf('}') < start ||
52+
text.IndexOf('{', start) > 0)
5453
{
55-
literal = candidate;
56-
return true;
54+
name = null;
55+
return false;
5756
}
5857

59-
literal = null;
60-
return false;
58+
start++;
59+
while (start < text.Length &&
60+
text[start] == ' ')
61+
{
62+
start++;
63+
}
64+
65+
var end = text.IndexOf('}', start);
66+
if (end < 0)
67+
{
68+
name = null;
69+
return false;
70+
}
71+
72+
while (text[end] == ' ')
73+
{
74+
end--;
75+
}
76+
77+
name = text.Substring(start, end - start);
78+
return true;
6179
}
6280

63-
private static bool TryGetRouteParameter(LiteralExpressionSyntax literal, out string name)
81+
private static bool TryGetTemplate(AttributeSyntax attribute, SyntaxNodeAnalysisContext context, out UrlTemplate template)
6482
{
65-
var match = Regex.Match(literal.Token.ValueText, @"\{(?<name>\w+)}");
66-
if (match.Success)
83+
if (attribute.ArgumentList is AttributeArgumentListSyntax argumentList &&
84+
argumentList.Arguments.TrySingle(out var argument) &&
85+
argument.Expression is LiteralExpressionSyntax literal &&
86+
literal.IsKind(SyntaxKind.StringLiteralExpression) &&
87+
(Attribute.IsType(attribute, KnownSymbol.HttpGetAttribute, context.SemanticModel, context.CancellationToken) ||
88+
Attribute.IsType(attribute, KnownSymbol.HttpPostAttribute, context.SemanticModel, context.CancellationToken) ||
89+
Attribute.IsType(attribute, KnownSymbol.HttpPutAttribute, context.SemanticModel, context.CancellationToken) ||
90+
Attribute.IsType(attribute, KnownSymbol.HttpDeleteAttribute, context.SemanticModel, context.CancellationToken)) &&
91+
UrlTemplate.TryParse(literal, out template))
6792
{
68-
name = match.Groups["name"].Value;
6993
return true;
7094
}
7195

72-
name = null;
96+
template = default(UrlTemplate);
7397
return false;
7498
}
7599

76-
private static bool IsFromRoute(IParameterSymbol parameter)
100+
private static bool IsFromRoute(IParameterSymbol p)
77101
{
78-
foreach (var attributeData in parameter.GetAttributes())
102+
foreach (var attributeData in p.GetAttributes())
79103
{
80104
if (attributeData.AttributeClass == KnownSymbol.FromRouteAttribute)
81105
{
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
22
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=analyzers/@EntryIndexedValue">True</s:Boolean>
33
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=codefixes/@EntryIndexedValue">True</s:Boolean>
4+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=helpers/@EntryIndexedValue">True</s:Boolean>
5+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=helpers_005Cknownsymbols/@EntryIndexedValue">True</s:Boolean>
46

57
</wpf:ResourceDictionary>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
namespace AspNetCoreAnalyzers
2+
{
3+
using System;
4+
using Microsoft.CodeAnalysis;
5+
using Microsoft.CodeAnalysis.CSharp.Syntax;
6+
using Microsoft.CodeAnalysis.Text;
7+
8+
public struct Component : IEquatable<Component>
9+
{
10+
private readonly LiteralExpressionSyntax literal;
11+
private readonly int start;
12+
private readonly int end;
13+
14+
public Component(LiteralExpressionSyntax literal, int start, int end)
15+
{
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;
39+
}
40+
41+
public override bool Equals(object obj)
42+
{
43+
return obj is Component other &&
44+
this.Equals(other);
45+
}
46+
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+
}
57+
}
58+
}

AspNetCoreAnalyzers/Helpers/KnownSymbol.cs renamed to AspNetCoreAnalyzers/Helpers/KnownSymbols/KnownSymbol.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace AspNetCoreAnalyzers.Helpers
1+
namespace AspNetCoreAnalyzers
22
{
33
using Gu.Roslyn.AnalyzerExtensions;
44

AspNetCoreAnalyzers/Helpers/ObjectType.cs renamed to AspNetCoreAnalyzers/Helpers/KnownSymbols/ObjectType.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace AspNetCoreAnalyzers.Helpers
1+
namespace AspNetCoreAnalyzers
22
{
33
using Gu.Roslyn.AnalyzerExtensions;
44

0 commit comments

Comments
 (0)