File: Infrastructure\AmbiguousRoutePatternComparer.cs
Web Access
Project: src\src\Framework\AspNetCoreAnalyzers\src\Analyzers\Microsoft.AspNetCore.App.Analyzers.csproj (Microsoft.AspNetCore.App.Analyzers)
// 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;
    }
}