File: src\Workspaces\SharedUtilitiesAndExtensions\Workspace\CSharp\CodeGeneration\CSharpDeclarationComparer.cs
Web Access
Project: src\src\CodeStyle\CSharp\CodeFixes\Microsoft.CodeAnalysis.CSharp.CodeStyle.Fixes.csproj (Microsoft.CodeAnalysis.CSharp.CodeStyle.Fixes)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.CodeGeneration;
 
internal class CSharpDeclarationComparer : IComparer<SyntaxNode>
{
    private static readonly Dictionary<SyntaxKind, int> s_kindPrecedenceMap = new(SyntaxFacts.EqualityComparer)
    {
        { SyntaxKind.FieldDeclaration, 0 },
        { SyntaxKind.ConstructorDeclaration, 1 },
        { SyntaxKind.DestructorDeclaration, 2 },
        { SyntaxKind.IndexerDeclaration, 3 },
        { SyntaxKind.PropertyDeclaration, 4 },
        { SyntaxKind.EventFieldDeclaration, 5 },
        { SyntaxKind.EventDeclaration, 6 },
        { SyntaxKind.MethodDeclaration, 7 },
        { SyntaxKind.OperatorDeclaration, 8 },
        { SyntaxKind.ConversionOperatorDeclaration, 9 },
        { SyntaxKind.EnumDeclaration, 10 },
        { SyntaxKind.InterfaceDeclaration, 11 },
        { SyntaxKind.StructDeclaration, 12 },
        { SyntaxKind.ClassDeclaration, 13 },
        { SyntaxKind.RecordDeclaration, 14 },
        { SyntaxKind.RecordStructDeclaration, 15 },
        { SyntaxKind.DelegateDeclaration, 16 }
    };
 
    private static readonly Dictionary<SyntaxKind, int> s_operatorPrecedenceMap = new(SyntaxFacts.EqualityComparer)
    {
        { SyntaxKind.PlusToken, 0 },
        { SyntaxKind.MinusToken, 1 },
        { SyntaxKind.ExclamationToken, 2 },
        { SyntaxKind.TildeToken, 3 },
        { SyntaxKind.PlusPlusToken, 4 },
        { SyntaxKind.MinusMinusToken, 5 },
        { SyntaxKind.AsteriskToken, 6 },
        { SyntaxKind.SlashToken, 7 },
        { SyntaxKind.PercentToken, 8 },
        { SyntaxKind.AmpersandToken, 9 },
        { SyntaxKind.BarToken, 10 },
        { SyntaxKind.CaretToken, 11 },
        { SyntaxKind.LessThanLessThanToken, 12 },
        { SyntaxKind.GreaterThanGreaterThanToken, 13 },
        { SyntaxKind.EqualsEqualsToken, 14 },
        { SyntaxKind.ExclamationEqualsToken, 15 },
        { SyntaxKind.LessThanToken, 16 },
        { SyntaxKind.GreaterThanToken, 17 },
        { SyntaxKind.LessThanEqualsToken, 18 },
        { SyntaxKind.GreaterThanEqualsToken, 19 },
        { SyntaxKind.TrueKeyword, 20 },
        { SyntaxKind.FalseKeyword, 21 },
        { SyntaxKind.GreaterThanGreaterThanGreaterThanToken, 22 },
    };
 
    public static readonly CSharpDeclarationComparer WithNamesInstance = new(includeName: true);
    public static readonly CSharpDeclarationComparer WithoutNamesInstance = new(includeName: false);
 
    private readonly bool _includeName;
 
    private CSharpDeclarationComparer(bool includeName)
        => _includeName = includeName;
 
    public int Compare(SyntaxNode? x, SyntaxNode? y)
    {
        if (ReferenceEquals(x, y))
        {
            return 0;
        }
 
        if (x is null)
        {
            return -1;
        }
 
        if (y is null)
        {
            return 1;
        }
 
        if (x.Kind() != y.Kind())
        {
            if (!s_kindPrecedenceMap.TryGetValue(x.Kind(), out var xPrecedence) ||
                !s_kindPrecedenceMap.TryGetValue(y.Kind(), out var yPrecedence))
            {
                // The containing declaration is malformed and contains a node kind we did not expect.
                // Ignore comparisons with those unexpected nodes and sort them to the end of the declaration.
                return 1;
            }
 
            return xPrecedence < yPrecedence ? -1 : 1;
        }
 
        switch (x.Kind())
        {
            case SyntaxKind.DelegateDeclaration:
                return Compare((DelegateDeclarationSyntax)x, (DelegateDeclarationSyntax)y);
 
            case SyntaxKind.FieldDeclaration:
            case SyntaxKind.EventFieldDeclaration:
                return Compare((BaseFieldDeclarationSyntax)x, (BaseFieldDeclarationSyntax)y);
 
            case SyntaxKind.ConstructorDeclaration:
                return Compare((ConstructorDeclarationSyntax)x, (ConstructorDeclarationSyntax)y);
 
            case SyntaxKind.DestructorDeclaration:
                // All destructors are equal since there can only be one per named type
                return 0;
 
            case SyntaxKind.MethodDeclaration:
                return Compare((MethodDeclarationSyntax)x, (MethodDeclarationSyntax)y);
 
            case SyntaxKind.OperatorDeclaration:
                return Compare((OperatorDeclarationSyntax)x, (OperatorDeclarationSyntax)y);
 
            case SyntaxKind.EventDeclaration:
                return Compare((EventDeclarationSyntax)x, (EventDeclarationSyntax)y);
 
            case SyntaxKind.IndexerDeclaration:
                return Compare((IndexerDeclarationSyntax)x, (IndexerDeclarationSyntax)y);
 
            case SyntaxKind.PropertyDeclaration:
                return Compare((PropertyDeclarationSyntax)x, (PropertyDeclarationSyntax)y);
 
            case SyntaxKind.EnumDeclaration:
                return Compare((EnumDeclarationSyntax)x, (EnumDeclarationSyntax)y);
 
            case SyntaxKind.InterfaceDeclaration:
            case SyntaxKind.StructDeclaration:
            case SyntaxKind.RecordStructDeclaration:
            case SyntaxKind.ClassDeclaration:
            case SyntaxKind.RecordDeclaration:
                return Compare((BaseTypeDeclarationSyntax)x, (BaseTypeDeclarationSyntax)y);
 
            case SyntaxKind.ConversionOperatorDeclaration:
                return Compare((ConversionOperatorDeclarationSyntax)x, (ConversionOperatorDeclarationSyntax)y);
 
            case SyntaxKind.IncompleteMember:
                // Since these are incomplete members they are considered to be equal
                return 0;
            case SyntaxKind.GlobalStatement:
                // for REPL, don't mess with order, just put new one at the end.
                return 1;
            default:
                throw ExceptionUtilities.UnexpectedValue(x.Kind());
        }
    }
 
    private int Compare(DelegateDeclarationSyntax x, DelegateDeclarationSyntax y)
    {
        if (EqualAccessibility(x, x.Modifiers, y, y.Modifiers, out var result))
        {
            if (_includeName)
            {
                EqualIdentifierName(x.Identifier, y.Identifier, out result);
            }
        }
 
        return result;
    }
 
    private int Compare(BaseFieldDeclarationSyntax x, BaseFieldDeclarationSyntax y)
    {
        if (EqualConstness(x.Modifiers, y.Modifiers, out var result) &&
            EqualStaticness(x.Modifiers, y.Modifiers, out result) &&
            EqualReadOnlyness(x.Modifiers, y.Modifiers, out result) &&
            EqualAccessibility(x, x.Modifiers, y, y.Modifiers, out result))
        {
            if (_includeName)
            {
                EqualIdentifierName(
                    x.Declaration.Variables.First().Identifier,
                    y.Declaration.Variables.First().Identifier,
                    out result);
            }
        }
 
        return result;
    }
 
    private static int Compare(ConstructorDeclarationSyntax x, ConstructorDeclarationSyntax y)
    {
        if (EqualStaticness(x.Modifiers, y.Modifiers, out var result) &&
            EqualAccessibility(x, x.Modifiers, y, y.Modifiers, out result))
        {
            EqualParameterCount(x.ParameterList, y.ParameterList, out result);
        }
 
        return result;
    }
 
    private int Compare(MethodDeclarationSyntax x, MethodDeclarationSyntax y)
    {
        if (EqualStaticness(x.Modifiers, y.Modifiers, out var result) &&
            EqualAccessibility(x, x.Modifiers, y, y.Modifiers, out result))
        {
            if (!_includeName)
            {
                EqualIdentifierName(x.Identifier, y.Identifier, out result);
            }
        }
 
        return result;
    }
 
    private static int Compare(ConversionOperatorDeclarationSyntax x, ConversionOperatorDeclarationSyntax y)
    {
        if (x.ImplicitOrExplicitKeyword.Kind() != y.ImplicitOrExplicitKeyword.Kind())
        {
            return x.ImplicitOrExplicitKeyword.Kind() == SyntaxKind.ImplicitKeyword ? -1 : 1;
        }
 
        EqualParameterCount(x.ParameterList, y.ParameterList, out var result);
 
        return result;
    }
 
    private static int Compare(OperatorDeclarationSyntax x, OperatorDeclarationSyntax y)
    {
        if (EqualOperatorPrecedence(x.OperatorToken, y.OperatorToken, out var result))
        {
            EqualParameterCount(x.ParameterList, y.ParameterList, out result);
        }
 
        return result;
    }
 
    private int Compare(EventDeclarationSyntax x, EventDeclarationSyntax y)
    {
        if (EqualStaticness(x.Modifiers, y.Modifiers, out var result) &&
            EqualAccessibility(x, x.Modifiers, y, y.Modifiers, out result))
        {
            if (_includeName)
            {
                EqualIdentifierName(x.Identifier, y.Identifier, out result);
            }
        }
 
        return result;
    }
 
    private static int Compare(IndexerDeclarationSyntax x, IndexerDeclarationSyntax y)
    {
        if (EqualStaticness(x.Modifiers, y.Modifiers, out var result) &&
            EqualAccessibility(x, x.Modifiers, y, y.Modifiers, out result))
        {
            EqualParameterCount(x.ParameterList, y.ParameterList, out result);
        }
 
        return result;
    }
 
    private int Compare(PropertyDeclarationSyntax x, PropertyDeclarationSyntax y)
    {
        if (EqualStaticness(x.Modifiers, y.Modifiers, out var result) &&
            EqualAccessibility(x, x.Modifiers, y, y.Modifiers, out result))
        {
            if (_includeName)
            {
                EqualIdentifierName(x.Identifier, y.Identifier, out result);
            }
        }
 
        return result;
    }
 
    private int Compare(EnumDeclarationSyntax x, EnumDeclarationSyntax y)
    {
        if (EqualAccessibility(x, x.Modifiers, y, y.Modifiers, out var result))
        {
            if (_includeName)
            {
                EqualIdentifierName(x.Identifier, y.Identifier, out result);
            }
        }
 
        return result;
    }
 
    private int Compare(BaseTypeDeclarationSyntax x, BaseTypeDeclarationSyntax y)
    {
        if (EqualStaticness(x.Modifiers, y.Modifiers, out var result) &&
            EqualAccessibility(x, x.Modifiers, y, y.Modifiers, out result))
        {
            if (_includeName)
            {
                EqualIdentifierName(x.Identifier, y.Identifier, out result);
            }
        }
 
        return result;
    }
 
    private static bool ContainsToken(SyntaxTokenList list, SyntaxKind kind)
        => list.Contains(token => token.Kind() == kind);
 
    private enum Accessibility
    {
        Public,
        Protected,
        ProtectedInternal,
        Internal,
        PrivateProtected,
        Private
    }
 
    private static int GetAccessibilityPrecedence(SyntaxTokenList modifiers, SyntaxNode? parent)
    {
        if (ContainsToken(modifiers, SyntaxKind.PublicKeyword))
        {
            return (int)Accessibility.Public;
        }
        else if (ContainsToken(modifiers, SyntaxKind.ProtectedKeyword))
        {
            if (ContainsToken(modifiers, SyntaxKind.InternalKeyword))
            {
                return (int)Accessibility.ProtectedInternal;
            }
 
            if (ContainsToken(modifiers, SyntaxKind.PrivateKeyword))
            {
                return (int)Accessibility.PrivateProtected;
            }
 
            return (int)Accessibility.Protected;
        }
        else if (ContainsToken(modifiers, SyntaxKind.InternalKeyword))
        {
            return (int)Accessibility.Internal;
        }
        else if (ContainsToken(modifiers, SyntaxKind.PrivateKeyword))
        {
            return (int)Accessibility.Private;
        }
 
        // Determine default accessibility: This declaration is internal if we traverse up
        // the syntax tree and don't find a containing named type.
        for (var node = parent; node != null; node = node.Parent)
        {
            if (node.Kind() == SyntaxKind.InterfaceDeclaration)
            {
                // All interface members are public
                return (int)Accessibility.Public;
            }
            else if (node.Kind() is SyntaxKind.StructDeclaration or SyntaxKind.ClassDeclaration or SyntaxKind.RecordDeclaration or SyntaxKind.RecordStructDeclaration)
            {
                // Members and nested types default to private
                return (int)Accessibility.Private;
            }
        }
 
        return (int)Accessibility.Internal;
    }
 
    private static bool BothHaveModifier(SyntaxTokenList x, SyntaxTokenList y, SyntaxKind modifierKind, out int comparisonResult)
    {
        var xHasModifier = ContainsToken(x, modifierKind);
        var yHasModifier = ContainsToken(y, modifierKind);
 
        if (xHasModifier == yHasModifier)
        {
            comparisonResult = 0;
            return true;
        }
 
        comparisonResult = xHasModifier ? -1 : 1;
        return false;
    }
 
    private static bool EqualStaticness(SyntaxTokenList x, SyntaxTokenList y, out int comparisonResult)
        => BothHaveModifier(x, y, SyntaxKind.StaticKeyword, out comparisonResult);
 
    private static bool EqualConstness(SyntaxTokenList x, SyntaxTokenList y, out int comparisonResult)
        => BothHaveModifier(x, y, SyntaxKind.ConstKeyword, out comparisonResult);
 
    private static bool EqualReadOnlyness(SyntaxTokenList x, SyntaxTokenList y, out int comparisonResult)
        => BothHaveModifier(x, y, SyntaxKind.ReadOnlyKeyword, out comparisonResult);
 
    private static bool EqualAccessibility(SyntaxNode x, SyntaxTokenList xModifiers, SyntaxNode y, SyntaxTokenList yModifiers, out int comparisonResult)
    {
        var xAccessibility = GetAccessibilityPrecedence(xModifiers, x.Parent ?? y.Parent);
        var yAccessibility = GetAccessibilityPrecedence(yModifiers, y.Parent ?? x.Parent);
 
        comparisonResult = xAccessibility - yAccessibility;
        return comparisonResult == 0;
    }
 
    private static bool EqualIdentifierName(SyntaxToken x, SyntaxToken y, out int comparisonResult)
    {
        comparisonResult = string.Compare(x.ValueText, y.ValueText, StringComparison.OrdinalIgnoreCase);
        return comparisonResult == 0;
    }
 
    private static bool EqualOperatorPrecedence(SyntaxToken x, SyntaxToken y, out int comparisonResult)
    {
        s_operatorPrecedenceMap.TryGetValue(x.Kind(), out var xPrecedence);
        s_operatorPrecedenceMap.TryGetValue(y.Kind(), out var yPrecedence);
 
        comparisonResult = xPrecedence - yPrecedence;
        return comparisonResult == 0;
    }
 
    private static bool EqualParameterCount(BaseParameterListSyntax x, BaseParameterListSyntax y, out int comparisonResult)
    {
        var xParameterCount = x.Parameters.Count;
        var yParameterCount = y.Parameters.Count;
 
        comparisonResult = xParameterCount - yParameterCount;
 
        return comparisonResult == 0;
    }
}