File: ConvertToRecordHelpers.cs
Web Access
Project: ..\..\..\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.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.ConvertToRecord
{
    internal static class ConvertToRecordHelpers
    {
        public static bool IsSimpleEqualsMethod(
            Compilation compilation,
            IMethodSymbol methodSymbol,
            IMethodBodyOperation methodBodyOperation,
            ImmutableArray<IFieldSymbol> expectedComparedFields)
        {
            if (methodSymbol.Name == nameof(Equals) &&
                methodSymbol.ReturnType.SpecialType == SpecialType.System_Boolean &&
                methodSymbol.Parameters.IsSingle())
            {
                var type = methodSymbol.ContainingType;
                var equatableType = GetIEquatableType(compilation, type);
                if (OverridesEquals(compilation, methodSymbol, equatableType))
                {
                    if (equatableType != null &&
                        methodSymbol.Parameters.First().Type.SpecialType == SpecialType.System_Object &&
                        GetBlockOfMethodBody(methodBodyOperation) is IBlockOperation
                        {
                            Operations: [IReturnOperation
                            {
                                ReturnedValue: IInvocationOperation
                                {
                                    Instance: IInstanceReferenceOperation,
                                    TargetMethod: IMethodSymbol { Name: nameof(Equals) },
                                    Arguments: [IArgumentOperation { Value: IOperation arg }]
                                }
                            }]
                        } && arg.WalkDownConversion() is IParameterReferenceOperation { Parameter: IParameterSymbol param }
                        && param.Equals(methodSymbol.Parameters.First()))
                    {
                        // in this case where we have an Equals(C? other) from IEquatable but the current one
                        // is Equals(object? other), we accept something of the form:
                        // return Equals(other as C);
                        return true;
                    }
 
                    // otherwise we check to see which fields are compared (either by themselves or through properties)
                    var actualFields = GetEqualizedFields(methodBodyOperation, methodSymbol);
                    return actualFields.SetEquals(expectedComparedFields);
                }
            }
 
            return false;
        }
 
        public static INamedTypeSymbol? GetIEquatableType(Compilation compilation, INamedTypeSymbol containingType)
        {
            // can't use nameof since it's generic and we need the type parameter
            var equatable = compilation.GetBestTypeByMetadataName("System.IEquatable`1")?.Construct(containingType);
            return containingType.Interfaces.FirstOrDefault(iface => iface.Equals(equatable));
        }
 
        public static bool IsSimpleHashCodeMethod(
            Compilation compilation,
            IMethodSymbol methodSymbol,
            IMethodBodyOperation methodOperation,
            ImmutableArray<IFieldSymbol> expectedHashedFields)
        {
            if (methodSymbol.Name == nameof(GetHashCode) &&
                methodSymbol.Parameters.IsEmpty &&
                HashCodeAnalyzer.TryGetAnalyzer(compilation, out var analyzer))
            {
                // Hash Code method, see if it would be a default implementation that we can remove
                var (_, members, _) = analyzer.GetHashedMembers(
                    methodSymbol, methodOperation.BlockBody ?? methodOperation.ExpressionBody);
                if (members != default)
                {
                    // the user could access a member using either the property or the underlying field
                    // so anytime they access a property instead of the underlying field we convert it to the
                    // corresponding underlying field
                    var actualMembers = members
                        .SelectAsArray(UnwrapPropertyToField).WhereNotNull().AsImmutable();
 
                    return actualMembers.SetEquals(expectedHashedFields);
                }
            }
            return false;
        }
 
        /// <summary>
        /// Returns true if the method contents match a simple reference to the equals method
        /// which would be the compiler generated implementation
        /// </summary>
        public static bool IsDefaultEqualsOperator(IMethodBodyOperation operation)
        {
            // must look like
            // public static operator ==(C c1, object? c2)
            // {
            //  return c1.Equals(c2);
            // }
            // or
            // public static operator ==(C c1, object? c2) => c1.Equals(c2);
            return GetBlockOfMethodBody(operation) is IBlockOperation
            {
                // look for only one operation, a return operation that consists of an equals invocation
                Operations: [IReturnOperation { ReturnedValue: IOperation returnedValue }]
            } &&
            IsDotEqualsInvocation(returnedValue);
        }
 
        /// <summary>
        /// Whether the method simply returns !(equals), where "equals" is
        /// c1 == c2 or c1.Equals(c2)
        /// </summary>
        internal static bool IsDefaultNotEqualsOperator(
            IMethodBodyOperation operation)
        {
            // looking for:
            // return !(operand);
            // or:
            // => !(operand);
            if (GetBlockOfMethodBody(operation) is not IBlockOperation
                {
                    Operations: [IReturnOperation
                    {
                        ReturnedValue: IUnaryOperation
                        {
                            OperatorKind: UnaryOperatorKind.Not,
                            Operand: IOperation operand
                        }
                    }]
                })
            {
                return false;
            }
 
            // check to see if operand is an equals invocation that references the parameters
            if (IsDotEqualsInvocation(operand))
                return true;
 
            // we accept an == operator, for example
            // return !(obj1 == obj2);
            // since this would call our == operator, which would in turn call .Equals (or equivalent)
            // but we need to make sure that the operands are parameter references
            if (operand is not IBinaryOperation
                {
                    OperatorKind: BinaryOperatorKind.Equals,
                    LeftOperand: IOperation leftOperand,
                    RightOperand: IOperation rightOperand,
                })
            {
                return false;
            }
 
            // now we know we have an == comparison, but we want to make sure these actually reference parameters
            var left = GetParamFromArgument(leftOperand);
            var right = GetParamFromArgument(rightOperand);
            // make sure we're not referencing the same parameter twice
            return left != null && right != null && !left.Equals(right);
        }
 
        /// <summary>
        /// Matches constructors where each statement simply assigns one of the provided parameters to one of the provided properties
        /// with no duplicate assignment or any other type of statement
        /// </summary>
        /// <param name="operation">Constructor body</param>
        /// <param name="properties">Properties expected to be assigned (would be replaced with positional constructor).
        /// Will re-order this list to match parameter order if successful.</param>
        /// <param name="parameters">Constructor parameters</param>
        /// <returns>Whether the constructor body matches the pattern described</returns>
        public static bool IsSimplePrimaryConstructor(
            IConstructorBodyOperation operation,
            ref ImmutableArray<IPropertySymbol> properties,
            ImmutableArray<IParameterSymbol> parameters)
        {
            if (GetBlockOfMethodBody(operation) is not { Operations.Length: int opLength } ||
                opLength != properties.Length)
            {
                return false;
            }
 
            var assignmentValues = GetAssignmentValuesForConstructor(operation,
                assignment => (assignment as IParameterReferenceOperation)?.Parameter);
 
            // we must assign to all the properties (keys) and use all the parameters (values)
            if (assignmentValues.Keys.SetEquals(properties) &&
                assignmentValues.Values.SetEquals(parameters))
            {
                // order properties in order of the parameters that they were assigned to
                // e.g if we originally have Properties: [int Y, int X]
                // and constructor:
                // public C(int x, int y)
                // {
                //     X = x;
                //     Y = y;
                // }
                // then we would re-order the properties to: [int X, int Y]
                properties = properties
                    .OrderBy(property => parameters.IndexOf(assignmentValues[property]))
                    .AsImmutable();
                return true;
            }
 
            return false;
        }
 
        /// <summary>
        /// Checks to see if all fields/properties were assigned from the parameter
        /// </summary>
        /// <param name="operation">constructor body</param>
        /// <param name="fields">all instance fields, including backing fields of constructors</param>
        /// <param name="parameter">parameter to copy constructor</param>
        public static bool IsSimpleCopyConstructor(
            IConstructorBodyOperation operation,
            ImmutableArray<IFieldSymbol> fields,
            IParameterSymbol parameter)
        {
            if (GetBlockOfMethodBody(operation) is not { Operations.Length: int opLength } ||
                opLength != fields.Length)
            {
                return false;
            }
 
            var assignmentValues = GetAssignmentValuesForConstructor(operation,
                assignment => assignment switch
                {
                    IPropertyReferenceOperation
                    {
                        Instance: IParameterReferenceOperation { Parameter: IParameterSymbol referencedParameter },
                        Property: IPropertySymbol referencedProperty
                    } =>
                        referencedParameter.Equals(parameter) ? referencedProperty.GetBackingFieldIfAny() : null,
                    IFieldReferenceOperation
                    {
                        Instance: IParameterReferenceOperation { Parameter: IParameterSymbol referencedParameter },
                        Field: IFieldSymbol referencedField
                    } =>
                       referencedParameter.Equals(parameter) ? referencedField : null,
                    _ => null
                });
 
            // left hand side of each assignment
            var assignedUnderlyingFields = assignmentValues.Keys.SelectAsArray(UnwrapPropertyToField);
 
            // Each right hand assignment should assign the same property.
            // All assigned properties should be equal (in potentially a different order)
            // to all the properties we would be moving
            return assignedUnderlyingFields.SequenceEqual(assignmentValues.Values) &&
                assignedUnderlyingFields.SetEquals(fields);
        }
 
        /// <summary>
        /// Given a non-primary, non-copy constructor, get expressions that are assigned to
        /// primary constructor properties via simple assignment.
        /// </summary>
        /// <param name="operation">The constructor body operation</param>
        /// <param name="positionalParams">the primary constructor parameters</param>
        /// <returns>
        /// Expressions that were assigned to a primary constructor property in the constructor,
        /// or default/null if there wasn't an assignment found. Returned in order of primary parameters.
        /// </returns>
        /// <remarks>
        /// Example (assume we decided on positional parameters int Foo, bool Bar, int Baz):
        /// <code>
        /// public C(int foo, bool bar)
        /// {
        ///     Bar = bar;
        ///     Foo = foo;
        ///     Mumble = 0;
        /// }
        /// </code>
        /// we would return: [foo, bar, default]
        /// where foo and bar are the nodes in the assignment, and default is factory constructed.
        /// </remarks>
        public static ImmutableArray<ExpressionSyntax> GetAssignmentValuesForNonPrimaryConstructor(
            IConstructorBodyOperation operation,
            ImmutableArray<IPropertySymbol> positionalParams)
        {
            // make sure the assignment wouldn't reference local variables we may have declared
            var assignmentValues = GetAssignmentValuesForConstructor(operation,
                assignment => IsSafeAssignment(assignment)
                    ? assignment.Syntax as ExpressionSyntax
                    : null);
 
            if (operation.Initializer is
                IInvocationOperation { Arguments: ImmutableArray<IArgumentOperation> args })
            {
                var additionalValuesBuilder = assignmentValues.ToBuilder();
                // in a "base" or "this" initializer
                foreach (var arg in args)
                {
                    if (arg is { Parameter: IParameterSymbol param, Value.Syntax: ExpressionSyntax captured })
                    {
                        // We're looking to see if this initializer is a primary constructor,
                        // i.e. the parameters are declared as auto-implemented properties in the record definition.
                        // Since there's no way to associate positional parameters (from the primary constructor)
                        // to the properties that they declare other than by comparing their declaration locations,
                        // we have to do this rather convoluted comparison.
                        // Note: We can use AssociatedSymbol once this is implemented:
                        // https://github.com/dotnet/roslyn/issues/54286
                        var positionalParam = param.ContainingSymbol.ContainingType.GetMembers().FirstOrDefault(member
                                => member.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() ==
                                    param.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax());
                        if (positionalParam is IPropertySymbol property)
                        {
                            if (additionalValuesBuilder.ContainsKey(property))
                            {
                                // don't allow assignment to the same property more than once
                                return ImmutableArray<ExpressionSyntax>.Empty;
                            }
 
                            additionalValuesBuilder.Add(property, captured);
                        }
                    }
                }
 
                assignmentValues = additionalValuesBuilder.ToImmutable();
            }
 
            var expressions = GetAssignmentExpressionsFromValuesMap(positionalParams, assignmentValues);
 
            return expressions;
        }
 
        /// <summary>
        /// Given an object creation with a block initializer and a parameterless constructor declaration,
        /// finds values that were assigned to the primary constructor parameters
        /// </summary>
        /// <param name="operation">Object creation expression operation</param>
        /// <param name="positionalParams">primary constructor parameters</param>
        /// <returns>
        /// values that were assigned to primary constructor parameters, in order of the passed in primary constructor
        /// </returns>
        /// <remarks>
        ///  Example (assume we decided on positional parameters int Foo, bool Bar, int Baz):
        /// <code>
        /// var c = new C
        /// {
        ///     Bar = true;
        ///     Foo = 10;
        ///     Mumble = 0;
        /// };
        /// </code>
        /// We would return [10, true, default]
        /// where 10 and true are the actual nodes and default was a constructed node
        /// </remarks>
        public static ImmutableArray<ExpressionSyntax> GetAssignmentValuesFromObjectCreation(
            IObjectCreationOperation operation,
            ImmutableArray<IPropertySymbol> positionalParams)
        {
            // we want to be very careful about when we refactor because if the user has a constructor
            // and initializer it could be what they intend. Since we gave initializers to all non-primary
            // constructors they already have, any calls to an explicit constructor with additional block initialization
            // still work absolutely fine. Further, we can't necessarily associate their constructor args to
            // primary constructor args or any other constructor args. Therefore,
            // the only time we want to actually make a change is if they use the default no-param constructor,
            // and a block initializer.
            if (operation is IObjectCreationOperation
                {
                    Arguments: ImmutableArray<IArgumentOperation> { IsEmpty: true },
                    Initializer: IObjectOrCollectionInitializerOperation initializer,
                    Constructor: IMethodSymbol { IsImplicitlyDeclared: true }
                })
            {
                var dictionaryBuilder = ImmutableDictionary<ISymbol, ExpressionSyntax>.Empty.ToBuilder();
 
                foreach (var assignment in initializer.Initializers)
                {
                    if (assignment is ISimpleAssignmentOperation
                        {
                            Target: IPropertyReferenceOperation { Property: IPropertySymbol property },
                            Value: IOperation { Syntax: ExpressionSyntax syntax }
                        })
                    {
                        dictionaryBuilder.Add(property, syntax);
                    }
                }
 
                var expressions = GetAssignmentExpressionsFromValuesMap(positionalParams, dictionaryBuilder.ToImmutable());
 
                return expressions;
            }
 
            // no initializer or uses explicit constructor, no need to make a change
            return ImmutableArray<ExpressionSyntax>.Empty;
        }
 
        private static ImmutableArray<ExpressionSyntax> GetAssignmentExpressionsFromValuesMap(
            ImmutableArray<IPropertySymbol> positionalParams,
            ImmutableDictionary<ISymbol, ExpressionSyntax> assignmentValues)
        => positionalParams.SelectAsArray(property =>
        {
            if (assignmentValues.ContainsKey(property))
            {
                return assignmentValues[property];
            }
            else
            {
                return SyntaxFactory.LiteralExpression(
                    property.Type.NullableAnnotation == NullableAnnotation.Annotated
                        ? SyntaxKind.NullLiteralExpression
                        : SyntaxKind.DefaultLiteralExpression);
            }
        });
 
        private static ImmutableDictionary<ISymbol, T> GetAssignmentValuesForConstructor<T>(
            IConstructorBodyOperation constructorOperation,
            Func<IOperation, T?> captureAssignedSymbol)
        {
            var body = GetBlockOfMethodBody(constructorOperation);
            var dictionaryBuilder = ImmutableDictionary<ISymbol, T>.Empty.ToBuilder();
 
            // We expect the constructor to have exactly one statement per property,
            // where the statement is a simple assignment from the parameter's property to the property
            if (body == null)
            {
                return ImmutableDictionary<ISymbol, T>.Empty;
            }
 
            foreach (var operation in body.Operations)
            {
                if (operation is IExpressionStatementOperation
                    {
                        Operation: ISimpleAssignmentOperation
                        {
                            Target: IOperation assignee,
                            Value: IOperation assignment
                        }
                    } &&
                    captureAssignedSymbol(assignment) is T captured)
                {
                    ISymbol? symbol = assignee switch
                    {
                        IFieldReferenceOperation
                        { Instance: IInstanceReferenceOperation, Field: IFieldSymbol field }
                            => field,
                        IPropertyReferenceOperation
                        { Instance: IInstanceReferenceOperation, Property: IPropertySymbol property }
                            => property,
                        _ => null,
                    };
 
                    if (symbol != null)
                    {
                        if (dictionaryBuilder.ContainsKey(symbol))
                        {
                            // don't allow assignment to the same property more than once
                            return ImmutableDictionary<ISymbol, T>.Empty;
                        }
 
                        dictionaryBuilder.Add(symbol, captured);
                    }
                }
            }
 
            return dictionaryBuilder.ToImmutable();
        }
 
        /// <summary>
        /// Determines whether the operation is safe to move into the "this(...)" initializer
        /// i.e. Doesn't reference any other created variables but the parameters
        /// </summary>
        private static bool IsSafeAssignment(IOperation operation)
        {
            if (operation is ILocalReferenceOperation)
            {
                return false;
            }
 
            return operation.ChildOperations.All(IsSafeAssignment);
        }
 
        /// <summary>
        /// Get all the fields (including implicit fields underlying properties) that this
        /// equals method compares
        /// </summary>
        /// <param name="operation"></param>
        /// <param name="methodSymbol">the symbol of the equals method</param>
        /// <returns></returns>
        private static ImmutableArray<IFieldSymbol> GetEqualizedFields(
            IMethodBodyOperation operation,
            IMethodSymbol methodSymbol)
        {
            var type = methodSymbol.ContainingType;
 
            var body = GetBlockOfMethodBody(operation);
 
            if (body == null || !methodSymbol.Parameters.IsSingle())
                return ImmutableArray<IFieldSymbol>.Empty;
 
            var bodyOps = body.Operations;
            var parameter = methodSymbol.Parameters.First();
            using var _1 = ArrayBuilder<IFieldSymbol>.GetInstance(out var fields);
            ISymbol? otherC = null;
            IEnumerable<IOperation>? statementsToCheck = null;
 
            // see whether we are calling on a param of the same type or of object
            if (parameter.Type.Equals(type))
            {
                // we need to check all the statements, and we already have the
                // variable that is used to access the members
                otherC = parameter;
                statementsToCheck = bodyOps;
            }
            else if (parameter.Type.SpecialType == SpecialType.System_Object)
            {
                // we could look for some cast which rebinds the parameter
                // to a local of the type such as any of the following:
                // var otherc = other as C; *null and additional equality checks*
                // if (other is C otherc) { *additional equality checks* } (optional else) return false;
                // if (other is not C otherc) { return false; } (optional else) { *additional equality checks* }
                // if (other is C) { otherc = (C) other;  *additional equality checks* } (optional else) return false;
                // if (other is not C) { return false; } (optional else) { otherc = (C) other;  *additional equality checks* }
                // return other is C otherC && ...
                // return !(other is not C otherC || ...
 
                // check for single return operation which binds the variable as the first condition in a sequence
                if (bodyOps is [IReturnOperation { ReturnedValue: IOperation value }] &&
                    TryAddEqualizedFieldsForConditionWithoutTypedVariable(
                        value, successRequirement: true, type, fields, out var _2))
                {
                    // we're done, no more statements to check
                    return fields.ToImmutable();
                }
                // check for the first statement as an explicit cast to a variable declaration
                // like: var otherC = other as C;
                else if (TryGetAssignmentFromParameterWithExplicitCast(bodyOps.FirstOrDefault(), parameter, out otherC))
                {
                    // ignore the first statement as we just ensured it was a cast
                    statementsToCheck = bodyOps.Skip(1);
                }
                // check for the first statement as an if statement where the cast check occurs
                // and a variable assignment happens (either in the if or in a following statement)
                else if (!TryGetBindingCastInFirstIfStatement(bodyOps, parameter, type, fields, out otherC, out statementsToCheck))
                {
                    return ImmutableArray<IFieldSymbol>.Empty;
                }
            }
 
            if (otherC == null || statementsToCheck == null ||
                !TryAddEqualizedFieldsForStatements(statementsToCheck, otherC, type, fields))
            {
                // no patterns matched to bind variable or statements didn't match expectation
                return ImmutableArray<IFieldSymbol>.Empty;
            }
 
            return fields.ToImmutable();
        }
 
        /// <summary>
        /// Matches: var otherC = (C) other;
        /// or: var otherC = other as C;
        /// </summary>
        private static bool TryGetAssignmentFromParameterWithExplicitCast(
            IOperation? operation,
            IParameterSymbol parameter,
            [NotNullWhen(true)] out ISymbol? assignedVariable)
        {
            assignedVariable = null;
            if (operation is IVariableDeclarationGroupOperation
                {
                    Declarations: [IVariableDeclarationOperation
                    {
                        Declarators: [IVariableDeclaratorOperation
                        {
                            Symbol: ILocalSymbol castOther,
                            Initializer: IVariableInitializerOperation
                            {
                                Value: IConversionOperation
                                {
                                    IsImplicit: false,
                                    Operand: IParameterReferenceOperation
                                    {
                                        Parameter: IParameterSymbol referencedParameter1
                                    }
                                }
                            }
                        }]
                    }]
                } && referencedParameter1.Equals(parameter))
            {
                assignedVariable = castOther;
                return true;
            }
 
            return false;
        }
 
        /// <summary>
        /// Get the referenced parameter (and unwraps implicit cast if necessary) or null if a parameter wasn't referenced
        /// </summary>
        /// <param name="operation">The operation for which to get the parameter</param>
        /// <returns>the referenced parameter or null if unable to find</returns>
        private static IParameterSymbol? GetParamFromArgument(IOperation operation)
            => (operation.WalkDownConversion() as IParameterReferenceOperation)?.Parameter;
 
        private static ISymbol? GetReferencedSymbolObject(IOperation reference)
        {
            return reference.WalkDownConversion() switch
            {
                IInstanceReferenceOperation thisReference => thisReference.Type,
                ILocalReferenceOperation localReference => localReference.Local,
                IParameterReferenceOperation paramReference => paramReference.Parameter,
                _ => null,
            };
        }
 
        /// <summary>
        /// matches form:
        /// c1.Equals(c2)
        /// where c1 and c2 are parameter references
        /// </summary>
        private static bool IsDotEqualsInvocation(IOperation operation)
        {
            // must be called on one of the parameters
            if (operation is not IInvocationOperation
                {
                    TargetMethod.Name: nameof(Equals),
                    Instance: IOperation instance,
                    Arguments: [IArgumentOperation { Value: IOperation arg }]
                })
            {
                return false;
            }
 
            // get the (potential) parameters, uwrapping any potential implicit casts
            var invokedOn = GetParamFromArgument(instance);
            var param = GetParamFromArgument(arg);
            // make sure we're not referencing the same parameter twice
            return param != null && invokedOn != null && !invokedOn.Equals(param);
        }
 
        /// <summary>
        /// checks for binary expressions of the type otherC == null or null == otherC
        /// or a pattern against null like otherC is (not) null
        /// and "otherC" is a reference to otherObject.
        /// </summary>
        /// <param name="operation">Operation to check for</param>
        /// <param name="successRequirement">if we're in a context where the operation evaluating to true
        /// would end up being false within the equals method, we look for != instead</param>
        /// <param name="otherObject">Object to be compared to null</param>
        private static bool IsNullCheck(
            IOperation operation,
            bool successRequirement,
            ISymbol otherObject)
        {
            if (operation is IBinaryOperation binOp)
            {
                // if success would return true, then we want the checked object to not be null
                // so we expect a notEquals operator
                var expectedKind = successRequirement
                    ? BinaryOperatorKind.NotEquals
                    : BinaryOperatorKind.Equals;
 
                return binOp.OperatorKind == expectedKind &&
                    // one of the objects must be a reference to the "otherObject"
                    // and the other must be a constant null literal
                    AreConditionsSatisfiedEitherOrder(binOp.LeftOperand, binOp.RightOperand,
                        op => op.WalkDownConversion().IsNullLiteral(),
                        op => otherObject.Equals(GetReferencedSymbolObject(op)));
            }
            else if (operation is IIsPatternOperation patternOp)
            {
                // matches: otherC is null
                // or: otherC is not null
                // based on successRequirement
                IConstantPatternOperation? constantPattern;
                if (successRequirement)
                {
                    constantPattern = (patternOp.Pattern as INegatedPatternOperation)?.
                        Pattern as IConstantPatternOperation;
                }
                else
                {
                    constantPattern = patternOp.Pattern as IConstantPatternOperation;
                }
 
                return constantPattern != null &&
                    otherObject.Equals(GetReferencedSymbolObject(patternOp.Value)) &&
                    constantPattern.Value.WalkDownConversion().IsNullLiteral();
            }
 
            // neither of the expected forms
            return false;
        }
 
        private static bool ReturnsFalseImmediately(IEnumerable<IOperation> operation)
        {
            return operation.FirstOrDefault() is IReturnOperation
            {
                ReturnedValue: ILiteralOperation
                {
                    ConstantValue.HasValue: true,
                    ConstantValue.Value: false,
                }
            };
        }
 
        /// <summary>
        /// looks just at conditional expressions such as "A == other.A &amp;&amp; B == other.B..."
        /// To determine which members were accessed and compared
        /// </summary>
        /// <param name="condition">Condition to look at, should be a boolean expression</param>
        /// <param name="successRequirement">Whether to look for operators that would indicate equality success
        /// (==, .Equals, &amp;&amp;) or inequality operators (!=, ||)</param>
        /// <param name="currentObject">Symbol that would be referenced with this</param>
        /// <param name="otherObject">symbol representing other object, either from a param or cast as a local</param>
        /// <param name="builder">Builder to add members to</param>
        /// <returns>true if addition was successful, false if we see something odd 
        /// (equality checking in the wrong order, side effects, etc)</returns>
        private static bool TryAddEqualizedFieldsForCondition(
            IOperation condition,
            bool successRequirement,
            ISymbol currentObject,
            ISymbol otherObject,
            ArrayBuilder<IFieldSymbol> builder)
        => (successRequirement, condition) switch
        {
            // if we see a not we want to invert the current success status
            // e.g !(A != other.A || B != other.B) is equivalent to (A == other.A && B == other.B)
            // using DeMorgans law. We recurse to see any members accessed
            (_, IUnaryOperation { OperatorKind: UnaryOperatorKind.Not, Operand: IOperation newCondition })
                => TryAddEqualizedFieldsForCondition(newCondition, !successRequirement, currentObject, otherObject, builder),
            // We want our equality check to be exhaustive, i.e. all checks must pass for the condition to pass
            // we recurse into each operand to try to find some props to bind
            (true, IBinaryOperation { OperatorKind: BinaryOperatorKind.ConditionalAnd } andOp)
                => TryAddEqualizedFieldsForCondition(andOp.LeftOperand, successRequirement, currentObject, otherObject, builder) &&
                    TryAddEqualizedFieldsForCondition(andOp.RightOperand, successRequirement, currentObject, otherObject, builder),
            // Exhaustive binary operator for inverted checks via DeMorgan's law
            // We see an or here, but we're in a context where this being true will return false
            // for example: return !(expr || expr)
            // or: if (expr || expr) return false;
            (false, IBinaryOperation { OperatorKind: BinaryOperatorKind.ConditionalOr } orOp)
                => TryAddEqualizedFieldsForCondition(orOp.LeftOperand, successRequirement, currentObject, otherObject, builder) &&
                    TryAddEqualizedFieldsForCondition(orOp.RightOperand, successRequirement, currentObject, otherObject, builder),
            // we are actually comparing two values that are potentially members,
            // e.g: return A == other.A;
            (true, IBinaryOperation
            {
                OperatorKind: BinaryOperatorKind.Equals,
                LeftOperand: IMemberReferenceOperation leftMemberReference,
                RightOperand: IMemberReferenceOperation rightMemberReference,
            }) => TryAddFieldFromComparison(leftMemberReference, rightMemberReference, currentObject, otherObject, builder),
            // we are comparing two potential members, but in a context where if the expression is true, we return false
            // e.g: return !(A != other.A); 
            (false, IBinaryOperation
            {
                OperatorKind: BinaryOperatorKind.NotEquals,
                LeftOperand: IMemberReferenceOperation leftMemberReference,
                RightOperand: IMemberReferenceOperation rightMemberReference,
            }) => TryAddFieldFromComparison(leftMemberReference, rightMemberReference, currentObject, otherObject, builder),
            // equals invocation, something like: A.Equals(other.A)
            (true, IInvocationOperation
            {
                TargetMethod.Name: nameof(Equals),
                Instance: IMemberReferenceOperation invokedOn,
                Arguments: [IMemberReferenceOperation arg]
            }) => TryAddFieldFromComparison(invokedOn, arg, currentObject, otherObject, builder),
            // some other operation, or an incorrect operation (!= when we expect == based on context, etc).
            // If one of the conditions is just a null check on the "otherObject", then it's valid but doesn't check any members
            // Otherwise we fail as it has unknown behavior
            _ => IsNullCheck(condition, successRequirement, otherObject)
        };
 
        /// <summary>
        /// Same as <see cref="TryAddEqualizedFieldsForCondition"/> but we're looking for
        /// a variable binding through an "is" pattern first/>
        /// </summary>
        /// <returns>the cast parameter symbol if found, null if not</returns>
        private static bool TryAddEqualizedFieldsForConditionWithoutTypedVariable(
            IOperation condition,
            bool successRequirement,
            ISymbol currentObject,
            ArrayBuilder<IFieldSymbol> builder,
            [NotNullWhen(true)] out ISymbol? boundVariable,
            IEnumerable<IOperation>? additionalConditions = null)
        {
            boundVariable = null;
            additionalConditions ??= Enumerable.Empty<IOperation>();
            return (successRequirement, condition) switch
            {
                (_, IUnaryOperation { OperatorKind: UnaryOperatorKind.Not, Operand: IOperation newCondition })
                    => TryAddEqualizedFieldsForConditionWithoutTypedVariable(
                        newCondition,
                        !successRequirement,
                        currentObject,
                        builder,
                        out boundVariable,
                        additionalConditions),
                (true, IBinaryOperation
                {
                    OperatorKind: BinaryOperatorKind.ConditionalAnd,
                    LeftOperand: IOperation leftOperation,
                    RightOperand: IOperation rightOperation,
                }) => TryAddEqualizedFieldsForConditionWithoutTypedVariable(
                        leftOperation,
                        successRequirement,
                        currentObject,
                        builder,
                        out boundVariable,
                        additionalConditions.Append(rightOperation)),
                (false, IBinaryOperation
                {
                    OperatorKind: BinaryOperatorKind.ConditionalOr,
                    LeftOperand: IOperation leftOperation,
                    RightOperand: IOperation rightOperation,
                }) => TryAddEqualizedFieldsForConditionWithoutTypedVariable(
                    leftOperation,
                    successRequirement,
                    currentObject,
                    builder,
                    out boundVariable,
                    additionalConditions.Append(rightOperation)),
                (_, IIsPatternOperation
                {
                    Pattern: IPatternOperation isPattern
                }) => TryGetBoundVariableForIsPattern(isPattern, out boundVariable),
                _ => false,
            };
 
            bool TryGetBoundVariableForIsPattern(IPatternOperation isPattern, [NotNullWhen(true)] out ISymbol? boundVariable)
            {
                boundVariable = null;
                // got to the leftmost condition and it is an "is" pattern
                if (!successRequirement)
                {
                    // we could be in an "expect false for successful equality" condition
                    // and so we would want the pattern to be an "is not" pattern instead of an "is" pattern
                    if (isPattern is INegatedPatternOperation negatedPattern)
                    {
                        isPattern = negatedPattern.Pattern;
                    }
                    else
                    {
                        // if we don't see "is not" then the pattern sequence is incorrect
                        return false;
                    }
                }
 
                if (isPattern is IDeclarationPatternOperation
                    {
                        DeclaredSymbol: ISymbol otherVar,
                        MatchedType: INamedTypeSymbol matchedType,
                    } &&
                    matchedType.Equals(currentObject.GetSymbolType()) &&
                    // found the correct binding, add any members we equate in the rest of the binary condition
                    // if we were in a binary condition at all, and signal failure if any condition is bad
                    additionalConditions.All(otherCondition => TryAddEqualizedFieldsForCondition(
                        otherCondition, successRequirement, currentObject, otherVar, builder)))
                {
                    boundVariable = otherVar;
                    return true;
                }
                return false;
            }
        }
 
        /// <summary>
        /// Match a list of statements and add members that are compared
        /// </summary>
        /// <param name="statementsToCheck">operations which should compare members</param>
        /// <param name="otherC">non-this comparison of the type we're equating</param>
        /// <param name="type">the this symbol</param>
        /// <param name="builder">builder for property/field comparisons we encounter</param>
        /// <returns>whether every statment was one of the expected forms</returns>
        private static bool TryAddEqualizedFieldsForStatements(
            IEnumerable<IOperation> statementsToCheck,
            ISymbol otherC,
            INamedTypeSymbol type,
            ArrayBuilder<IFieldSymbol> builder)
            => statementsToCheck.FirstOrDefault() switch
            {
                IReturnOperation
                {
                    ReturnedValue: ILiteralOperation
                    {
                        ConstantValue.HasValue: true,
                        ConstantValue.Value: true,
                    }
                }
                    // we are done with the comparison, the final statment does no checks
                    => true,
                IReturnOperation { ReturnedValue: IOperation value } => TryAddEqualizedFieldsForCondition(
                    value, successRequirement: true, currentObject: type, otherObject: otherC, builder: builder),
                IConditionalOperation
                {
                    Condition: IOperation condition,
                    WhenTrue: IOperation whenTrue,
                    WhenFalse: var whenFalse,
                }
                    // 1. Check structure of if statment, get success requirement
                    // and any potential statments in the non failure block
                    // 2. Check condition for compared members
                    // 3. Check remaining members in non failure block
                    => TryGetSuccessCondition(whenTrue, whenFalse, statementsToCheck.Skip(1),
                        out var successRequirement, out var remainingStatements) &&
                    TryAddEqualizedFieldsForCondition(
                            condition, successRequirement, type, otherC, builder) &&
                    TryAddEqualizedFieldsForStatements(remainingStatements, otherC, type, builder),
                _ => false
            };
 
        private static bool TryAddFieldFromComparison(
            IMemberReferenceOperation memberReference1,
            IMemberReferenceOperation memberReference2,
            ISymbol currentObject,
            ISymbol otherObject,
            ArrayBuilder<IFieldSymbol> builder)
        {
            var leftObject = GetReferencedSymbolObject(memberReference1.Instance!);
            var rightObject = GetReferencedSymbolObject(memberReference2.Instance!);
            if (memberReference1.Member.Equals(memberReference2.Member) &&
                leftObject != null &&
                rightObject != null &&
                !leftObject.Equals(rightObject) &&
                AreConditionsSatisfiedEitherOrder(leftObject, rightObject, currentObject.Equals, otherObject.Equals))
            {
                var field = UnwrapPropertyToField(memberReference1.Member);
                if (field == null)
                    // not a field or no backing field for property so member is invalid
                    return false;
 
                builder.Add(field);
                return true;
            }
            return false;
        }
 
        /// <summary>
        /// Matches a pattern where the first statement is an if statement that ensures a cast
        /// of the parameter to the correct type, and either binds it through an "is" pattern
        /// or later assigns it to a local varaiable
        /// </summary>
        /// <param name="bodyOps">method body to search in</param>
        /// <param name="parameter">uncast object parameter</param>
        /// <param name="type">type which is being called</param>
        /// <param name="builder">members that may have been incidentally checked</param>
        /// <param name="otherC">if matched, the variable that the cast parameter was assigned to</param>
        /// <param name="statementsToCheck">remaining non-check, non-assignment operations
        /// to look for additional compared members. This can have statements even if there was no binding
        /// as long as it found an if check that checks the correct type</param>
        /// <returns>whether or not the pattern matched</returns>
        private static bool TryGetBindingCastInFirstIfStatement(
            ImmutableArray<IOperation> bodyOps,
            IParameterSymbol parameter,
            INamedTypeSymbol type,
            ArrayBuilder<IFieldSymbol> builder,
            [NotNullWhen(true)] out ISymbol? otherC,
            [NotNullWhen(true)] out IEnumerable<IOperation>? statementsToCheck)
        {
            otherC = null;
            statementsToCheck = null;
 
            // we require the if statement (with or without a cast) to be the first operation in the body
            if (bodyOps.FirstOrDefault() is not IConditionalOperation
                {
                    Condition: IOperation condition,
                    WhenTrue: IOperation whenTrue,
                    WhenFalse: var whenFalse,
                })
            {
                return false;
            }
 
            // find out if we return false after the condition is true or false and get the statements
            // corresponding to the other branch
            if (!TryGetSuccessCondition(
                whenTrue, whenFalse, bodyOps.Skip(1).AsImmutable(), out var successRequirement, out var remainingStatments))
            {
                return false;
            }
 
            // if we have no else block, we could get no remaining statements, in that case we take all the
            // statments after the if condition operation
            statementsToCheck = !remainingStatments.IsEmpty() ? remainingStatments : bodyOps.Skip(1);
 
            // checks for simple "is" or "is not" statement without a variable binding
            ITypeSymbol? testType = null;
            IParameterSymbol? referencedParameter = null;
            if (successRequirement)
            {
                if (condition is IIsTypeOperation typeCondition)
                {
                    testType = typeCondition.TypeOperand;
                    referencedParameter = (typeCondition.ValueOperand as IParameterReferenceOperation)?.Parameter;
                }
            }
            else
            {
                if (condition is IIsPatternOperation
                    {
                        Value: IParameterReferenceOperation parameterReference,
                        Pattern: INegatedPatternOperation
                        {
                            Pattern: ITypePatternOperation typePattern
                        }
                    })
                {
                    testType = typePattern.MatchedType;
                    referencedParameter = parameterReference.Parameter;
                }
            }
 
            if (testType != null && referencedParameter != null &&
                testType.Equals(type) && referencedParameter.Equals(parameter))
            {
                // found correct pattern/type check, so we know we have something equivalent to
                // if (other is C) { ... } else return false;
                // we look for an explicit cast to assign a variable like:
                // var otherC = (C)other;
                // var otherC = other as C;
                if (TryGetAssignmentFromParameterWithExplicitCast(statementsToCheck.FirstOrDefault(), parameter, out otherC))
                {
                    statementsToCheck = statementsToCheck.Skip(1);
                    return true;
                }
 
                return false;
            }
            // look for the condition to also contain a binding to a variable and optionally additional
            // checks based on that assigned variable
            return TryAddEqualizedFieldsForConditionWithoutTypedVariable(
                condition, successRequirement, type, builder, out otherC);
        }
 
        /// <summary>
        /// Attempts to get information about an if operation in an equals method,
        /// such as whether the condition being true would cause the method to return false
        /// and the remaining statments in the branch not returning false (if any)
        /// </summary>
        /// <param name="whenTrue">"then" branch</param>
        /// <param name="whenFalse">"else" branch (if any)</param>
        /// <param name="successRequirement">whether the condition being true would cause the method to return false
        /// or the condition being false would cause the method to return false</param>
        /// <param name="remainingStatements">Potential remaining statements of the branch that does not return false</param>
        /// <returns>whether the pattern was matched (one of the branches must have a simple "return false")</returns>
        private static bool TryGetSuccessCondition(
            IOperation whenTrue,
            IOperation? whenFalse,
            IEnumerable<IOperation> otherOps,
            out bool successRequirement,
            out IEnumerable<IOperation> remainingStatements)
        {
            // this will be changed if we successfully match the pattern
            successRequirement = default;
            // this could be empty even if we match, if there is no else block
            remainingStatements = Enumerable.Empty<IOperation>();
 
            // all the operations that would happen after the condition is true or false
            // branches can either be block bodies or single statements
            // each branch is followed by statements outside the branch either way
            var trueOps = ((whenTrue as IBlockOperation)?.Operations ?? ImmutableArray.Create(whenTrue))
                .Concat(otherOps);
            var falseOps = ((whenFalse as IBlockOperation)?.Operations ??
                (whenFalse != null
                    ? ImmutableArray.Create(whenFalse)
                    : ImmutableArray<IOperation>.Empty))
                .Concat(otherOps);
 
            // We expect one of the true or false branch to have exactly one statement: return false.
            // If we don't find that, it indicates complex behavior such as
            // extra statments, backup equality if one condition fails, or something else.
            // We don't necessarily expect a return true because we could see
            // a final return statement that checks a last set of conditions such as:
            // if (other is C otherC)
            // {
            //     return otherC.A == A;
            // }
            // return false;
            // We should never have a case where both branches could potentially return true;
            // at least one branch must simply return false.
            if (ReturnsFalseImmediately(trueOps) == ReturnsFalseImmediately(falseOps))
                // both or neither fit the return false pattern, this if statement either doesn't do
                // anything or does something too complex for us, so we assume it's not a default.
                return false;
 
            // when condition succeeds (evaluates to true), we return false
            // so for equality the condition should not succeed
            successRequirement = !ReturnsFalseImmediately(trueOps);
            remainingStatements = successRequirement ? trueOps : falseOps;
            return true;
        }
 
        /// <summary>
        /// Whether the equals method overrides object or IEquatable Equals method
        /// </summary>
        private static bool OverridesEquals(Compilation compilation, IMethodSymbol equals, INamedTypeSymbol? equatableType)
        {
            if (equatableType != null &&
                equatableType.GetMembers(nameof(Equals)).FirstOrDefault() is IMethodSymbol equatableEquals &&
                equals.Equals(equals.ContainingType.FindImplementationForInterfaceMember(equatableEquals)))
            {
                return true;
            }
 
            var objectType = compilation.GetSpecialType(SpecialType.System_Object);
            var objectEquals = objectType?.GetMembers(nameof(Equals)).FirstOrDefault() as IMethodSymbol;
            var curr = equals;
            while (curr != null)
            {
                if (curr.Equals(objectEquals))
                    return true;
                curr = curr.OverriddenMethod;
            }
 
            return false;
        }
 
        private static IBlockOperation? GetBlockOfMethodBody(IMethodBodyBaseOperation body)
            => body.BlockBody ?? body.ExpressionBody;
 
        private static IFieldSymbol? UnwrapPropertyToField(ISymbol propertyOrField)
            => propertyOrField switch
            {
                IPropertySymbol prop => prop.GetBackingFieldIfAny(),
                IFieldSymbol field => field,
                _ => null
            };
 
        private static bool AreConditionsSatisfiedEitherOrder<T>(T firstItem, T secondItem,
            Func<T, bool> firstCondition, Func<T, bool> secondCondition)
        {
            return (firstCondition(firstItem) && secondCondition(secondItem))
                || (firstCondition(secondItem) && secondCondition(firstItem));
        }
    }
}