// 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;
}
}