forked from dotnet/aspnetcore
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ambiguous route analyzer (dotnet#45582)
Co-authored-by: Youssef Victor <[email protected]>
- Loading branch information
1 parent
5da2a44
commit f19d4a1
Showing
22 changed files
with
1,095 additions
and
138 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
158 changes: 158 additions & 0 deletions
158
...amework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/AmbiguousRoutePatternComparer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using Microsoft.AspNetCore.Analyzers.Infrastructure.RoutePattern; | ||
|
||
namespace Microsoft.AspNetCore.Analyzers.Infrastructure; | ||
|
||
/// <summary> | ||
/// This route pattern comparer checks to see if two route patterns match the same URL and create ambiguous match exceptions at runtime. | ||
/// It doesn't check two routes exactly equal each other. For example, "/product/{id}" and "/product/{name}" aren't exactly equal but will match the same URL. | ||
/// </summary> | ||
internal sealed class AmbiguousRoutePatternComparer : IEqualityComparer<RoutePatternTree> | ||
{ | ||
public static AmbiguousRoutePatternComparer Instance { get; } = new(); | ||
|
||
public bool Equals(RoutePatternTree x, RoutePatternTree y) | ||
{ | ||
if (x.Root.Parts.Length != y.Root.Parts.Length) | ||
{ | ||
return false; | ||
} | ||
|
||
for (var i = 0; i < x.Root.Parts.Length; i++) | ||
{ | ||
var xPart = x.Root.Parts[i]; | ||
var yPart = y.Root.Parts[i]; | ||
|
||
var equal = xPart switch | ||
{ | ||
RoutePatternSegmentSeparatorNode _ => yPart is RoutePatternSegmentSeparatorNode, | ||
RoutePatternSegmentNode xSegment => Equals(xSegment, yPart as RoutePatternSegmentNode), | ||
_ => throw new InvalidOperationException($"Unexpected part type '{xPart.Kind}'."), | ||
}; | ||
|
||
if (!equal) | ||
{ | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
private static bool Equals(RoutePatternSegmentNode x, RoutePatternSegmentNode? y) | ||
{ | ||
if (y is null) | ||
{ | ||
return false; | ||
} | ||
|
||
if (x.Children.Length != y.Children.Length) | ||
{ | ||
return false; | ||
} | ||
|
||
for (var i = 0; i < x.Children.Length; i++) | ||
{ | ||
var xChild = x.Children[i]; | ||
var yChild = y.Children[i]; | ||
|
||
var equal = xChild switch | ||
{ | ||
RoutePatternOptionalSeparatorNode _ => yChild is RoutePatternOptionalSeparatorNode, | ||
RoutePatternReplacementNode xReplacement => yChild is RoutePatternReplacementNode yReplacement && IgnoreCaseEquals(xReplacement.TextToken.Value, yReplacement.TextToken.Value), | ||
RoutePatternLiteralNode xLiteral => yChild is RoutePatternLiteralNode yLiteral && IgnoreCaseEquals(xLiteral.LiteralToken.Value, yLiteral.LiteralToken.Value), | ||
RoutePatternParameterNode xParameter => Equals(xParameter, yChild as RoutePatternParameterNode), | ||
_ => throw new InvalidOperationException($"Unexpected segment node type '{xChild.Kind}'."), | ||
}; | ||
|
||
if (!equal) | ||
{ | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
private static bool IgnoreCaseEquals(object? value1, object? value2) | ||
{ | ||
var s1 = value1 as string; | ||
var s2 = value2 as string; | ||
|
||
if (s1 is null || s2 is null) | ||
{ | ||
return false; | ||
} | ||
|
||
return string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase); | ||
} | ||
|
||
private static bool Equals(RoutePatternParameterNode x, RoutePatternParameterNode? y) | ||
{ | ||
if (y is null) | ||
{ | ||
return false; | ||
} | ||
|
||
// Only parameter policies differentiate between parameters. | ||
var xParameterPolicies = x.ParameterParts.Where(p => p.Kind == RoutePatternKind.ParameterPolicy).OfType<RoutePatternPolicyParameterPartNode>().ToList(); | ||
var yParameterPolicies = y.ParameterParts.Where(p => p.Kind == RoutePatternKind.ParameterPolicy).OfType<RoutePatternPolicyParameterPartNode>().ToList(); | ||
|
||
if (xParameterPolicies.Count != yParameterPolicies.Count) | ||
{ | ||
return false; | ||
} | ||
|
||
for (var i = 0; i < xParameterPolicies.Count; i++) | ||
{ | ||
var xPolicy = xParameterPolicies[i]; | ||
var yPolicy = yParameterPolicies[i]; | ||
|
||
if (!Equals(xPolicy, yPolicy)) | ||
{ | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
private static bool Equals(RoutePatternPolicyParameterPartNode x, RoutePatternPolicyParameterPartNode y) | ||
{ | ||
if (x.PolicyFragments.Length != y.PolicyFragments.Length) | ||
{ | ||
return false; | ||
} | ||
|
||
for (var i = 0; i < x.PolicyFragments.Length; i++) | ||
{ | ||
var xPart = x.PolicyFragments[i]; | ||
var yPart = y.PolicyFragments[i]; | ||
|
||
var equal = xPart switch | ||
{ | ||
RoutePatternPolicyFragment xFragment => yPart is RoutePatternPolicyFragment yFragment && Equals(xFragment.ArgumentToken.Value, yFragment.ArgumentToken.Value), | ||
RoutePatternPolicyFragmentEscapedNode xFragmentEscaped => yPart is RoutePatternPolicyFragmentEscapedNode yFragmentEscaped && Equals(xFragmentEscaped.ArgumentToken.Value, yFragmentEscaped.ArgumentToken.Value), | ||
_ => throw new InvalidOperationException($"Unexpected policy node type '{xPart.Kind}'."), | ||
}; | ||
|
||
if (!equal) | ||
{ | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
public int GetHashCode(RoutePatternTree obj) | ||
{ | ||
// TODO: Improve hash code calculation. This is rudimentary and will generate a lot of collisions. | ||
return obj.Root.ChildCount; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.