File: SymbolEquivalenceComparer.EquivalenceVisitor.cs
Web Access
Project: ..\..\..\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.
 
////#define TRACKDEPTH
 
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Shared.Utilities
{
    internal partial class SymbolEquivalenceComparer
    {
        private class EquivalenceVisitor
        {
            private readonly bool _compareMethodTypeParametersByIndex;
            private readonly bool _objectAndDynamicCompareEqually;
            private readonly SymbolEquivalenceComparer _symbolEquivalenceComparer;
 
            public EquivalenceVisitor(
                SymbolEquivalenceComparer symbolEquivalenceComparer,
                bool compareMethodTypeParametersByIndex,
                bool objectAndDynamicCompareEqually)
            {
                _symbolEquivalenceComparer = symbolEquivalenceComparer;
                _compareMethodTypeParametersByIndex = compareMethodTypeParametersByIndex;
                _objectAndDynamicCompareEqually = objectAndDynamicCompareEqually;
            }
 
#if TRACKDEPTH
            private int depth = 0;
#endif
            public bool AreEquivalent(ISymbol? x, ISymbol? y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies)
            {
#if TRACKDEPTH
                try
                { 
                this.depth++;
                if (depth > 100)
                {
                    throw new InvalidOperationException("Stack too deep.");
                }
#endif
 
                if (ReferenceEquals(x, y))
                {
                    return true;
                }
 
                if (x == null || y == null)
                {
                    return false;
                }
 
                var xKind = GetKindAndUnwrapAlias(ref x);
                var yKind = GetKindAndUnwrapAlias(ref y);
 
                // Normally, if they're different types, then they're not the same.
                if (xKind != yKind)
                {
                    // Special case.  If we're comparing signatures then we want to compare 'object'
                    // and 'dynamic' as the same.  However, since they're different types, we don't
                    // want to bail out using the above check.
                    if (_objectAndDynamicCompareEqually)
                    {
                        return (xKind == SymbolKind.DynamicType && IsObjectType(y)) ||
                               (yKind == SymbolKind.DynamicType && IsObjectType(x));
                    }
 
                    return false;
                }
 
                return AreEquivalentWorker(x, y, xKind, equivalentTypesWithDifferingAssemblies);
 
#if TRACKDEPTH
            }
            finally
            {
                this.depth--;
            }
#endif
            }
 
            internal bool AreEquivalent(CustomModifier x, CustomModifier y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies)
                => x.IsOptional == y.IsOptional && AreEquivalent(x.Modifier, y.Modifier, equivalentTypesWithDifferingAssemblies);
 
            internal bool AreEquivalent(ImmutableArray<CustomModifier> x, ImmutableArray<CustomModifier> y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies)
            {
                Debug.Assert(!x.IsDefault && !y.IsDefault);
                if (x.Length != y.Length)
                {
                    return false;
                }
 
                for (var i = 0; i < x.Length; i++)
                {
                    if (!AreEquivalent(x[i], y[i], equivalentTypesWithDifferingAssemblies))
                    {
                        return false;
                    }
                }
 
                return true;
            }
 
            private bool NullableAnnotationsEquivalent(ITypeSymbol x, ITypeSymbol y)
                => _symbolEquivalenceComparer._ignoreNullableAnnotations || x.NullableAnnotation == y.NullableAnnotation;
 
            private bool AreEquivalentWorker(ISymbol x, ISymbol y, SymbolKind k, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies)
            {
                Debug.Assert(x.Kind == y.Kind && x.Kind == k);
                return k switch
                {
                    SymbolKind.ArrayType => ArrayTypesAreEquivalent((IArrayTypeSymbol)x, (IArrayTypeSymbol)y, equivalentTypesWithDifferingAssemblies),
                    SymbolKind.Assembly => AssembliesAreEquivalent((IAssemblySymbol)x, (IAssemblySymbol)y),
                    SymbolKind.DynamicType => NullableAnnotationsEquivalent((IDynamicTypeSymbol)x, (IDynamicTypeSymbol)y),
                    SymbolKind.Event => EventsAreEquivalent((IEventSymbol)x, (IEventSymbol)y, equivalentTypesWithDifferingAssemblies),
                    SymbolKind.Field => FieldsAreEquivalent((IFieldSymbol)x, (IFieldSymbol)y, equivalentTypesWithDifferingAssemblies),
                    SymbolKind.Label => LabelsAreEquivalent((ILabelSymbol)x, (ILabelSymbol)y),
                    SymbolKind.Local => LocalsAreEquivalent((ILocalSymbol)x, (ILocalSymbol)y),
                    SymbolKind.Method => MethodsAreEquivalent((IMethodSymbol)x, (IMethodSymbol)y, equivalentTypesWithDifferingAssemblies),
                    SymbolKind.NetModule => ModulesAreEquivalent((IModuleSymbol)x, (IModuleSymbol)y),
                    SymbolKind.NamedType => NamedTypesAreEquivalent((INamedTypeSymbol)x, (INamedTypeSymbol)y, equivalentTypesWithDifferingAssemblies),
                    SymbolKind.ErrorType => NamedTypesAreEquivalent((INamedTypeSymbol)x, (INamedTypeSymbol)y, equivalentTypesWithDifferingAssemblies),
                    SymbolKind.Namespace => NamespacesAreEquivalent((INamespaceSymbol)x, (INamespaceSymbol)y, equivalentTypesWithDifferingAssemblies),
                    SymbolKind.Parameter => ParametersAreEquivalent((IParameterSymbol)x, (IParameterSymbol)y, equivalentTypesWithDifferingAssemblies),
                    SymbolKind.PointerType => PointerTypesAreEquivalent((IPointerTypeSymbol)x, (IPointerTypeSymbol)y, equivalentTypesWithDifferingAssemblies),
                    SymbolKind.Property => PropertiesAreEquivalent((IPropertySymbol)x, (IPropertySymbol)y, equivalentTypesWithDifferingAssemblies),
                    SymbolKind.RangeVariable => RangeVariablesAreEquivalent((IRangeVariableSymbol)x, (IRangeVariableSymbol)y),
                    SymbolKind.TypeParameter => TypeParametersAreEquivalent((ITypeParameterSymbol)x, (ITypeParameterSymbol)y, equivalentTypesWithDifferingAssemblies),
                    SymbolKind.Preprocessing => PreprocessingSymbolsAreEquivalent((IPreprocessingSymbol)x, (IPreprocessingSymbol)y),
                    SymbolKind.FunctionPointerType => FunctionPointerTypesAreEquivalent((IFunctionPointerTypeSymbol)x, (IFunctionPointerTypeSymbol)y, equivalentTypesWithDifferingAssemblies),
                    _ => false,
                };
            }
 
            private bool ArrayTypesAreEquivalent(IArrayTypeSymbol x, IArrayTypeSymbol y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies)
            {
                return
                    x.Rank == y.Rank &&
                    AreEquivalent(x.CustomModifiers, y.CustomModifiers, equivalentTypesWithDifferingAssemblies) &&
                    AreEquivalent(x.ElementType, y.ElementType, equivalentTypesWithDifferingAssemblies) &&
                    NullableAnnotationsEquivalent(x, y);
            }
 
            private bool AssembliesAreEquivalent(IAssemblySymbol x, IAssemblySymbol y)
                => _symbolEquivalenceComparer._assemblyComparerOpt?.Equals(x, y) ?? true;
 
            private bool FieldsAreEquivalent(IFieldSymbol x, IFieldSymbol y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies)
            {
                return
                    x.Name == y.Name &&
                    AreEquivalent(x.CustomModifiers, y.CustomModifiers, equivalentTypesWithDifferingAssemblies) &&
                    AreEquivalent(x.ContainingSymbol, y.ContainingSymbol, equivalentTypesWithDifferingAssemblies);
            }
 
            private static bool LabelsAreEquivalent(ILabelSymbol x, ILabelSymbol y)
            {
                return
                    x.Name == y.Name &&
                    HaveSameLocation(x, y);
            }
 
            private static bool LocalsAreEquivalent(ILocalSymbol x, ILocalSymbol y)
                => HaveSameLocation(x, y);
 
            private bool MethodsAreEquivalent(IMethodSymbol x, IMethodSymbol y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies, bool considerReturnRefKinds = false)
            {
                if (!AreCompatibleMethodKinds(x.MethodKind, y.MethodKind))
                {
                    return false;
                }
 
                if (x.MethodKind == MethodKind.ReducedExtension)
                {
                    var rx = x.ReducedFrom;
                    var ry = y.ReducedFrom;
 
                    // reduced from symbols are equivalent
                    if (!AreEquivalent(rx, ry, equivalentTypesWithDifferingAssemblies))
                    {
                        return false;
                    }
 
                    // receiver types are equivalent
                    if (!AreEquivalent(x.ReceiverType, y.ReceiverType, equivalentTypesWithDifferingAssemblies))
                    {
                        return false;
                    }
                }
                else
                {
                    if (x.MethodKind is MethodKind.AnonymousFunction or
                        MethodKind.LocalFunction)
                    {
                        // Treat local and anonymous functions just like we do ILocalSymbols.  
                        // They're only equivalent if they have the same location.
                        return HaveSameLocation(x, y);
                    }
 
                    if (IsPartialMethodDefinitionPart(x) != IsPartialMethodDefinitionPart(y) ||
                        IsPartialMethodImplementationPart(x) != IsPartialMethodImplementationPart(y) ||
                        x.IsDefinition != y.IsDefinition ||
                        IsConstructedFromSelf(x) != IsConstructedFromSelf(y) ||
                        x.Arity != y.Arity ||
                        x.Parameters.Length != y.Parameters.Length ||
                        x.Name != y.Name)
                    {
                        return false;
                    }
 
                    var checkContainingType = CheckContainingType(x);
                    if (checkContainingType)
                    {
                        if (!AreEquivalent(x.ContainingSymbol, y.ContainingSymbol, equivalentTypesWithDifferingAssemblies))
                        {
                            return false;
                        }
                    }
 
                    if (!ParametersAreEquivalent(x.Parameters, y.Parameters, equivalentTypesWithDifferingAssemblies))
                    {
                        return false;
                    }
 
                    if (!ReturnTypesAreEquivalent(x, y, equivalentTypesWithDifferingAssemblies))
                    {
                        return false;
                    }
 
                    if (considerReturnRefKinds && !AreRefKindsEquivalent(x.RefKind, y.RefKind, distinguishRefFromOut: false))
                    {
                        return false;
                    }
                }
 
                // If it's an unconstructed method, then we don't need to check the type arguments.
                if (IsConstructedFromSelf(x))
                {
                    return true;
                }
 
                return TypeArgumentsAreEquivalent(x.TypeArguments, y.TypeArguments, equivalentTypesWithDifferingAssemblies);
            }
 
            private static bool AreCompatibleMethodKinds(MethodKind kind1, MethodKind kind2)
            {
                if (kind1 == kind2)
                {
                    return true;
                }
 
                if ((kind1 == MethodKind.Ordinary && kind2.IsPropertyAccessor()) ||
                    (kind1.IsPropertyAccessor() && kind2 == MethodKind.Ordinary))
                {
                    return true;
                }
 
                // User-defined and Built-in operators are comparable
                if ((kind1 == MethodKind.BuiltinOperator && kind2 == MethodKind.UserDefinedOperator) ||
                    (kind1 == MethodKind.UserDefinedOperator && kind2 == MethodKind.BuiltinOperator))
                {
                    return true;
                }
 
                return false;
            }
 
            private static bool HaveSameLocation(ISymbol x, ISymbol y)
            {
                return x.Locations.Length == 1 && y.Locations.Length == 1 &&
                    x.Locations.First().Equals(y.Locations.First());
            }
 
            private bool ModulesAreEquivalent(IModuleSymbol x, IModuleSymbol y)
                => AssembliesAreEquivalent(x.ContainingAssembly, y.ContainingAssembly) && x.Name == y.Name;
 
            private bool NamedTypesAreEquivalent(INamedTypeSymbol x, INamedTypeSymbol y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies)
            {
                // PERF: Avoid multiple virtual calls to fetch the TypeKind property
                var xTypeKind = GetTypeKind(x);
                var yTypeKind = GetTypeKind(y);
 
                if (xTypeKind == TypeKind.Error ||
                    yTypeKind == TypeKind.Error)
                {
                    // Slow path: x or y is an error type. We need to compare
                    // all the candidates in both.
                    return NamedTypesAreEquivalentError(x, y, equivalentTypesWithDifferingAssemblies);
                }
 
                // Fast path: we can compare the symbols directly,
                // avoiding any allocations associated with the Unwrap()
                // enumerator.
                return xTypeKind == yTypeKind && HandleNamedTypesWorker(x, y, equivalentTypesWithDifferingAssemblies);
            }
 
            private bool NamedTypesAreEquivalentError(INamedTypeSymbol x, INamedTypeSymbol y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies)
            {
                foreach (var type1 in Unwrap(x))
                {
                    var typeKind1 = GetTypeKind(type1);
                    foreach (var type2 in Unwrap(y))
                    {
                        var typeKind2 = GetTypeKind(type2);
                        if (typeKind1 == typeKind2 && HandleNamedTypesWorker(type1, type2, equivalentTypesWithDifferingAssemblies))
                        {
                            return true;
                        }
                    }
                }
 
                return false;
            }
 
            /// <summary>
            /// Worker for comparing two named types for equivalence. Note: The two
            /// types must have the same TypeKind.
            /// </summary>
            /// <param name="x">The first type to compare</param>
            /// <param name="y">The second type to compare</param>
            /// <param name="equivalentTypesWithDifferingAssemblies">
            /// Map of equivalent non-nested types to be populated, such that each key-value pair of named types are equivalent but reside in different assemblies.
            /// This map is populated only if we are ignoring assemblies for symbol equivalence comparison, i.e. <see cref="_assemblyComparerOpt"/> is true.
            /// </param>
            /// <returns>True if the two types are equivalent.</returns>
            private bool HandleNamedTypesWorker(INamedTypeSymbol x, INamedTypeSymbol y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies)
            {
                Debug.Assert(GetTypeKind(x) == GetTypeKind(y));
 
                if (x.IsTupleType != y.IsTupleType)
                    return false;
 
                if (x.IsNativeIntegerType != y.IsNativeIntegerType)
                    return false;
 
                if (x.IsTupleType)
                    return HandleTupleTypes(x, y, equivalentTypesWithDifferingAssemblies);
 
                if (IsConstructedFromSelf(x) != IsConstructedFromSelf(y) ||
                    x.Arity != y.Arity ||
                    x.Name != y.Name ||
                    x.IsAnonymousType != y.IsAnonymousType ||
                    x.IsUnboundGenericType != y.IsUnboundGenericType ||
                    !NullableAnnotationsEquivalent(x, y))
                {
                    return false;
                }
 
                if (x.Kind == SymbolKind.ErrorType &&
                    x.ContainingSymbol is INamespaceSymbol xNamespace &&
                    y.ContainingSymbol is INamespaceSymbol yNamespace)
                {
                    Debug.Assert(y.Kind == SymbolKind.ErrorType);
 
                    // For error types, we just ensure that the containing namespaces are equivalent up to the root.
                    while (true)
                    {
                        if (xNamespace.Name != yNamespace.Name)
                            return false;
 
                        // Error namespaces don't set the IsGlobalNamespace bit unfortunately.  So we just do the
                        // nominal check to see if we've actually hit the root.
                        if (xNamespace.Name == "")
                            break;
 
                        xNamespace = xNamespace.ContainingNamespace;
                        yNamespace = yNamespace.ContainingNamespace;
                    }
                }
                else
                {
                    if (!AreEquivalent(x.ContainingSymbol, y.ContainingSymbol, equivalentTypesWithDifferingAssemblies))
                        return false;
 
                    // Above check makes sure that the containing assemblies are considered the same by the assembly comparer being used.
                    // If they are in fact not the same (have different name) and the caller requested to know about such types add {x, y} 
                    // to equivalentTypesWithDifferingAssemblies map.
                    if (equivalentTypesWithDifferingAssemblies != null &&
                        x.ContainingType == null &&
                        x.ContainingAssembly != null &&
                        !AssemblyIdentityComparer.SimpleNameComparer.Equals(x.ContainingAssembly.Name, y.ContainingAssembly.Name) &&
                        !equivalentTypesWithDifferingAssemblies.ContainsKey(x))
                    {
                        equivalentTypesWithDifferingAssemblies.Add(x, y);
                    }
                }
 
                if (x.IsAnonymousType)
                    return HandleAnonymousTypes(x, y, equivalentTypesWithDifferingAssemblies);
 
                // They look very similar at this point.  In the case of non constructed types, we're
                // done.  However, if they are constructed, then their type arguments have to match
                // as well.
                return
                    IsConstructedFromSelf(x) ||
                    x.IsUnboundGenericType ||
                    TypeArgumentsAreEquivalent(x.TypeArguments, y.TypeArguments, equivalentTypesWithDifferingAssemblies);
            }
 
            private bool HandleTupleTypes(INamedTypeSymbol x, INamedTypeSymbol y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies)
            {
                Debug.Assert(y.IsTupleType);
 
                var xElements = x.TupleElements;
                var yElements = y.TupleElements;
 
                if (xElements.Length != yElements.Length)
                    return false;
 
                // Check names first if necessary.
                if (_symbolEquivalenceComparer._tupleNamesMustMatch)
                {
                    for (var i = 0; i < xElements.Length; i++)
                    {
                        var xElement = xElements[i];
                        var yElement = yElements[i];
                        if (xElement.Name != yElement.Name)
                            return false;
                    }
                }
 
                // If we're validating the actual unconstructed ValueTuple type itself, we're done at this point.  No
                // need to check field types.
                //
                // For VB we have to unwrap tuples to their underlying types to do this check.
                // https://github.com/dotnet/roslyn/issues/42860
                if (IsConstructedFromSelf(x.TupleUnderlyingType ?? x))
                    return true;
 
                for (var i = 0; i < xElements.Length; i++)
                {
                    var xElement = xElements[i];
                    var yElement = yElements[i];
 
                    if (!AreEquivalent(xElement.Type, yElement.Type, equivalentTypesWithDifferingAssemblies))
                        return false;
                }
 
                return true;
            }
 
            private bool ParametersAreEquivalent(
                ImmutableArray<IParameterSymbol> xParameters,
                ImmutableArray<IParameterSymbol> yParameters,
                Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies,
                bool compareParameterName = false,
                bool isParameterNameCaseSensitive = false)
            {
                // Note the special parameter comparer we pass in.  We do this so we don't end up
                // infinitely looping between parameters -> type parameters -> methods -> parameters
                var count = xParameters.Length;
                if (yParameters.Length != count)
                {
                    return false;
                }
 
                for (var i = 0; i < count; i++)
                {
                    if (!_symbolEquivalenceComparer.ParameterEquivalenceComparer.Equals(xParameters[i], yParameters[i], equivalentTypesWithDifferingAssemblies, compareParameterName, isParameterNameCaseSensitive))
                    {
                        return false;
                    }
                }
 
                return true;
            }
 
            internal bool ReturnTypesAreEquivalent(IMethodSymbol x, IMethodSymbol y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies = null)
            {
                return _symbolEquivalenceComparer.SignatureTypeEquivalenceComparer.Equals(x.ReturnType, y.ReturnType, equivalentTypesWithDifferingAssemblies) &&
                       AreEquivalent(x.ReturnTypeCustomModifiers, y.ReturnTypeCustomModifiers, equivalentTypesWithDifferingAssemblies);
            }
 
            private bool TypeArgumentsAreEquivalent(ImmutableArray<ITypeSymbol> xTypeArguments, ImmutableArray<ITypeSymbol> yTypeArguments, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies)
            {
                var count = xTypeArguments.Length;
                if (yTypeArguments.Length != count)
                {
                    return false;
                }
 
                for (var i = 0; i < count; i++)
                {
                    if (!AreEquivalent(xTypeArguments[i], yTypeArguments[i], equivalentTypesWithDifferingAssemblies))
                    {
                        return false;
                    }
                }
 
                return true;
            }
 
            private bool HandleAnonymousTypes(INamedTypeSymbol x, INamedTypeSymbol y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies)
            {
                if (x.TypeKind == TypeKind.Delegate)
                {
                    return AreEquivalent(x.DelegateInvokeMethod, y.DelegateInvokeMethod, equivalentTypesWithDifferingAssemblies);
                }
                else
                {
                    var xMembers = x.GetValidAnonymousTypeProperties();
                    var yMembers = y.GetValidAnonymousTypeProperties();
 
                    var xMembersEnumerator = xMembers.GetEnumerator();
                    var yMembersEnumerator = yMembers.GetEnumerator();
 
                    while (xMembersEnumerator.MoveNext())
                    {
                        if (!yMembersEnumerator.MoveNext())
                        {
                            return false;
                        }
 
                        var p1 = xMembersEnumerator.Current;
                        var p2 = yMembersEnumerator.Current;
 
                        if (p1.Name != p2.Name ||
                            p1.IsReadOnly != p2.IsReadOnly ||
                            !AreEquivalent(p1.Type, p2.Type, equivalentTypesWithDifferingAssemblies))
                        {
                            return false;
                        }
                    }
 
                    return !yMembersEnumerator.MoveNext();
                }
            }
 
            private bool NamespacesAreEquivalent(INamespaceSymbol x, INamespaceSymbol y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies)
            {
                if (x.IsGlobalNamespace != y.IsGlobalNamespace ||
                    x.Name != y.Name)
                {
                    return false;
                }
 
                if (x.IsGlobalNamespace && _symbolEquivalenceComparer._assemblyComparerOpt == null)
                {
                    // No need to compare the containers of global namespace when assembly identities are ignored.
                    return true;
                }
 
                return AreEquivalent(x.ContainingSymbol, y.ContainingSymbol, equivalentTypesWithDifferingAssemblies);
            }
 
            private bool ParametersAreEquivalent(IParameterSymbol x, IParameterSymbol y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies)
            {
                return
                    x.IsRefOrOut() == y.IsRefOrOut() &&
                    x.Name == y.Name &&
                    AreEquivalent(x.CustomModifiers, y.CustomModifiers, equivalentTypesWithDifferingAssemblies) &&
                    AreEquivalent(x.Type, y.Type, equivalentTypesWithDifferingAssemblies) &&
                    AreEquivalent(x.ContainingSymbol, y.ContainingSymbol, equivalentTypesWithDifferingAssemblies);
            }
 
            private bool PointerTypesAreEquivalent(IPointerTypeSymbol x, IPointerTypeSymbol y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies)
            {
                return
                    AreEquivalent(x.CustomModifiers, y.CustomModifiers, equivalentTypesWithDifferingAssemblies) &&
                    AreEquivalent(x.PointedAtType, y.PointedAtType, equivalentTypesWithDifferingAssemblies);
            }
 
            private bool FunctionPointerTypesAreEquivalent(IFunctionPointerTypeSymbol x, IFunctionPointerTypeSymbol y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies)
                => MethodsAreEquivalent(x.Signature, y.Signature, equivalentTypesWithDifferingAssemblies);
 
            private bool PropertiesAreEquivalent(IPropertySymbol x, IPropertySymbol y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies)
            {
                if (x.ContainingType.IsAnonymousType && y.ContainingType.IsAnonymousType)
                {
                    // We can short circuit here and just use the symbols themselves to determine
                    // equality.  This will properly handle things like the VB case where two
                    // anonymous types will be considered the same if they have properties that
                    // differ in casing.
                    if (x.Equals(y))
                    {
                        return true;
                    }
                }
 
                return
                    x.IsIndexer == y.IsIndexer &&
                    x.MetadataName == y.MetadataName &&
                    x.Parameters.Length == y.Parameters.Length &&
                    ParametersAreEquivalent(x.Parameters, y.Parameters, equivalentTypesWithDifferingAssemblies) &&
                    AreEquivalent(x.ContainingSymbol, y.ContainingSymbol, equivalentTypesWithDifferingAssemblies);
            }
 
            private bool EventsAreEquivalent(IEventSymbol x, IEventSymbol y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies)
            {
                return
                    x.Name == y.Name &&
                    AreEquivalent(x.ContainingSymbol, y.ContainingSymbol, equivalentTypesWithDifferingAssemblies);
            }
 
            private bool TypeParametersAreEquivalent(ITypeParameterSymbol x, ITypeParameterSymbol y, Dictionary<INamedTypeSymbol, INamedTypeSymbol>? equivalentTypesWithDifferingAssemblies)
            {
                Debug.Assert(
                    (x.TypeParameterKind == TypeParameterKind.Method && IsConstructedFromSelf(x.DeclaringMethod!)) ||
                    (x.TypeParameterKind == TypeParameterKind.Type && IsConstructedFromSelf(x.ContainingType)) ||
                    x.TypeParameterKind == TypeParameterKind.Cref);
                Debug.Assert(
                    (y.TypeParameterKind == TypeParameterKind.Method && IsConstructedFromSelf(y.DeclaringMethod!)) ||
                    (y.TypeParameterKind == TypeParameterKind.Type && IsConstructedFromSelf(y.ContainingType)) ||
                    y.TypeParameterKind == TypeParameterKind.Cref);
 
                if (x.Ordinal != y.Ordinal ||
                    x.TypeParameterKind != y.TypeParameterKind)
                {
                    return false;
                }
 
                // If this is a method type parameter, and we are in 'non-recurse' mode (because
                // we're comparing method parameters), then we're done at this point.  The types are
                // equal.
                if (x.TypeParameterKind == TypeParameterKind.Method && _compareMethodTypeParametersByIndex)
                {
                    return true;
                }
 
                if (x.TypeParameterKind == TypeParameterKind.Type && x.ContainingType.IsAnonymousType)
                {
                    // Anonymous type type parameters compare by index as well to prevent
                    // recursion.
                    return true;
                }
 
                if (x.TypeParameterKind == TypeParameterKind.Cref)
                {
                    return true;
                }
 
                return AreEquivalent(x.ContainingSymbol, y.ContainingSymbol, equivalentTypesWithDifferingAssemblies);
            }
 
            private static bool RangeVariablesAreEquivalent(IRangeVariableSymbol x, IRangeVariableSymbol y)
                => HaveSameLocation(x, y);
 
            private static bool PreprocessingSymbolsAreEquivalent(IPreprocessingSymbol x, IPreprocessingSymbol y)
                => x.Name == y.Name;
        }
    }
}