File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\Core\Utilities\SymbolEquivalenceComparer.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// 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.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Shared.Utilities;
 
/// <summary>
/// Provides a way to test two symbols for equivalence.  While there are ways to ask for
/// different sorts of equivalence, the following must hold for two symbols to be considered
/// equivalent.
/// <list type="number">
/// <item>The kinds of the two symbols must match.</item>
/// <item>The names of the two symbols must match.</item>
/// <item>The arity of the two symbols must match.</item>
/// <item>If the symbols are methods or parameterized properties, then the signatures of the two
/// symbols must match.</item>
/// <item>Both symbols must be definitions or must be instantiations.  If they are instantiations,
/// then they must be instantiated in the same manner.</item>
/// <item>The containing symbols of the two symbols must be equivalent.</item>
/// <item>Nullability of symbols is not involved in the comparison.</item>
/// </list>
/// Note: equivalence does not concern itself with whole symbols.  Two types are considered
/// equivalent if the above hold, even if one type has different members than the other.  Note:
/// type parameters, and signature parameters are not considered 'children' when comparing
/// symbols.
/// 
/// Options are provided to tweak the above slightly.  For example, by default, symbols are
/// equivalent only if they come from the same assembly or different assemblies of the same simple name.
/// However, one can ask if two symbols are equivalent even if their assemblies differ.
/// </summary>
internal sealed partial class SymbolEquivalenceComparer : IEqualityComparer<ISymbol?>
{
    private readonly ImmutableArray<EquivalenceVisitor> _equivalenceVisitors;
    private readonly ImmutableArray<GetHashCodeVisitor> _getHashCodeVisitors;
 
    public static readonly SymbolEquivalenceComparer Instance = Create(distinguishRefFromOut: false, tupleNamesMustMatch: false, ignoreNullableAnnotations: true, objectAndDynamicCompareEqually: true, arrayAndReadOnlySpanCompareEqually: false);
    public static readonly SymbolEquivalenceComparer TupleNamesMustMatchInstance = Create(distinguishRefFromOut: false, tupleNamesMustMatch: true, ignoreNullableAnnotations: true, objectAndDynamicCompareEqually: true, arrayAndReadOnlySpanCompareEqually: false);
    public static readonly SymbolEquivalenceComparer IgnoreAssembliesInstance = new(assemblyComparer: null, distinguishRefFromOut: false, tupleNamesMustMatch: false, ignoreNullableAnnotations: true, objectAndDynamicCompareEqually: true, arrayAndReadOnlySpanCompareEqually: false);
 
    private readonly IEqualityComparer<IAssemblySymbol>? _assemblyComparer;
 
    private readonly bool _distinguishRefFromOut;
    private readonly bool _tupleNamesMustMatch;
    private readonly bool _ignoreNullableAnnotations;
    private readonly bool _objectAndDynamicCompareEqually;
    private readonly bool _arrayAndReadOnlySpanCompareEqually;
 
    public ParameterSymbolEqualityComparer ParameterEquivalenceComparer { get; }
    public SignatureTypeSymbolEquivalenceComparer SignatureTypeEquivalenceComparer { get; }
 
    public SymbolEquivalenceComparer(
        IEqualityComparer<IAssemblySymbol>? assemblyComparer,
        bool distinguishRefFromOut,
        bool tupleNamesMustMatch,
        bool ignoreNullableAnnotations,
        bool objectAndDynamicCompareEqually,
        bool arrayAndReadOnlySpanCompareEqually)
    {
        _assemblyComparer = assemblyComparer;
        _distinguishRefFromOut = distinguishRefFromOut;
        _tupleNamesMustMatch = tupleNamesMustMatch;
        _ignoreNullableAnnotations = ignoreNullableAnnotations;
        _objectAndDynamicCompareEqually = objectAndDynamicCompareEqually;
        _arrayAndReadOnlySpanCompareEqually = arrayAndReadOnlySpanCompareEqually;
 
        this.ParameterEquivalenceComparer = new ParameterSymbolEqualityComparer(this, distinguishRefFromOut);
        this.SignatureTypeEquivalenceComparer = new SignatureTypeSymbolEquivalenceComparer(this);
 
        // There are only so many EquivalenceVisitors and GetHashCodeVisitors we can have.
        // Create them all up front.
        using var equivalenceVisitors = TemporaryArray<EquivalenceVisitor>.Empty;
        using var getHashCodeVisitors = TemporaryArray<GetHashCodeVisitor>.Empty;
 
        AddVisitors(compareMethodTypeParametersByIndex: true);
        AddVisitors(compareMethodTypeParametersByIndex: false);
 
        _equivalenceVisitors = equivalenceVisitors.ToImmutableAndClear();
        _getHashCodeVisitors = getHashCodeVisitors.ToImmutableAndClear();
 
        return;
 
        void AddVisitors(bool compareMethodTypeParametersByIndex)
        {
            equivalenceVisitors.Add(new(this, compareMethodTypeParametersByIndex));
            getHashCodeVisitors.Add(new(this, compareMethodTypeParametersByIndex));
        }
    }
 
    public static SymbolEquivalenceComparer Create(
        bool distinguishRefFromOut,
        bool tupleNamesMustMatch,
        bool ignoreNullableAnnotations,
        bool objectAndDynamicCompareEqually,
        bool arrayAndReadOnlySpanCompareEqually)
    {
        return new(SimpleNameAssemblyComparer.Instance, distinguishRefFromOut, tupleNamesMustMatch, ignoreNullableAnnotations, objectAndDynamicCompareEqually, arrayAndReadOnlySpanCompareEqually);
    }
 
    public SymbolEquivalenceComparer With(
        Optional<bool> distinguishRefFromOut = default,
        Optional<bool> tupleNamesMustMatch = default,
        Optional<bool> ignoreNullableAnnotations = default,
        Optional<bool> objectAndDynamicCompareEqually = default,
        Optional<bool> arrayAndReadOnlySpanCompareEqually = default)
    {
        var newDistinguishRefFromOut = distinguishRefFromOut.HasValue ? distinguishRefFromOut.Value : _distinguishRefFromOut;
        var newTupleNamesMustMatch = tupleNamesMustMatch.HasValue ? tupleNamesMustMatch.Value : _tupleNamesMustMatch;
        var newIgnoreNullableAnnotations = ignoreNullableAnnotations.HasValue ? ignoreNullableAnnotations.Value : _ignoreNullableAnnotations;
        var newObjectAndDynamicCompareEqually = objectAndDynamicCompareEqually.HasValue ? objectAndDynamicCompareEqually.Value : _objectAndDynamicCompareEqually;
        var newArrayAndReadOnlySpanCompareEqually = arrayAndReadOnlySpanCompareEqually.HasValue ? arrayAndReadOnlySpanCompareEqually.Value : _arrayAndReadOnlySpanCompareEqually;
 
        return new(_assemblyComparer, newDistinguishRefFromOut, newTupleNamesMustMatch, newIgnoreNullableAnnotations, newObjectAndDynamicCompareEqually, newArrayAndReadOnlySpanCompareEqually);
    }
 
    // Very subtle logic here.  When checking if two parameters are the same, we can end up with
    // a tricky infinite loop.  Specifically, consider the case if the parameter refers to a
    // method type parameter.  i.e. "void Goo<T>(IList<T> arg)".  If we compare two method type
    // parameters for equality, then we'll end up asking if their methods are the same.  And that
    // will cause us to check if their parameters are the same.  And then we'll be right back
    // here.  So, instead, when asking if parameters are equal, we pass an appropriate flag so
    // that method type parameters are just compared by index and nothing else.
    private EquivalenceVisitor GetEquivalenceVisitor(
        bool compareMethodTypeParametersByIndex = false)
    {
        var visitorIndex = GetVisitorIndex(compareMethodTypeParametersByIndex);
        return _equivalenceVisitors[visitorIndex];
    }
 
    private GetHashCodeVisitor GetGetHashCodeVisitor(
        bool compareMethodTypeParametersByIndex)
    {
        var visitorIndex = GetVisitorIndex(compareMethodTypeParametersByIndex);
        return _getHashCodeVisitors[visitorIndex];
    }
 
    private static int GetVisitorIndex(bool compareMethodTypeParametersByIndex)
        => compareMethodTypeParametersByIndex ? 0 : 1;
 
    public bool ReturnTypeEquals(IMethodSymbol x, IMethodSymbol y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies = null)
        => GetEquivalenceVisitor().ReturnTypesAreEquivalent(x, y, equivalentTypesWithDifferingAssemblies);
 
    /// <summary>
    /// Compares given symbols <paramref name="x"/> and <paramref name="y"/> for equivalence.
    /// </summary>
    public bool Equals(ISymbol? x, ISymbol? y)
        => EqualsCore(x, y, equivalentTypesWithDifferingAssemblies: null);
 
    /// <summary>
    /// Compares given symbols <paramref name="x"/> and <paramref name="y"/> for equivalence and populates <paramref name="equivalentTypesWithDifferingAssemblies"/>
    /// with equivalent non-nested named type key-value pairs that are contained in different assemblies.
    /// These equivalent named type key-value pairs represent possibly equivalent forwarded types, but this API doesn't perform any type forwarding equivalence checks. 
    /// </summary>
    /// <remarks>This API is only supported for <see cref="SymbolEquivalenceComparer.IgnoreAssembliesInstance"/>.</remarks>
    public bool Equals(ISymbol? x, ISymbol? y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies)
    {
        Debug.Assert(_assemblyComparer == null);
        return EqualsCore(x, y, equivalentTypesWithDifferingAssemblies);
    }
 
    private bool EqualsCore(ISymbol? x, ISymbol? y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies)
        => GetEquivalenceVisitor().AreEquivalent(x, y, equivalentTypesWithDifferingAssemblies);
 
    public int GetHashCode(ISymbol? x)
        => GetGetHashCodeVisitor(compareMethodTypeParametersByIndex: false).GetHashCode(x, currentHash: 0);
 
    private static ISymbol UnwrapAlias(ISymbol symbol)
        => symbol.IsKind(SymbolKind.Alias, out IAliasSymbol? alias) ? alias.Target : symbol;
 
    private static SymbolKind GetKindAndUnwrapAlias(ref ISymbol symbol)
    {
        var k = symbol.Kind;
        if (k == SymbolKind.Alias)
        {
            symbol = ((IAliasSymbol)symbol).Target;
            k = symbol.Kind;
        }
 
        return k;
    }
 
    private static bool IsConstructedFromSelf(INamedTypeSymbol symbol)
        => symbol.Equals(symbol.ConstructedFrom);
 
    private static bool IsConstructedFromSelf(IMethodSymbol symbol)
        => symbol.Equals(symbol.ConstructedFrom);
 
    private static bool IsObjectType(ISymbol symbol)
        => symbol.IsKind(SymbolKind.NamedType, out ITypeSymbol? typeSymbol) && typeSymbol.SpecialType == SpecialType.System_Object;
 
    private static bool CheckContainingType(IMethodSymbol x)
    {
        if (x is { MethodKind: MethodKind.DelegateInvoke, ContainingType.IsAnonymousType: true })
            return false;
 
        // We use the signature of a function pointer type to determine equivalence, but
        // function pointer types do not have containing types.
        if (x.MethodKind == MethodKind.FunctionPointerSignature)
            return false;
 
        return true;
    }
 
    private static OneOrMany<INamedTypeSymbol> Unwrap(INamedTypeSymbol namedType)
    {
        // Make the common case non-allocating.
        if (namedType is not IErrorTypeSymbol errorType)
            return OneOrMany.Create(namedType);
 
        using var builder = TemporaryArray<INamedTypeSymbol>.Empty;
        builder.Add(namedType);
 
        foreach (var candidate in errorType.CandidateSymbols)
        {
            if (candidate is INamedTypeSymbol candidateType)
                builder.Add(candidateType);
        }
 
        return OneOrMany.Create(builder.ToImmutableAndClear());
    }
 
    private static bool IsPartialMethodDefinitionPart(IMethodSymbol symbol)
        => symbol.PartialImplementationPart != null;
 
    private static bool IsPartialMethodImplementationPart(IMethodSymbol symbol)
        => symbol.PartialDefinitionPart != null;
 
    private static bool IsPartialMethodDefinitionPart(IPropertySymbol symbol)
        => symbol.PartialImplementationPart != null;
 
    private static bool IsPartialMethodImplementationPart(IPropertySymbol symbol)
        => symbol.PartialDefinitionPart != null;
 
    private static TypeKind GetTypeKind(INamedTypeSymbol x)
        => x.TypeKind switch
        {
            // Treat static classes and modules as equivalent.
            TypeKind.Module => TypeKind.Class,
            var v => v,
        };
}