File: src\roslyn\src\Analyzers\Core\Analyzers\Helpers\HashCodeAnalyzer\HashCodeAnalyzer.OperationDeconstructor.cs
Web Access
Project: src\src\roslyn\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.Immutable;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Shared.Utilities;

internal partial struct HashCodeAnalyzer
{
    /// <summary>
    /// Breaks down complex <see cref="IOperation"/> trees, looking for particular
    /// <see cref="object.GetHashCode"/> patterns and extracting out the field and property
    /// symbols use to compute the hash.
    /// </summary>
    private struct OperationDeconstructor(
        HashCodeAnalyzer analyzer, IMethodSymbol method, ILocalSymbol? hashCodeVariable) : IDisposable
    {
        private readonly HashCodeAnalyzer _analyzer = analyzer;
        private readonly IMethodSymbol _method = method;
        private readonly ILocalSymbol? _hashCodeVariable = hashCodeVariable;

        private readonly ArrayBuilder<ISymbol> _hashedSymbols = ArrayBuilder<ISymbol>.GetInstance();
        private bool _accessesBase = false;

        public readonly void Dispose()
            => _hashedSymbols.Free();

        public readonly (bool accessesBase, ImmutableArray<ISymbol> hashedSymbol) GetResult()
            => (_accessesBase, _hashedSymbols.ToImmutable());

        /// <summary>
        /// Recursive function that decomposes <paramref name="value"/>, looking for particular
        /// forms that VS or ReSharper generate to hash fields in the containing type.
        /// </summary>
        /// <param name="seenHash">'seenHash' is used to determine if we actually saw something
        /// that indicates that we really hashed a field/property and weren't just simply
        /// referencing it.  This is used as we recurse down to make sure we've seen a
        /// pattern we explicitly recognize by the time we hit a field/prop.</param>
        public bool TryAddHashedSymbol(IOperation value, bool seenHash)
        {
            value = Unwrap(value);
            switch (value)
            {
                case IBinaryOperation topBinary:
                    // (hashCode op1 constant) op1 hashed_value
                    //
                    // This is generated by both VS and ReSharper.  Though each use different mathematical
                    // ops to combine the values.
                    return _hashCodeVariable != null &&
                           topBinary.LeftOperand is IBinaryOperation leftBinary &&
                           IsLocalReference(leftBinary.LeftOperand, _hashCodeVariable) &&
                           IsLiteralNumber(leftBinary.RightOperand) &&
                           TryAddHashedSymbol(topBinary.RightOperand, seenHash: true);

                case IInvocationOperation invocation:
                    var targetMethod = invocation.TargetMethod;
                    if (_analyzer.OverridesSystemObject(targetMethod))
                    {
                        // Either:
                        //
                        //      a.GetHashCode()
                        //
                        // or
                        //
                        //      (hashCode * -1521134295 + a.GetHashCode()).GetHashCode()
                        //
                        // recurse on the value we're calling GetHashCode on.
                        RoslynDebug.Assert(invocation.Instance is not null);
                        return TryAddHashedSymbol(invocation.Instance, seenHash: true);
                    }

                    if (targetMethod.Name == nameof(GetHashCode) &&
                        Equals(_analyzer._equalityComparerType, targetMethod.ContainingType.OriginalDefinition) &&
                        invocation.Arguments.Length == 1)
                    {
                        // EqualityComparer<T>.Default.GetHashCode(i)
                        //
                        // VS codegen only.
                        return TryAddHashedSymbol(invocation.Arguments[0].Value, seenHash: true);
                    }

                    // No other invocations supported.
                    return false;

                case IConditionalOperation conditional:
                    // (StringProperty != null ? StringProperty.GetHashCode() : 0)
                    //
                    // ReSharper codegen only.
                    if (conditional.Condition is IBinaryOperation binary &&
                        Unwrap(binary.RightOperand).IsNullLiteral() &&
                        TryGetFieldOrProperty(binary.LeftOperand, out _))
                    {
                        if (binary.OperatorKind == BinaryOperatorKind.Equals)
                        {
                            // (StringProperty == null ? 0 : StringProperty.GetHashCode())
                            RoslynDebug.Assert(conditional.WhenFalse is not null);
                            return TryAddHashedSymbol(conditional.WhenFalse, seenHash: true);
                        }
                        else if (binary.OperatorKind == BinaryOperatorKind.NotEquals)
                        {
                            // (StringProperty != null ? StringProperty.GetHashCode() : 0)
                            return TryAddHashedSymbol(conditional.WhenTrue, seenHash: true);
                        }
                    }

                    // no other conditional forms supported.
                    return false;
            }

            // Look to see if we're referencing some field/prop/base.  However, we only accept
            // this reference if we've at least been through something that indicates that we've
            // hashed the value.
            if (seenHash)
            {
                if (value is IInstanceReferenceOperation instanceReference &&
                    instanceReference.ReferenceKind == InstanceReferenceKind.ContainingTypeInstance &&
                    Equals(_method.ContainingType.BaseType, instanceReference.Type))
                {
                    if (_accessesBase)
                    {
                        // already had a reference to base.GetHashCode();
                        return false;
                    }

                    // reference to base.
                    //
                    // Happens with code like: `var hashCode = base.GetHashCode();`
                    _accessesBase = true;
                    return true;
                }

                // After decomposing all of the above patterns, we must end up with an operation that is
                // a reference to an instance-field (or prop) in our type.  If so, and this is the only
                // time we've seen that field/prop, then we're good.
                //
                // We only do this if we actually did something that counts as hashing along the way.  This
                // way
                if (TryGetFieldOrProperty(value, out var fieldOrProp) &&
                    Equals(fieldOrProp.ContainingType.OriginalDefinition, _method.ContainingType))
                {
                    return TryAddSymbol(fieldOrProp);
                }

                if (value is ITupleOperation tupleOperation)
                {
                    foreach (var element in tupleOperation.Elements)
                    {
                        if (!TryAddHashedSymbol(element, seenHash: true))
                        {
                            return false;
                        }
                    }

                    return true;
                }
            }

            // Anything else is not recognized.
            return false;
        }

        private static bool TryGetFieldOrProperty(IOperation operation, [NotNullWhen(true)] out ISymbol? symbol)
        {
            operation = Unwrap(operation);

            if (operation is IFieldReferenceOperation fieldReference)
            {
                symbol = fieldReference.Member;
                return !symbol.IsStatic;
            }

            if (operation is IPropertyReferenceOperation propertyReference)
            {
                symbol = propertyReference.Member;
                return !symbol.IsStatic;
            }

            symbol = null;
            return false;
        }

        private readonly bool TryAddSymbol(ISymbol member)
        {
            // Not a legal GetHashCode to convert if we refer to members multiple times.
            if (_hashedSymbols.Contains(member))
            {
                return false;
            }

            _hashedSymbols.Add(member);
            return true;
        }
    }
}