File: src\roslyn\src\Workspaces\SharedUtilitiesAndExtensions\Workspace\Core\Extensions\SyntaxGeneratorExtensions_CreateEqualsMethod.cs
Web Access
Project: src\roslyn\src\RoslynAnalyzers\Roslyn.Diagnostics.Analyzers\Core\Roslyn.Diagnostics.Analyzers.csproj (Roslyn.Diagnostics.Analyzers)
// 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.CodeGeneration;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Shared.Extensions;

internal static partial class SyntaxGeneratorExtensions
{
    public static IMethodSymbol CreateEqualsMethod(
        this SyntaxGenerator factory,
        SyntaxGeneratorInternal generatorInternal,
        Compilation compilation,
        ParseOptions parseOptions,
        INamedTypeSymbol containingType,
        ImmutableArray<ISymbol> symbols,
        string? localNameOpt,
        SyntaxAnnotation statementAnnotation)
    {
        var isRecord = containingType.IsRecord;
        var localName = localNameOpt ?? (isRecord ? GetLocalName(containingType) : null);
        var parameterName = isRecord ? localName : ObjName;
        var statements = CreateEqualsMethodStatements(
            factory, generatorInternal, compilation, parseOptions, containingType, symbols, localName);

        return CreateEqualsMethod(
            compilation,
            modifiers: isRecord ? containingType.IsSealed ? DeclarationModifiers.None : DeclarationModifiers.Virtual : null,
            parameterType: isRecord ? containingType : null,
            parameterName: parameterName,
            statements.SelectAsArray(s => s.WithAdditionalAnnotations(statementAnnotation)));
    }

    public static IMethodSymbol CreateEqualsMethod(this Compilation compilation, ImmutableArray<SyntaxNode> statements)
        => CreateEqualsMethod(compilation, modifiers: null, parameterType: null, parameterName: null, statements);

    private static IMethodSymbol CreateEqualsMethod(
        this Compilation compilation,
        DeclarationModifiers? modifiers,
        ITypeSymbol? parameterType,
        string? parameterName,
        ImmutableArray<SyntaxNode> statements)
    {
        parameterType ??= compilation.GetSpecialType(SpecialType.System_Object);
        parameterName ??= ObjName;
        modifiers ??= DeclarationModifiers.Override;

        return CodeGenerationSymbolFactory.CreateMethodSymbol(
            attributes: default,
            accessibility: Accessibility.Public,
            modifiers: modifiers.Value,
            returnType: compilation.GetSpecialType(SpecialType.System_Boolean),
            refKind: RefKind.None,
            explicitInterfaceImplementations: default,
            name: EqualsName,
            typeParameters: default,
            parameters: [CodeGenerationSymbolFactory.CreateParameterSymbol(parameterType.WithNullableAnnotation(NullableAnnotation.Annotated), parameterName)],
            statements: statements);
    }

    public static IMethodSymbol CreateIEquatableEqualsMethod(
        this SyntaxGenerator factory,
        SyntaxGeneratorInternal generatorInternal,
        SemanticModel semanticModel,
        INamedTypeSymbol containingType,
        ImmutableArray<ISymbol> symbols,
        INamedTypeSymbol constructedEquatableType,
        SyntaxAnnotation statementAnnotation)
    {
        var statements = CreateIEquatableEqualsMethodStatements(
            factory, generatorInternal, semanticModel.Compilation, semanticModel.SyntaxTree.Options, containingType, symbols);
        statements = statements.SelectAsArray(s => s.WithAdditionalAnnotations(statementAnnotation));

        var methodSymbol = constructedEquatableType
            .GetMembers(EqualsName)
            .OfType<IMethodSymbol>()
            .Single(m => containingType.Equals(m.Parameters.FirstOrDefault()?.Type));

        var originalParameter = methodSymbol.Parameters.First();

        // Replace `[AllowNull] Foo` with `Foo` or `Foo?` (no longer needed after https://github.com/dotnet/roslyn/issues/39256?)
        var parameters = ImmutableArray.Create(CodeGenerationSymbolFactory.CreateParameterSymbol(
            originalParameter,
            type: constructedEquatableType.GetTypeArguments()[0],
            attributes: ImmutableArray<AttributeData>.Empty));

        if (generatorInternal.RequiresExplicitImplementationForInterfaceMembers)
        {
            return CodeGenerationSymbolFactory.CreateMethodSymbol(
                methodSymbol,
                modifiers: DeclarationModifiers.None,
                explicitInterfaceImplementations: [methodSymbol],
                parameters: parameters,
                statements: statements);
        }
        else
        {
            return CodeGenerationSymbolFactory.CreateMethodSymbol(
                methodSymbol,
                modifiers: DeclarationModifiers.None,
                parameters: parameters,
                statements: statements);
        }
    }

    private static ImmutableArray<SyntaxNode> CreateEqualsMethodStatements(
        SyntaxGenerator factory,
        SyntaxGeneratorInternal generatorInternal,
        Compilation compilation,
        ParseOptions parseOptions,
        INamedTypeSymbol containingType,
        ImmutableArray<ISymbol> members,
        string? localNameOpt)
    {
        // A ref like type can not be boxed. Because of this an overloaded Equals taking object in the general case
        // can never be true, because an equivalent object can never be boxed into the object itself. Therefore only
        // need to return false.
        if (containingType.IsRefLikeType)
            return [factory.ReturnStatement(factory.FalseLiteralExpression())];

        using var statements = TemporaryArray<SyntaxNode>.Empty;

        // Come up with a good name for the local variable we're going to compare against.
        // For example, if the class name is "CustomerOrder" then we'll generate:
        //
        //      var order = obj as CustomerOrder;

        var localName = localNameOpt ?? GetLocalName(containingType);

        var localNameExpression = factory.IdentifierName(localName);
        var objNameExpression = factory.IdentifierName(ObjName);

        // These will be all the expressions that we'll '&&' together inside the final
        // return statement of 'Equals'.
        using var _2 = ArrayBuilder<SyntaxNode>.GetInstance(out var expressions);

        if (containingType.IsRecord)
        {
            if (!containingType.IsValueType)
            {
                // Ensure that the parameter we got was not null.
                AddReferenceNotNullCheck(
                    factory, generatorInternal, compilation, parseOptions, localNameExpression, expressions);
            }
        }
        else
        {
            if (generatorInternal.SupportsPatterns(parseOptions))
            {
                // If we support patterns then we can do "return obj is MyType myType && ..."
                expressions.Add(
                    generatorInternal.IsPatternExpression(objNameExpression,
                        generatorInternal.DeclarationPattern(containingType, localName)));
            }
            else if (containingType.IsValueType)
            {
                // If we're a value type, then we need an is-check first to make sure
                // the object is our type:
                //
                //      if (!(obj is MyType))
                //      {
                //          return false;
                //      }
                var ifStatement = factory.IfStatement(
                    factory.LogicalNotExpression(
                        factory.IsTypeExpression(
                            objNameExpression,
                            containingType)),
                    [factory.ReturnStatement(factory.FalseLiteralExpression())]);

                // Next, we cast the argument to our type:
                //
                //      var myType = (MyType)obj;

                var localDeclaration = factory.SimpleLocalDeclarationStatement(generatorInternal,
                    containingType, localName, factory.CastExpression(containingType, objNameExpression));

                statements.Add(ifStatement);
                statements.Add(localDeclaration);
            }
            else
            {
                // It's not a value type, we can just use "as" to test the parameter is the right type:
                //
                //      var myType = obj as MyType;

                var localDeclaration = factory.SimpleLocalDeclarationStatement(generatorInternal,
                    containingType, localName, factory.TryCastExpression(objNameExpression, containingType));

                statements.Add(localDeclaration);

                // Ensure that the parameter we got was not null (which also ensures the 'as' test succeeded):
                AddReferenceNotNullCheck(
                    factory, generatorInternal, compilation, parseOptions, localNameExpression, expressions);
            }
        }

        if (!containingType.IsValueType && HasExistingBaseEqualsMethod(containingType))
        {
            // If we're overriding something that also provided an overridden 'Equals',
            // then ensure the base type thinks it is equals as well.
            //
            //      base.Equals(obj)
            expressions.Add(factory.InvocationExpression(
                factory.MemberAccessExpression(
                    factory.BaseExpression(),
                    factory.IdentifierName(EqualsName)),
                containingType.IsRecord ? localNameExpression : objNameExpression));
        }

        AddMemberChecks(factory, generatorInternal, compilation, members, localNameExpression, expressions);

        // Now combine all the comparison expressions together into one final statement like:
        //
        //      return myType != null &&
        //             base.Equals(obj) &&
        //             this.S1 == myType.S1;
        statements.Add(factory.ReturnStatement(
            expressions.Aggregate(factory.LogicalAndExpression)));

        return statements.ToImmutableAndClear();
    }

    private static void AddMemberChecks(
        SyntaxGenerator factory, SyntaxGeneratorInternal generatorInternal, Compilation compilation,
        ImmutableArray<ISymbol> members, SyntaxNode localNameExpression,
        ArrayBuilder<SyntaxNode> expressions)
    {
        var iequatableType = compilation.GetTypeByMetadataName(typeof(IEquatable<>).FullName);

        // Now, iterate over all the supplied members and ensure that our instance
        // and the parameter think they are equals.  Specialize how we do this for
        // common types.  Fall-back to EqualityComparer<SType>.Default.Equals for
        // everything else.
        foreach (var member in members)
        {
            var symbolNameExpression = factory.IdentifierName(member.Name);
            var thisSymbol = factory.MemberAccessExpression(factory.ThisExpression(), symbolNameExpression)
                                    .WithAdditionalAnnotations(Simplification.Simplifier.Annotation);
            var otherSymbol = factory.MemberAccessExpression(localNameExpression, symbolNameExpression);

            var memberType = member.GetSymbolType();

            if (ShouldUseEqualityOperator(memberType))
            {
                expressions.Add(factory.ValueEqualsExpression(thisSymbol, otherSymbol));
                continue;
            }

            var valueIEquatable = memberType?.IsValueType == true && ImplementsIEquatable(memberType, iequatableType);
            if (valueIEquatable || memberType?.IsTupleType == true)
            {
                // If it's a value type and implements IEquatable<T>, Or if it's a tuple, then
                // just call directly into .Equals. This keeps the code simple and avoids an
                // unnecessary null check.
                //
                //      this.a.Equals(other.a)
                expressions.Add(factory.InvocationExpression(
                    factory.MemberAccessExpression(thisSymbol, nameof(object.Equals)),
                    otherSymbol));
                continue;
            }

            // Otherwise call EqualityComparer<SType>.Default.Equals(this.a, other.a).
            // This will do the appropriate null checks as well as calling directly
            // into IEquatable<T>.Equals implementations if available.

            expressions.Add(factory.InvocationExpression(
                    factory.MemberAccessExpression(
                        GetDefaultEqualityComparer(factory, generatorInternal, compilation, GetType(compilation, member)),
                        factory.IdentifierName(EqualsName)),
                    thisSymbol,
                    otherSymbol));
        }
    }

    private static ImmutableArray<SyntaxNode> CreateIEquatableEqualsMethodStatements(
        SyntaxGenerator factory,
        SyntaxGeneratorInternal generatorInternal,
        Compilation compilation,
        ParseOptions parseOptions,
        INamedTypeSymbol containingType,
        ImmutableArray<ISymbol> members)
    {
        var statements = ArrayBuilder<SyntaxNode>.GetInstance();

        var otherNameExpression = factory.IdentifierName(OtherName);

        // These will be all the expressions that we'll '&&' together inside the final
        // return statement of 'Equals'.
        using var _ = ArrayBuilder<SyntaxNode>.GetInstance(out var expressions);

        if (!containingType.IsValueType)
        {
            // It's not a value type. Ensure that the parameter we got was not null.

            // if we support patterns, we can do `x is not null`
            AddReferenceNotNullCheck(
                factory, generatorInternal, compilation, parseOptions, otherNameExpression, expressions);

            if (HasExistingBaseEqualsMethod(containingType))
            {
                // If we're overriding something that also provided an overridden 'Equals',
                // then ensure the base type thinks it is equals as well.
                //
                //      base.Equals(obj)
                expressions.Add(factory.InvocationExpression(
                    factory.MemberAccessExpression(
                        factory.BaseExpression(),
                        factory.IdentifierName(EqualsName)),
                    otherNameExpression));
            }
        }

        AddMemberChecks(factory, generatorInternal, compilation, members, otherNameExpression, expressions);

        // Now combine all the comparison expressions together into one final statement like:
        //
        //      return other != null &&
        //             base.Equals(other) &&
        //             this.S1 == other.S1;
        statements.Add(factory.ReturnStatement(
            expressions.Aggregate(factory.LogicalAndExpression)));

        return statements.ToImmutableAndFree();
    }

    private static void AddReferenceNotNullCheck(
        SyntaxGenerator factory, SyntaxGeneratorInternal generatorInternal, Compilation compilation, ParseOptions parseOptions, SyntaxNode otherNameExpression, ArrayBuilder<SyntaxNode> expressions)
    {
        var nullLiteral = factory.NullLiteralExpression();
        if (compilation.Language == LanguageNames.VisualBasic)
        {
            // VB supports `x is not nothing` as an idiomatic null check.
            expressions.Add(factory.ReferenceNotEqualsExpression(otherNameExpression, nullLiteral));
            return;
        }

        if (generatorInternal.SyntaxFacts.SupportsNotPattern(parseOptions))
        {
            // If we support not patterns then we can do "obj is not null && ..."
            expressions.Add(
                generatorInternal.IsPatternExpression(otherNameExpression,
                    generatorInternal.NotPattern(
                        generatorInternal.ConstantPattern(nullLiteral))));
        }
        else if (generatorInternal.SupportsPatterns(parseOptions))
        {
            // if we support patterns then we can do `!(obj is null)`
            expressions.Add(
                factory.LogicalNotExpression(
                    generatorInternal.IsPatternExpression(otherNameExpression,
                        generatorInternal.ConstantPattern(nullLiteral))));
        }
        else
        {
            // Otherwise, emit a call to ReferenceEquals(x, null) as the best way to do a null check
            // without potentially going through an overloaded operator (now or in the future).
            expressions.Add(
                factory.LogicalNotExpression(
                    factory.InvocationExpression(
                        factory.IdentifierName(nameof(ReferenceEquals)),
                        otherNameExpression,
                        nullLiteral)));
        }
    }

#nullable enable

    [return: NotNullIfNotNull(nameof(fallback))]
    public static string? GetLocalName(this ITypeSymbol containingType, string? fallback = "v")
    {
        // Don't want to do things like `String string`.  That's not idiomatic in .net.
        if (!containingType.IsSpecialType())
        {
            var name = containingType.Name;
            if (name.Length > 0)
            {
                using var parts = TemporaryArray<TextSpan>.Empty;
                StringBreaker.AddWordParts(name, ref parts.AsRef());
                for (var i = parts.Count - 1; i >= 0; i--)
                {
                    var p = parts[i];
                    if (p.Length > 0 && char.IsLetter(name[p.Start]))
                        return name.Substring(p.Start, p.Length).ToCamelCase();
                }
            }
        }

        return fallback;
    }

    private static bool ImplementsIEquatable(ITypeSymbol memberType, INamedTypeSymbol? iequatableType)
    {
        if (iequatableType != null)
        {
            // We compare ignoring nested nullability here, as it's possible the underlying object could have implemented IEquatable<Type>
            // or IEquatable<Type?>. From the perspective of this, either is allowable.
            var constructed = iequatableType.Construct(memberType);
            return memberType.AllInterfaces.Contains(constructed, equalityComparer: SymbolEqualityComparer.Default);
        }

        return false;
    }

    private static bool ShouldUseEqualityOperator(ITypeSymbol? typeSymbol)
    {
        if (typeSymbol != null)
        {
            if (typeSymbol.IsNullable(out var underlyingType))
            {
                typeSymbol = underlyingType;
            }

            if (typeSymbol.IsEnumType())
            {
                return true;
            }

            switch (typeSymbol.SpecialType)
            {
                case SpecialType.System_Boolean:
                case SpecialType.System_Char:
                case SpecialType.System_SByte:
                case SpecialType.System_Byte:
                case SpecialType.System_Int16:
                case SpecialType.System_UInt16:
                case SpecialType.System_Int32:
                case SpecialType.System_UInt32:
                case SpecialType.System_Int64:
                case SpecialType.System_UInt64:
                case SpecialType.System_Decimal:
                case SpecialType.System_Single:
                case SpecialType.System_Double:
                case SpecialType.System_String:
                case SpecialType.System_DateTime:
                    return true;
            }
        }

        return false;
    }

    private static bool HasExistingBaseEqualsMethod(INamedTypeSymbol containingType)
    {
        // Check if any of our base types override Equals.  If so, first check with them.
        var existingMethods =
            from baseType in containingType.GetBaseTypes()
            from method in baseType.GetMembers(EqualsName).OfType<IMethodSymbol>()
            where method.IsOverride &&
                  method.DeclaredAccessibility == Accessibility.Public &&
                  !method.IsStatic &&
                  method.Parameters.Length == 1 &&
                  method.ReturnType.SpecialType == SpecialType.System_Boolean &&
                  method.Parameters[0].Type.SpecialType == SpecialType.System_Object &&
                  !method.IsAbstract
            select method;

        return existingMethods.Any();
    }

    public static ImmutableArray<ISymbol> CreateMemberDelegatingConstructor(
        this SyntaxGenerator factory,
        SyntaxGeneratorInternal generatorInternal,
        SemanticModel semanticModel,
        string typeName,
        INamedTypeSymbol? containingType,
        ImmutableArray<IParameterSymbol> parameters,
        Accessibility accessibility,
        ImmutableDictionary<string, ISymbol>? parameterToExistingMemberMap,
        ImmutableDictionary<string, string>? parameterToNewMemberMap,
        bool addNullChecks,
        bool preferThrowExpression,
        bool generateProperties,
        bool isContainedInUnsafeType)
    {
        var newMembers = generateProperties
            ? CreatePropertiesForParameters(parameters, parameterToNewMemberMap, isContainedInUnsafeType)
            : CreateFieldsForParameters(parameters, parameterToNewMemberMap, isContainedInUnsafeType);
        var statements = factory.CreateAssignmentStatements(
            generatorInternal, semanticModel,
            parameters, parameterToExistingMemberMap, parameterToNewMemberMap,
            addNullChecks, preferThrowExpression).SelectAsArray(
                s => s.WithAdditionalAnnotations(Simplifier.Annotation));

        var constructor = CodeGenerationSymbolFactory.CreateConstructorSymbol(
            attributes: default,
            accessibility: accessibility,
            modifiers: DeclarationModifiers.None.WithIsUnsafe(!isContainedInUnsafeType && parameters.Any(static p => p.RequiresUnsafeModifier())),
            typeName: typeName,
            parameters: parameters,
            statements: statements,
            thisConstructorArguments: ShouldGenerateThisConstructorCall(containingType, parameterToExistingMemberMap)
                ? []
                : default);

        return newMembers.Concat(constructor);
    }

    private static bool ShouldGenerateThisConstructorCall(
        INamedTypeSymbol? containingType,
        IDictionary<string, ISymbol>? parameterToExistingFieldMap)
    {
        if (containingType?.TypeKind == TypeKind.Struct)
        {
            // Special case.  If we're generating a struct constructor, then we'll need
            // to initialize all fields in the struct, not just the ones we're creating.
            // If there is any field or auto-property not being set by a parameter, we
            // call the default constructor.

            return containingType.GetMembers()
                .OfType<IFieldSymbol>()
                .Where(field => !field.IsStatic)
                .Select(field => field.AssociatedSymbol ?? field)
                .Except(parameterToExistingFieldMap?.Values ?? [])
                .Any();
        }

        return false;
    }
}