From 006875883c515ddfd2a01050fabb7b6ccb4a80a2 Mon Sep 17 00:00:00 2001 From: Charles Stoner Date: Tue, 2 Jun 2020 14:58:58 -0700 Subject: [PATCH 1/7] Records: Support EqualityContract in Equals --- .../Compiler/MethodBodySynthesizer.Lowered.cs | 9 +- .../CSharp/Portable/Symbols/LexicalSortKey.cs | 15 +- .../Source/SourceMemberContainerSymbol.cs | 86 +- .../Records/SynthesizedRecordCopyCtor.cs | 16 +- ...nthesizedRecordEqualityContractProperty.cs | 182 ++++ .../Records/SynthesizedRecordEquals.cs | 182 +++- .../Records/SynthesizedRecordGetHashCode.cs | 8 +- .../Records/SynthesizedRecordObjEquals.cs | 18 +- .../Test/Semantic/Semantics/RecordTests.cs | 974 +++++++++++++++++- .../Test/Symbol/Symbols/Source/RecordTests.cs | 298 +++--- 10 files changed, 1528 insertions(+), 260 deletions(-) create mode 100644 src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityContractProperty.cs diff --git a/src/Compilers/CSharp/Portable/Compiler/MethodBodySynthesizer.Lowered.cs b/src/Compilers/CSharp/Portable/Compiler/MethodBodySynthesizer.Lowered.cs index 46f515c3e9b06..b472deecd3eba 100644 --- a/src/Compilers/CSharp/Portable/Compiler/MethodBodySynthesizer.Lowered.cs +++ b/src/Compilers/CSharp/Portable/Compiler/MethodBodySynthesizer.Lowered.cs @@ -4,7 +4,6 @@ #nullable enable -using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using Microsoft.CodeAnalysis.PooledObjects; @@ -249,18 +248,18 @@ internal override void GenerateMethodBody(TypeCompilationState compilationState, /// Contains methods related to synthesizing bound nodes in lowered form /// that does not need any processing before passing to codegen /// - internal static partial class MethodBodySynthesizer + internal static class MethodBodySynthesizer { /// /// Given a set of fields, produce an expression that is true when all of the given fields on /// `this` are equal to the fields on according to the /// default EqualityComparer. /// - public static BoundExpression GenerateFieldEquals( + public static BoundExpression GenerateFieldEquals( BoundExpression? initialExpression, BoundExpression otherReceiver, - TList fields, - SyntheticBoundNodeFactory F) where TList : IReadOnlyList + ArrayBuilder fields, + SyntheticBoundNodeFactory F) { Debug.Assert(fields.Count > 0); diff --git a/src/Compilers/CSharp/Portable/Symbols/LexicalSortKey.cs b/src/Compilers/CSharp/Portable/Symbols/LexicalSortKey.cs index 87193c5cf81b7..af66ae2e30dce 100644 --- a/src/Compilers/CSharp/Portable/Symbols/LexicalSortKey.cs +++ b/src/Compilers/CSharp/Portable/Symbols/LexicalSortKey.cs @@ -2,20 +2,15 @@ // 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.Diagnostics; using System.Threading; -using Microsoft.CodeAnalysis.CSharp.Symbols; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.CSharp.Symbols { /// /// A structure used to lexically order symbols. For performance, it's important that this be /// a STRUCTURE, and be able to be returned from a symbol without doing any additional allocations (even - /// if nothing is cached yet.) + /// if nothing is cached yet). /// internal struct LexicalSortKey { @@ -43,18 +38,14 @@ public int Position public static readonly LexicalSortKey NotInitialized = new LexicalSortKey() { _treeOrdinal = -1, _position = -1 }; - // Put Record Equals right before synthesized constructors. - public static LexicalSortKey SynthesizedRecordEquals => new LexicalSortKey() { _treeOrdinal = int.MaxValue, _position = int.MaxValue - 5 }; - public static LexicalSortKey SynthesizedRecordObjEquals => new LexicalSortKey() { _treeOrdinal = int.MaxValue, _position = int.MaxValue - 4 }; - + // Put other synthesized members right before synthesized constructors. + public static LexicalSortKey GetSynthesizedMemberKey(int offset) => new LexicalSortKey() { _treeOrdinal = int.MaxValue, _position = int.MaxValue - 2 - offset }; // Dev12 compiler adds synthetic constructors to the child list after adding all other members. // Methods are emitted in the children order, but synthetic cctors would be deferred // until later when it is known if they can be optimized or not. // As a result the last emitted method tokens are synthetic ctor and then synthetic cctor (if not optimized) // Since it is not too hard, we will try keeping the same order just to be easy on metadata diffing tools and such. - public static readonly LexicalSortKey SynthesizedRecordCopyCtor = new LexicalSortKey() { _treeOrdinal = int.MaxValue, _position = int.MaxValue - 3 }; - public static readonly LexicalSortKey SynthesizedRecordCtor = new LexicalSortKey() { _treeOrdinal = int.MaxValue, _position = int.MaxValue - 2 }; public static readonly LexicalSortKey SynthesizedCtor = new LexicalSortKey() { _treeOrdinal = int.MaxValue, _position = int.MaxValue - 1 }; public static readonly LexicalSortKey SynthesizedCCtor = new LexicalSortKey() { _treeOrdinal = int.MaxValue, _position = int.MaxValue }; diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs index d5333a46ec176..09d1432ea534b 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs @@ -2991,10 +2991,16 @@ private void AddSynthesizedRecordMembersIfNecessary(MembersAndInitializersBuilde addCopyCtor(); addCloneMethod(); - var thisEquals = addThisEquals(); + var equalityContract = addEqualityContract(); + var otherEqualsMethods = ArrayBuilder.GetInstance(); // PROTOTYPE: We don't need to hold onto the other Equals methods. The values aren't used. + getOtherEquals(otherEqualsMethods, equalityContract); + + var thisEquals = addThisEquals(equalityContract, otherEqualsMethods.Count == 0 ? null : otherEqualsMethods[0]); + addOtherEquals(otherEqualsMethods, equalityContract, thisEquals); addObjectEquals(thisEquals); addHashCode(); + otherEqualsMethods.Free(); memberSignatures.Free(); return; @@ -3017,7 +3023,7 @@ SynthesizedRecordConstructor addCtor(RecordDeclarationSyntax declWithParameters) void addCopyCtor() { - var ctor = new SynthesizedRecordCopyCtor(this, diagnostics); + var ctor = new SynthesizedRecordCopyCtor(this, memberOffset: members.Count); if (!memberSignatures.ContainsKey(ctor)) { members.Add(ctor); @@ -3040,7 +3046,7 @@ void addProperties(ImmutableArray recordParameters) { var property = new SynthesizedRecordPropertySymbol(this, param, diagnostics); if (!memberSignatures.ContainsKey(property) && - !hidesInheritedMember(property, this)) + getInheritedMember(property, this) is null) { members.Add(property); members.Add(property.GetMethod); @@ -3062,7 +3068,7 @@ void addProperties(ImmutableArray recordParameters) #endif } - static bool hidesInheritedMember(Symbol symbol, NamedTypeSymbol type) + static Symbol? getInheritedMember(Symbol symbol, NamedTypeSymbol type) { while ((type = type.BaseTypeNoUseSiteDiagnostics) is object) { @@ -3076,20 +3082,21 @@ static bool hidesInheritedMember(Symbol symbol, NamedTypeSymbol type) out var hiddenBuilder); if (hiddenBuilder is object) { + var result = hiddenBuilder[0]; hiddenBuilder.Free(); - return true; + return result; } if (bestMatch is object) { - return true; + return bestMatch; } } - return false; + return null; } void addObjectEquals(MethodSymbol thisEquals) { - var objEquals = new SynthesizedRecordObjEquals(this, thisEquals); + var objEquals = new SynthesizedRecordObjEquals(this, thisEquals, memberOffset: members.Count); if (!memberSignatures.ContainsKey(objEquals)) { // https://github.com/dotnet/roslyn/issues/44617: Don't add if the overridden method is sealed @@ -3099,7 +3106,7 @@ void addObjectEquals(MethodSymbol thisEquals) void addHashCode() { - var hashCode = new SynthesizedRecordGetHashCode(this); + var hashCode = new SynthesizedRecordGetHashCode(this, memberOffset: members.Count); if (!memberSignatures.ContainsKey(hashCode)) { // https://github.com/dotnet/roslyn/issues/44617: Don't add if the overridden method is sealed @@ -3107,9 +3114,25 @@ void addHashCode() } } - MethodSymbol addThisEquals() + PropertySymbol addEqualityContract() + { + var property = new SynthesizedRecordEqualityContractProperty(this, isOverride: false); + // PROTOTYPE: Test with explicit EqualityContract property from source. + // PROTOTYPE: Handle inherited member of unexpected member kind or unexpected + // property signature (distinct type, not virtual, sealed, etc.) + if (getInheritedMember(property, this) is PropertySymbol inheritedProperty) + { + // PROTOTYPE: Should not have to re-create property! + property = new SynthesizedRecordEqualityContractProperty(this, isOverride: true); + } + members.Add(property); + members.Add(property.GetMethod); + return property; + } + + MethodSymbol addThisEquals(PropertySymbol equalityContract, MethodSymbol? otherEqualsMethod) { - var thisEquals = new SynthesizedRecordEquals(this); + var thisEquals = new SynthesizedRecordEquals(this, parameterType: this, isOverride: false, equalityContract, otherEqualsMethod, memberOffset: members.Count); if (!memberSignatures.TryGetValue(thisEquals, out var existing)) { members.Add(thisEquals); @@ -3117,6 +3140,47 @@ MethodSymbol addThisEquals() } return (MethodSymbol)existing; } + + static void getOtherEquals(ArrayBuilder otherEqualsMethods, PropertySymbol equalityContract) + { + while ((equalityContract = equalityContract.OverriddenProperty) is object) + { + var containingType = equalityContract.ContainingType; + var member = containingType.GetMembers("Equals").FirstOrDefault(m => + { + if (m is MethodSymbol method) + { + var parameters = method.Parameters; + if (parameters.Length == 1 && parameters[0].Type.Equals(containingType, TypeCompareKind.AllIgnoreOptions)) + { + return true; + } + } + return false; + }); + // PROTOTYPE: Test with missing or unexpected Equals(Base) methods on base type. + if (member is MethodSymbol method) + { + otherEqualsMethods.Add(method); + } + } + } + + void addOtherEquals(ArrayBuilder otherEqualsMethods, PropertySymbol equalityContract, MethodSymbol thisEquals) + { + foreach (var otherEqualsMethod in otherEqualsMethods) + { + var method = new SynthesizedRecordEquals( + this, + parameterType: otherEqualsMethod.Parameters[0].Type, + isOverride: true, + equalityContract, + otherEqualsMethod: thisEquals, + memberOffset: members.Count); + // PROTOTYPE: Test with explicit strongly-typed Equals(Base) methods on derived record type. + members.Add(method); + } + } } private void AddSynthesizedConstructorsIfNecessary(ArrayBuilder members, ArrayBuilder> staticInitializers, DiagnosticBag diagnostics) diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordCopyCtor.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordCopyCtor.cs index b0a7adbac98af..564d9938d25e2 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordCopyCtor.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordCopyCtor.cs @@ -1,23 +1,24 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. #nullable enable using System.Collections.Immutable; -using System.Diagnostics; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.PooledObjects; namespace Microsoft.CodeAnalysis.CSharp.Symbols { internal sealed class SynthesizedRecordCopyCtor : SynthesizedInstanceConstructor { + private readonly int _memberOffset; + public SynthesizedRecordCopyCtor( SourceMemberContainerTypeSymbol containingType, - DiagnosticBag diagnostics) + int memberOffset) : base(containingType) { + _memberOffset = memberOffset; Parameters = ImmutableArray.Create(SynthesizedParameterSymbol.Create( this, TypeWithAnnotations.Create( @@ -29,12 +30,7 @@ public SynthesizedRecordCopyCtor( public override ImmutableArray Parameters { get; } - internal override LexicalSortKey GetLexicalSortKey() - { - // We need a separate sort key because struct records will have two synthesized - // constructors: the record constructor, and the parameterless constructor - return LexicalSortKey.SynthesizedRecordCopyCtor; - } + internal override LexicalSortKey GetLexicalSortKey() => LexicalSortKey.GetSynthesizedMemberKey(_memberOffset); internal override void GenerateMethodBodyStatements(SyntheticBoundNodeFactory F, ArrayBuilder statements, DiagnosticBag diagnostics) { diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityContractProperty.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityContractProperty.cs new file mode 100644 index 0000000000000..caf859edb1a86 --- /dev/null +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityContractProperty.cs @@ -0,0 +1,182 @@ +// 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. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Reflection; +using Microsoft.Cci; + +namespace Microsoft.CodeAnalysis.CSharp.Symbols +{ + internal sealed class SynthesizedRecordEqualityContractProperty : PropertySymbol + { + internal const string PropertyName = "EqualityContract"; + + public SynthesizedRecordEqualityContractProperty(NamedTypeSymbol containingType, bool isOverride) + { + ContainingType = containingType; + IsOverride = isOverride; + TypeWithAnnotations = TypeWithAnnotations.Create(containingType.DeclaringCompilation.GetWellKnownType(WellKnownType.System_Type), NullableAnnotation.NotAnnotated); + GetMethod = new GetAccessorSymbol(this); + } + + public override NamedTypeSymbol ContainingType { get; } + + public override MethodSymbol GetMethod { get; } + + public override MethodSymbol? SetMethod => null; + + public override RefKind RefKind => RefKind.None; + + public override TypeWithAnnotations TypeWithAnnotations { get; } + + public override ImmutableArray RefCustomModifiers => ImmutableArray.Empty; + + public override ImmutableArray Parameters => ImmutableArray.Empty; + + public override bool IsIndexer => false; + + public override bool IsImplicitlyDeclared => true; + + public override ImmutableArray ExplicitInterfaceImplementations => ImmutableArray.Empty; + + public override Symbol ContainingSymbol => ContainingType; + + public override ImmutableArray Locations => ContainingType.Locations; + + public override ImmutableArray DeclaringSyntaxReferences => ImmutableArray.Empty; + + public override Accessibility DeclaredAccessibility => Accessibility.Public; + + public override bool IsStatic => false; + + public override bool IsVirtual => true; + + public override bool IsOverride { get; } + + public override bool IsAbstract => false; + + public override bool IsSealed => false; + + public override bool IsExtern => false; + + internal override bool HasSpecialName => false; + + internal override CallingConvention CallingConvention => CallingConvention.HasThis; + + internal override bool MustCallMethodsDirectly => false; + + internal override ObsoleteAttributeData? ObsoleteAttributeData => null; + + public override string Name => PropertyName; + + public override ImmutableArray GetAttributes() => ImmutableArray.Empty; + + private sealed class GetAccessorSymbol : SynthesizedInstanceMethodSymbol + { + private readonly SynthesizedRecordEqualityContractProperty _property; + + public GetAccessorSymbol(SynthesizedRecordEqualityContractProperty property) + { + _property = property; + Name = SourcePropertyAccessorSymbol.GetAccessorName( + PropertyName, + getNotSet: true, + isWinMdOutput: false /* unused for getters */); + } + + public override string Name { get; } + + public override MethodKind MethodKind => MethodKind.PropertyGet; + + public override int Arity => 0; + + public override bool IsExtensionMethod => false; + + public override bool HidesBaseMethodsByName => false; + + public override bool IsVararg => false; + + public override bool ReturnsVoid => false; + + public override bool IsAsync => false; + + public override RefKind RefKind => RefKind.None; + + public override TypeWithAnnotations ReturnTypeWithAnnotations => _property.TypeWithAnnotations; + + public override FlowAnalysisAnnotations ReturnTypeFlowAnalysisAnnotations => FlowAnalysisAnnotations.None; + + public override ImmutableArray TypeArgumentsWithAnnotations => ImmutableArray.Empty; + + public override ImmutableArray TypeParameters => ImmutableArray.Empty; + + public override ImmutableArray Parameters => _property.Parameters; + + public override ImmutableArray ExplicitInterfaceImplementations => ImmutableArray.Empty; + + public override ImmutableArray RefCustomModifiers => _property.RefCustomModifiers; + + public override Symbol AssociatedSymbol => _property; + + public override Symbol ContainingSymbol => _property.ContainingSymbol; + + public override ImmutableArray Locations => _property.Locations; + + public override Accessibility DeclaredAccessibility => _property.DeclaredAccessibility; + + public override bool IsStatic => _property.IsStatic; + + public override bool IsVirtual => _property.IsVirtual; + + public override bool IsOverride => _property.IsOverride; + + public override bool IsAbstract => _property.IsAbstract; + + public override bool IsSealed => _property.IsSealed; + + public override bool IsExtern => _property.IsExtern; + + public override ImmutableHashSet ReturnNotNullIfParameterNotNull => ImmutableHashSet.Empty; + + internal override bool HasSpecialName => _property.HasSpecialName; + + internal override MethodImplAttributes ImplementationAttributes => MethodImplAttributes.Managed; + + internal override bool HasDeclarativeSecurity => false; + + internal override MarshalPseudoCustomAttributeData? ReturnValueMarshallingInformation => null; + + internal override bool RequiresSecurityObject => false; + + internal override CallingConvention CallingConvention => CallingConvention.HasThis; + + internal override bool GenerateDebugInfo => false; + + public override DllImportData? GetDllImportData() => null; + + internal override ImmutableArray GetAppliedConditionalSymbols() + => ImmutableArray.Empty; + + internal override IEnumerable GetSecurityInformation() + => Array.Empty(); + + internal override bool IsMetadataNewSlot(bool ignoreInterfaceImplementationChanges = false) => false; + + internal override bool IsMetadataVirtual(bool ignoreInterfaceImplementationChanges = false) => true; + + internal override bool SynthesizesLoweredBoundBody => true; + + internal override void GenerateMethodBody(TypeCompilationState compilationState, DiagnosticBag diagnostics) + { + var F = new SyntheticBoundNodeFactory(this, this.GetNonNullSyntaxNode(), compilationState, diagnostics); + F.CurrentFunction = this; + F.CloseMethod(F.Block(F.Return(F.Typeof(ContainingType)))); + } + } + } +} diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEquals.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEquals.cs index e17f6ffb6b6d2..3e84c821b19ab 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEquals.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEquals.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -13,17 +13,48 @@ namespace Microsoft.CodeAnalysis.CSharp.Symbols { + /// + /// A strongly-typed `public bool Equals(T other)` method. + /// There are two types of strongly-typed Equals methods: + /// the strongly-typed virtual method where T is the containing type; and + /// overrides of the strongly-typed virtual methods from base record types. + /// internal sealed class SynthesizedRecordEquals : SynthesizedInstanceMethodSymbol { + private readonly PropertySymbol _equalityContract; + private readonly MethodSymbol? _otherEqualsMethod; + private readonly int _memberOffset; + public override NamedTypeSymbol ContainingType { get; } - public SynthesizedRecordEquals(NamedTypeSymbol containingType) + public SynthesizedRecordEquals( + NamedTypeSymbol containingType, + TypeSymbol parameterType, + bool isOverride, + PropertySymbol equalityContract, + MethodSymbol? otherEqualsMethod, + int memberOffset) { + var compilation = containingType.DeclaringCompilation; + bool isStruct = parameterType.IsStructType(); + + _equalityContract = equalityContract; + _otherEqualsMethod = otherEqualsMethod; + _memberOffset = memberOffset; + ContainingType = containingType; - if (containingType.IsStructType()) + IsOverride = isOverride; + Parameters = ImmutableArray.Create(SynthesizedParameterSymbol.Create( + this, + TypeWithAnnotations.Create(parameterType, nullableAnnotation: isStruct ? NullableAnnotation.NotAnnotated : NullableAnnotation.Annotated), + ordinal: 0, + isStruct ? RefKind.In : RefKind.None)); + ReturnTypeWithAnnotations = TypeWithAnnotations.Create(compilation.GetSpecialType(SpecialType.System_Boolean)); + + if (isStruct) { // If the record type is a struct, the parameter is marked 'in' - containingType.DeclaringCompilation.EnsureIsReadOnlyAttributeExists( + compilation.EnsureIsReadOnlyAttributeExists( diagnostics: null, location: Location.None, modifyCompilation: true); @@ -48,19 +79,9 @@ public SynthesizedRecordEquals(NamedTypeSymbol containingType) public override RefKind RefKind => RefKind.None; - public override ImmutableArray Parameters - => ImmutableArray.Create(SynthesizedParameterSymbol.Create( - this, - TypeWithAnnotations.Create( - isNullableEnabled: true, - ContainingType, - isAnnotated: true), - ordinal: 0, - ContainingType.IsStructType() ? RefKind.In : RefKind.None)); + public override ImmutableArray Parameters { get; } - public override TypeWithAnnotations ReturnTypeWithAnnotations => TypeWithAnnotations.Create( - isNullableEnabled: true, - ContainingType.DeclaringCompilation.GetSpecialType(SpecialType.System_Boolean)); + public override TypeWithAnnotations ReturnTypeWithAnnotations { get; } public override FlowAnalysisAnnotations ReturnTypeFlowAnalysisAnnotations => FlowAnalysisAnnotations.None; @@ -85,9 +106,9 @@ public override ImmutableArray TypeArgumentsWithAnnotations public override bool IsStatic => false; - public override bool IsVirtual => false; + public override bool IsVirtual => true; - public override bool IsOverride => false; + public override bool IsOverride { get; } public override bool IsAbstract => false; @@ -97,7 +118,7 @@ public override ImmutableArray TypeArgumentsWithAnnotations internal override bool HasSpecialName => false; - internal override LexicalSortKey GetLexicalSortKey() => LexicalSortKey.SynthesizedRecordEquals; + internal override LexicalSortKey GetLexicalSortKey() => LexicalSortKey.GetSynthesizedMemberKey(_memberOffset); internal override MethodImplAttributes ImplementationAttributes => MethodImplAttributes.Managed; @@ -121,46 +142,117 @@ internal override IEnumerable GetSecurityInformation() internal override bool IsMetadataNewSlot(bool ignoreInterfaceImplementationChanges = false) => false; - internal override bool IsMetadataVirtual(bool ignoreInterfaceImplementationChanges = false) => false; + internal override bool IsMetadataVirtual(bool ignoreInterfaceImplementationChanges = false) => true; internal override bool SynthesizesLoweredBoundBody => true; + // Consider the following types: + // record A(int X); + // record B(int X, int Y) : A(X); + // record C(int X, int Y, int Z) : B(X, Y); + // + // Each record class defines a strongly-typed Equals method, with derived + // types overriding the methods from base classes: + // class A + // { + // public virtual bool Equals(A other) => other != null && EqualityContract == other.EqualityContract && X == other.X; + // } + // class B : A + // { + // public virtual bool Equals(B other) => base.Equals((A)other) && Y == other.Y; + // public override bool Equals(A other) => Equals(other as B); + // } + // class C : B + // { + // public virtual bool Equals(C other) => base.Equals((B)other) && Z == other.Z; + // public override bool Equals(B other) => Equals(other as C); + // public override bool Equals(A other) => Equals(other as C); + // } internal override void GenerateMethodBody(TypeCompilationState compilationState, DiagnosticBag diagnostics) { var F = new SyntheticBoundNodeFactory(this, ContainingType.GetNonNullSyntaxNode(), compilationState, diagnostics); - - // Compare all of the record properties in this class with the argument properties - // Body: - // { - // return other != null && `comparisons`; - // } - var other = F.Parameter(Parameters[0]); - BoundExpression? retExpr = null; - if (!ContainingType.IsStructType()) + BoundExpression? retExpr; + + if (IsOverride) { - retExpr = F.ObjectNotEqual(other, F.Null(F.SpecialType(SpecialType.System_Object))); + // This method is an override of a strongly-typed Equals method from a base record type. + // The definition of the method is as follows, and _otherEqualsMethod + // is the method to delegate to (see B.Equals(A), C.Equals(A), C.Equals(B) above): + // + // override bool Equals(Base other) => Equals(other as Derived); + retExpr = F.Call( + F.This(), + _otherEqualsMethod!, + F.As(other, ContainingType)); } - - var fields = ArrayBuilder.GetInstance(); - foreach (var f in ContainingType.GetFieldsToEmit()) + else { - if (!f.IsStatic) + // This method is the strongly-typed Equals method where the parameter type is + // the containing type. + + if (_otherEqualsMethod is null) { - fields.Add(f); + // There are no base record types. + // The definition of the method is as follows (see A.Equals(A) above): + // + // virtual bool Equals(T other) => + // other != null && + // EqualityContract == other.EqualityContract && + // field1 == other.field1 && ... && fieldN == other.fieldN; + + // other != null + retExpr = other.Type.IsStructType() ? + null : + F.ObjectNotEqual(other, F.Null(F.SpecialType(SpecialType.System_Object))); + + // EqualityContract == other.EqualityContract + var contractsEqual = F.Binary( + BinaryOperatorKind.ObjectEqual, + F.SpecialType(SpecialType.System_Boolean), + F.Property(F.This(), _equalityContract), + F.Property(other, _equalityContract)); + + retExpr = retExpr is null ? contractsEqual : F.LogicalAnd(retExpr, contractsEqual); } + else + { + // There are base record types. + // The definition of the method is as follows, and _otherEqualsMethod + // is the corresponding method on the nearest base record type to + // delegate to (see B.Equals(B), C.Equals(C) above): + // + // virtual bool Equals(Derived other) => + // base.Equals((Base)other) && + // field1 == other.field1 && ... && fieldN == other.fieldN; + retExpr = F.Call( + F.Base(_otherEqualsMethod.ContainingType), + _otherEqualsMethod!, + F.Convert(_otherEqualsMethod.Parameters[0].Type, other)); + } + + // field1 == other.field1 && ... && fieldN == other.fieldN + // PROTOTYPE: Should compare accessible fields from all non-record base types as well. + var fields = ArrayBuilder.GetInstance(); + foreach (var f in ContainingType.GetFieldsToEmit()) + { + if (!f.IsStatic) + { + fields.Add(f); + } + } + if (fields.Count > 0) + { + retExpr = MethodBodySynthesizer.GenerateFieldEquals( + retExpr, + other, + fields, + F); + } + fields.Free(); } - if (fields.Count > 0) - { - retExpr = MethodBodySynthesizer.GenerateFieldEquals( - retExpr, - other, - fields, - F); - } - fields.Free(); F.CloseMethod(F.Block(F.Return(retExpr))); } } -} \ No newline at end of file +} diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordGetHashCode.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordGetHashCode.cs index bdb1eade521a3..a381b14a0fcc7 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordGetHashCode.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordGetHashCode.cs @@ -14,11 +14,13 @@ namespace Microsoft.CodeAnalysis.CSharp.Symbols { internal sealed class SynthesizedRecordGetHashCode : SynthesizedInstanceMethodSymbol { - public override NamedTypeSymbol ContainingType { get; } + private readonly int _memberOffset; + public override NamedTypeSymbol ContainingType { get; } - public SynthesizedRecordGetHashCode(NamedTypeSymbol containingType) + public SynthesizedRecordGetHashCode(NamedTypeSymbol containingType, int memberOffset) { + _memberOffset = memberOffset; ContainingType = containingType; } @@ -42,7 +44,7 @@ public SynthesizedRecordGetHashCode(NamedTypeSymbol containingType) public override RefKind RefKind => RefKind.None; - internal override LexicalSortKey GetLexicalSortKey() => LexicalSortKey.SynthesizedRecordObjEquals; + internal override LexicalSortKey GetLexicalSortKey() => LexicalSortKey.GetSynthesizedMemberKey(_memberOffset); public override TypeWithAnnotations ReturnTypeWithAnnotations => TypeWithAnnotations.Create( isNullableEnabled: true, diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordObjEquals.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordObjEquals.cs index 08fc95051514d..e118bee3a8d7f 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordObjEquals.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordObjEquals.cs @@ -15,23 +15,24 @@ namespace Microsoft.CodeAnalysis.CSharp.Symbols internal sealed class SynthesizedRecordObjEquals : SynthesizedInstanceMethodSymbol { private readonly MethodSymbol _typedRecordEquals; + private readonly int _memberOffset; public override NamedTypeSymbol ContainingType { get; } public override ImmutableArray Parameters { get; } - public SynthesizedRecordObjEquals(NamedTypeSymbol containingType, MethodSymbol typedRecordEquals) + public SynthesizedRecordObjEquals(NamedTypeSymbol containingType, MethodSymbol typedRecordEquals, int memberOffset) { + var compilation = containingType.DeclaringCompilation; _typedRecordEquals = typedRecordEquals; + _memberOffset = memberOffset; ContainingType = containingType; Parameters = ImmutableArray.Create(SynthesizedParameterSymbol.Create( this, - TypeWithAnnotations.Create( - isNullableEnabled: true, - containingType.DeclaringCompilation.GetSpecialType(SpecialType.System_Object), - isAnnotated: true), + TypeWithAnnotations.Create(compilation.GetSpecialType(SpecialType.System_Object), NullableAnnotation.Annotated), ordinal: 0, RefKind.None)); + ReturnTypeWithAnnotations = TypeWithAnnotations.Create(compilation.GetSpecialType(SpecialType.System_Boolean)); } public override string Name => "Equals"; @@ -52,11 +53,9 @@ public SynthesizedRecordObjEquals(NamedTypeSymbol containingType, MethodSymbol t public override RefKind RefKind => RefKind.None; - internal override LexicalSortKey GetLexicalSortKey() => LexicalSortKey.SynthesizedRecordObjEquals; + internal override LexicalSortKey GetLexicalSortKey() => LexicalSortKey.GetSynthesizedMemberKey(_memberOffset); - public override TypeWithAnnotations ReturnTypeWithAnnotations => TypeWithAnnotations.Create( - isNullableEnabled: true, - ContainingType.DeclaringCompilation.GetSpecialType(SpecialType.System_Boolean)); + public override TypeWithAnnotations ReturnTypeWithAnnotations { get; } public override FlowAnalysisAnnotations ReturnTypeFlowAnalysisAnnotations => FlowAnalysisAnnotations.None; @@ -143,6 +142,7 @@ internal override void GenerateMethodBody(TypeCompilationState compilationState, } else { + // PROTOTYPE: Shouldn't have to bind to "Equals". The caller has the actual method in hand. // For classes: // return this.Equals(param as ContainingType); expression = F.InstanceCall(F.This(), "Equals", F.As(paramAccess, ContainingType)); diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs index aa5319cfd9bdd..2d7852356df15 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs @@ -292,6 +292,8 @@ public void set_X() { } var expectedMembers = new[] { "C C.Clone()", + "System.Type C.EqualityContract.get", + "System.Type C.EqualityContract { get; }", "C..ctor(System.Int32 X, System.Int32 Y)", "System.Int32 C.k__BackingField", "System.Int32 C.X.get", @@ -305,9 +307,9 @@ public void set_X() { } "void C.set_X()", "System.Int32 C.get_Y(System.Int32 value)", "System.Int32 C.set_Y(System.Int32 value)", - "System.Boolean C.Equals(C? )", - "System.Boolean C.Equals(System.Object? )", "System.Int32 C.GetHashCode()", + "System.Boolean C.Equals(System.Object? )", + "System.Boolean C.Equals(C? )", "C..ctor(C )", }; AssertEx.Equal(expectedMembers, actualMembers); @@ -2483,6 +2485,7 @@ record B(object P1, object P2, object P3, object P4, object P5, object P6) : A var actualMembers = GetProperties(comp, "B").ToTestDisplayStrings(); var expectedMembers = new[] { + "System.Type B.EqualityContract { get; }", "System.Object B.P6 { get; init; }", }; AssertEx.Equal(expectedMembers, actualMembers); @@ -2505,7 +2508,7 @@ private record B(object P1, object P2) : A var comp = CreateCompilation(source); comp.VerifyDiagnostics(); var actualMembers = GetProperties(comp, "A.B").ToTestDisplayStrings(); - AssertEx.Equal(new string[0], actualMembers); + AssertEx.Equal(new[] { "System.Type A.B.EqualityContract { get; }" }, actualMembers); } [WorkItem(44616, "https://github.com/dotnet/roslyn/issues/44616")] @@ -2528,7 +2531,7 @@ record C1(object P, object Q) : B { }"; var comp = CreateCompilation(sourceA); - AssertEx.Equal(new string[0], GetProperties(comp, "C1").ToTestDisplayStrings()); + AssertEx.Equal(new[] { "System.Type C1.EqualityContract { get; }" }, GetProperties(comp, "C1").ToTestDisplayStrings()); var refA = useCompilationReference ? comp.ToMetadataReference() : comp.EmitToImageReference(); var sourceB = @@ -2537,7 +2540,7 @@ record C1(object P, object Q) : B }"; comp = CreateCompilation(sourceB, references: new[] { refA }, parseOptions: TestOptions.RegularPreview); comp.VerifyDiagnostics(); - AssertEx.Equal(new[] { "System.Object C2.P { get; init; }" }, GetProperties(comp, "C2").ToTestDisplayStrings()); + AssertEx.Equal(new[] { "System.Type C2.EqualityContract { get; }", "System.Object C2.P { get; init; }" }, GetProperties(comp, "C2").ToTestDisplayStrings()); } [WorkItem(44616, "https://github.com/dotnet/roslyn/issues/44616")] @@ -2562,7 +2565,7 @@ record B(object P1, object P2, object P3, object P4, object P5, object P6, objec var comp = CreateCompilation(source); comp.VerifyDiagnostics(); var actualMembers = GetProperties(comp, "B").ToTestDisplayStrings(); - AssertEx.Equal(new string[0], actualMembers); + AssertEx.Equal(new[] { "System.Type B.EqualityContract { get; }" }, actualMembers); } [Fact] @@ -2581,7 +2584,7 @@ record B(int P1, object P2) : A var comp = CreateCompilation(source); comp.VerifyDiagnostics(); var actualMembers = GetProperties(comp, "B").ToTestDisplayStrings(); - AssertEx.Equal(new string[0], actualMembers); + AssertEx.Equal(new[] { "System.Type B.EqualityContract { get; }" }, actualMembers); } [Fact] @@ -2613,7 +2616,7 @@ static void Main() var comp = CreateCompilation(source); comp.VerifyDiagnostics(); var actualMembers = GetProperties(comp, "B").ToTestDisplayStrings(); - AssertEx.Equal(new string[0], actualMembers); + AssertEx.Equal(new[] { "System.Type B.EqualityContract { get; }" }, actualMembers); } [WorkItem(44785, "https://github.com/dotnet/roslyn/issues/44785")] @@ -2638,8 +2641,8 @@ record B2(int X, int Y) : A // record B2(int X, int Y) : A Diagnostic(ErrorCode.ERR_UnimplementedAbstractMethod, "B2").WithArguments("B2", "A.X.get").WithLocation(9, 8)); - AssertEx.Equal(new string[0], GetProperties(comp, "B1").ToTestDisplayStrings()); - AssertEx.Equal(new string[0], GetProperties(comp, "B2").ToTestDisplayStrings()); + AssertEx.Equal(new[] { "System.Type B1.EqualityContract { get; }" }, GetProperties(comp, "B1").ToTestDisplayStrings()); + AssertEx.Equal(new[] { "System.Type B2.EqualityContract { get; }" }, GetProperties(comp, "B2").ToTestDisplayStrings()); } [WorkItem(44785, "https://github.com/dotnet/roslyn/issues/44785")] @@ -2670,7 +2673,7 @@ record C(int X, int Y, int Z) : B Diagnostic(ErrorCode.ERR_UnimplementedAbstractMethod, "C").WithArguments("C", "A.X.get").WithLocation(11, 8)); var actualMembers = GetProperties(comp, "C").ToTestDisplayStrings(); - AssertEx.Equal(new string[0], actualMembers); + AssertEx.Equal(new[] { "System.Type C.EqualityContract { get; }" }, actualMembers); } [Fact] @@ -2689,15 +2692,17 @@ public void Inheritance_09() var expectedMembers = new[] { "C C.Clone()", + "System.Type C.EqualityContract.get", + "System.Type C.EqualityContract { get; }", "C..ctor(System.Int32 X, System.Int32 Y)", "System.Int32 C.X { get; }", "System.Int32 C.X.get", "System.Int32 C.k__BackingField", "System.Int32 C.Y { get; }", "System.Int32 C.Y.get", - "System.Boolean C.Equals(C? )", - "System.Boolean C.Equals(System.Object? )", "System.Int32 C.GetHashCode()", + "System.Boolean C.Equals(System.Object? )", + "System.Boolean C.Equals(C? )", "C..ctor(C )", }; AssertEx.Equal(expectedMembers, actualMembers); @@ -2784,6 +2789,7 @@ record B(object X, object Y) : A var actualMembers = GetProperties(comp, "B").ToTestDisplayStrings(); var expectedMembers = new[] { + "System.Type B.EqualityContract { get; }", "System.Object B.X { get; }", "System.Object B.Y { get; }", }; @@ -2814,6 +2820,7 @@ record B(object X, object Y) : A var actualMembers = GetProperties(comp, "B").ToTestDisplayStrings(); var expectedMembers = new[] { + "System.Type B.EqualityContract { get; }", "System.Object B.X { get; }", "System.Object B.Y { get; }", }; @@ -2842,7 +2849,7 @@ record C(object P1, int P2, object P3, int P4) : B var comp = CreateCompilation(source); comp.VerifyDiagnostics(); var actualMembers = GetProperties(comp, "C").ToTestDisplayStrings(); - AssertEx.Equal(new string[0], actualMembers); + AssertEx.Equal(new[] { "System.Type C.EqualityContract { get; }" }, actualMembers); } [Fact] @@ -2859,6 +2866,7 @@ public void Inheritance_15() var actualMembers = GetProperties(comp, "C").ToTestDisplayStrings(); var expectedMembers = new[] { + "System.Type C.EqualityContract { get; }", "System.Object C.P1 { get; set; }", "System.Int32 C.P2 { get; set; }", }; @@ -2886,6 +2894,7 @@ record B(object P1, int P2, object P3, int P4) : A var actualMembers = GetProperties(comp, "B").ToTestDisplayStrings(); var expectedMembers = new[] { + "System.Type B.EqualityContract { get; }", "System.Object B.P1 { get; }", "System.Object B.P2 { get; }", }; @@ -2914,6 +2923,7 @@ record B(object P1, int P2, object P3, int P4) : A var actualMembers = GetProperties(comp, "B").ToTestDisplayStrings(); var expectedMembers = new[] { + "System.Type B.EqualityContract { get; }", "System.Int32 B.P1 { get; }", "System.Int32 B.P2 { get; }", }; @@ -2948,6 +2958,7 @@ public object P3 { set { } } var actualMembers = GetProperties(comp, "C").ToTestDisplayStrings(); var expectedMembers = new[] { + "System.Type C.EqualityContract { get; }", "System.Object C.P1 { get; set; }", "System.Object C.P2 { get; }", "System.Object C.P3 { set; }", @@ -2981,7 +2992,7 @@ record B(dynamic P1, object[] P2, object P3, object?[] P4, (int, int) P5, (int X var comp = CreateCompilation(source); comp.VerifyDiagnostics(); var actualMembers = GetProperties(comp, "B").ToTestDisplayStrings(); - AssertEx.Equal(new string[0], actualMembers); + AssertEx.Equal(new[] { "System.Type B.EqualityContract { get; }" }, actualMembers); } [Fact] @@ -3006,6 +3017,7 @@ record C(dynamic P1, object[] P2, object P3, object?[] P4, (int, int) P5, (int X var actualMembers = GetProperties(comp, "C").ToTestDisplayStrings(); var expectedMembers = new[] { + "System.Type C.EqualityContract { get; }", "System.Object C.P1 { get; }", "dynamic[] C.P2 { get; }", "System.Object? C.P3 { get; }", @@ -3053,7 +3065,7 @@ static void Main() comp.VerifyDiagnostics(); var actualMembers = GetProperties(comp, "C").ToTestDisplayStrings(); - AssertEx.Equal(new string[0], actualMembers); + AssertEx.Equal(new[] { "System.Type C.EqualityContract { get; }" }, actualMembers); var verifier = CompileAndVerify(comp, expectedOutput: "(, )"); verifier.VerifyIL("C..ctor(object, object)", @@ -3105,7 +3117,7 @@ record C(object P1, object P2) : B var comp = CreateCompilation(source); comp.VerifyDiagnostics(); var actualMembers = GetProperties(comp, "C").ToTestDisplayStrings(); - AssertEx.Equal(new string[0], actualMembers); + AssertEx.Equal(new[] { "System.Type C.EqualityContract { get; }" }, actualMembers); } [Fact] @@ -3128,7 +3140,7 @@ record C(object P1, object P2) : B var comp = CreateCompilation(source); comp.VerifyDiagnostics(); var actualMembers = GetProperties(comp, "C").ToTestDisplayStrings(); - AssertEx.Equal(new string[0], actualMembers); + AssertEx.Equal(new[] { "System.Type C.EqualityContract { get; }" }, actualMembers); } [Fact] @@ -3157,6 +3169,8 @@ record C(object P) var expectedMembers = new[] { "B B.Clone()", + "System.Type B.EqualityContract.get", + "System.Type B.EqualityContract { get; }", "B..ctor(System.Object P, System.Object Q)", "System.Object B.

k__BackingField", "System.Object B.P.get", @@ -3166,9 +3180,9 @@ record C(object P) "System.Object B.Q.get", "void modreq(System.Runtime.CompilerServices.IsExternalInit) B.Q.init", "System.Object B.Q { get; init; }", - "System.Boolean B.Equals(B? )", - "System.Boolean B.Equals(System.Object? )", "System.Int32 B.GetHashCode()", + "System.Boolean B.Equals(System.Object? )", + "System.Boolean B.Equals(B? )", "B..ctor(B )", }; AssertEx.Equal(expectedMembers, comp.GetMember("B").GetMembers().ToTestDisplayStrings()); @@ -3176,6 +3190,8 @@ record C(object P) expectedMembers = new[] { "C C.Clone()", + "System.Type C.EqualityContract.get", + "System.Type C.EqualityContract { get; }", "C..ctor(System.Object P)", "System.Object C.

k__BackingField", "System.Object C.P.get", @@ -3183,9 +3199,9 @@ record C(object P) "System.Object C.P { get; init; }", "System.Object C.get_P()", "System.Object C.set_Q()", - "System.Boolean C.Equals(C? )", - "System.Boolean C.Equals(System.Object? )", "System.Int32 C.GetHashCode()", + "System.Boolean C.Equals(System.Object? )", + "System.Boolean C.Equals(C? )", "C..ctor(C )", }; AssertEx.Equal(expectedMembers, comp.GetMember("C").GetMembers().ToTestDisplayStrings()); @@ -3211,6 +3227,7 @@ public class P1 { } var actualMembers = GetProperties(comp, "B").ToTestDisplayStrings(); var expectedMembers = new[] { + "System.Type B.EqualityContract { get; }", "System.Object B.P4 { get; init; }", }; AssertEx.Equal(expectedMembers, actualMembers); @@ -3222,6 +3239,7 @@ public class P1 { } actualMembers = GetProperties(comp, "B").ToTestDisplayStrings(); expectedMembers = new[] { + "System.Type B.EqualityContract { get; }", "System.Object B.P2 { get; init; }", "System.Object B.P4 { get; init; }", }; @@ -3242,13 +3260,13 @@ public void Inheritance_26() }"; var comp = CreateCompilation(new[] { sourceA, sourceB }); comp.VerifyDiagnostics(); - AssertEx.Equal(new string[0], GetProperties(comp, "B").ToTestDisplayStrings()); + AssertEx.Equal(new[] { "System.Type B.EqualityContract { get; }" }, GetProperties(comp, "B").ToTestDisplayStrings()); comp = CreateCompilation(sourceA); var refA = comp.EmitToImageReference(); comp = CreateCompilation(sourceB, references: new[] { refA }, parseOptions: TestOptions.RegularPreview); comp.VerifyDiagnostics(); - AssertEx.Equal(new[] { "System.Object B.P { get; init; }" }, GetProperties(comp, "B").ToTestDisplayStrings()); + AssertEx.Equal(new[] { "System.Type B.EqualityContract { get; }", "System.Object B.P { get; init; }" }, GetProperties(comp, "B").ToTestDisplayStrings()); } [Fact] @@ -3268,6 +3286,7 @@ record B(object get_P, object set_Q) : A var actualMembers = GetProperties(comp, "B").ToTestDisplayStrings(); var expectedMembers = new[] { + "System.Type B.EqualityContract { get; }", "System.Object B.get_P { get; init; }", "System.Object B.set_Q { get; init; }", }; @@ -3295,8 +3314,8 @@ record C(object P) : I }"; var comp = CreateCompilation(source); comp.VerifyDiagnostics(); - AssertEx.Equal(new[] { "System.Object B.P { get; init; }" }, GetProperties(comp, "B").ToTestDisplayStrings()); - AssertEx.Equal(new[] { "System.Object C.P { get; init; }", "System.Object C.I.P { get; }" }, GetProperties(comp, "C").ToTestDisplayStrings()); + AssertEx.Equal(new[] { "System.Type B.EqualityContract { get; }", "System.Object B.P { get; init; }" }, GetProperties(comp, "B").ToTestDisplayStrings()); + AssertEx.Equal(new[] { "System.Type C.EqualityContract { get; }", "System.Object C.P { get; init; }", "System.Object C.I.P { get; }" }, GetProperties(comp, "C").ToTestDisplayStrings()); } [Fact] @@ -3335,6 +3354,7 @@ End Class var actualMembers = GetProperties(compB, "B").ToTestDisplayStrings(); var expectedMembers = new[] { + "System.Type B.EqualityContract { get; }", "System.Object B.Q { get; init; }", "System.Object B.P { get; }", }; @@ -3384,7 +3404,7 @@ End Class compB.VerifyDiagnostics(); var actualMembers = GetProperties(compB, "B").ToTestDisplayStrings(); - AssertEx.Equal(new string[0], actualMembers); + AssertEx.Equal(new[] { "System.Type B.EqualityContract { get; }" }, actualMembers); } [Fact] @@ -3449,6 +3469,7 @@ End Class var actualMembers = GetProperties(compB, "C").ToTestDisplayStrings(); var expectedMembers = new[] { + "System.Type C.EqualityContract { get; }", "System.Object C.R { get; init; }", }; AssertEx.Equal(expectedMembers, actualMembers); @@ -3480,6 +3501,8 @@ record B(int X, int Y) : A var expectedMembers = new[] { "B B.Clone()", + "System.Type B.EqualityContract.get", + "System.Type B.EqualityContract { get; }", "B..ctor(System.Int32 X, System.Int32 Y)", "System.Int32 B.k__BackingField", "System.Int32 B.X.get", @@ -3489,9 +3512,9 @@ record B(int X, int Y) : A "System.Int32 B.Y.get", "void modreq(System.Runtime.CompilerServices.IsExternalInit) B.Y.init", "System.Int32 B.Y { get; init; }", - "System.Boolean B.Equals(B? )", - "System.Boolean B.Equals(System.Object? )", "System.Int32 B.GetHashCode()", + "System.Boolean B.Equals(System.Object? )", + "System.Boolean B.Equals(B? )", "B..ctor(B )", }; AssertEx.Equal(expectedMembers, actualMembers); @@ -3520,6 +3543,8 @@ record B(int X, int Y) : A var expectedMembers = new[] { "B B.Clone()", + "System.Type B.EqualityContract.get", + "System.Type B.EqualityContract { get; }", "B..ctor(System.Int32 X, System.Int32 Y)", "System.Int32 B.k__BackingField", "System.Int32 B.X.get", @@ -3529,9 +3554,9 @@ record B(int X, int Y) : A "System.Int32 B.Y.get", "void modreq(System.Runtime.CompilerServices.IsExternalInit) B.Y.init", "System.Int32 B.Y { get; init; }", - "System.Boolean B.Equals(B? )", - "System.Boolean B.Equals(System.Object? )", "System.Int32 B.GetHashCode()", + "System.Boolean B.Equals(System.Object? )", + "System.Boolean B.Equals(B? )", "B..ctor(B )", }; AssertEx.Equal(expectedMembers, actualMembers); @@ -3556,6 +3581,7 @@ public void DuplicateProperty_01() var actualMembers = GetProperties(comp, "C").ToTestDisplayStrings(); var expectedMembers = new[] { + "System.Type C.EqualityContract { get; }", "System.Object C.Q { get; init; }", "System.Object C.P { get; }", "System.Object C.P { get; }", @@ -3587,6 +3613,7 @@ public void DuplicateProperty_02() var actualMembers = GetProperties(comp, "C").ToTestDisplayStrings(); var expectedMembers = new[] { + "System.Type C.EqualityContract { get; }", "System.Object C.P { get; }", "System.Int32 C.P { get; }", "System.Int32 C.Q { get; }", @@ -3619,7 +3646,7 @@ record B(object Q) : A Diagnostic(ErrorCode.ERR_DuplicateNameInClass, "Q").WithArguments("A", "Q").WithLocation(6, 16)); var actualMembers = GetProperties(comp, "B").ToTestDisplayStrings(); - AssertEx.Equal(new string[0], actualMembers); + AssertEx.Equal(new[] { "System.Type B.EqualityContract { get; }" }, actualMembers); } [Fact] @@ -3655,5 +3682,888 @@ private static ImmutableArray GetProperties(CSharpCompilation comp, stri { return comp.GetMember(typeName).GetMembers().WhereAsArray(m => m.Kind == SymbolKind.Property); } + + [Fact(Skip = "record struct")] + public void Equality_01() + { + var source = +@"using static System.Console; +data struct S; +class Program +{ + static void Main() + { + var x = new S(); + var y = new S(); + WriteLine(x.Equals(y)); + WriteLine(((object)x).Equals(y)); + } +}"; + var comp = CreateCompilation(source, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); + comp.VerifyDiagnostics(); + var verifier = CompileAndVerify(comp, expectedOutput: +@"True +True"); + verifier.VerifyIL("S.Equals(in S)", +@"{ + // Code size 23 (0x17) + .maxstack 2 + .locals init (S V_0) + IL_0000: ldarg.0 + IL_0001: call ""System.Type S.EqualityContract.get"" + IL_0006: ldarg.1 + IL_0007: ldobj ""S"" + IL_000c: stloc.0 + IL_000d: ldloca.s V_0 + IL_000f: call ""System.Type S.EqualityContract.get"" + IL_0014: ceq + IL_0016: ret +}"); + verifier.VerifyIL("S.Equals(object)", +@"{ + // Code size 26 (0x1a) + .maxstack 2 + .locals init (S V_0) + IL_0000: ldarg.1 + IL_0001: isinst ""S"" + IL_0006: brtrue.s IL_000a + IL_0008: ldc.i4.0 + IL_0009: ret + IL_000a: ldarg.0 + IL_000b: ldarg.1 + IL_000c: unbox.any ""S"" + IL_0011: stloc.0 + IL_0012: ldloca.s V_0 + IL_0014: call ""bool S.Equals(in S)"" + IL_0019: ret +}"); + } + + [Fact] + public void Equality_02() + { + var source = +@"using static System.Console; +record C; +class Program +{ + static void Main() + { + var x = new C(); + var y = new C(); + WriteLine(x.Equals(y)); + WriteLine(((object)x).Equals(y)); + } +}"; + var comp = CreateCompilation(source, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); + comp.VerifyDiagnostics(); + var verifier = CompileAndVerify(comp, expectedOutput: +@"True +True"); + verifier.VerifyIL("C.Equals(C)", +@"{ + // Code size 20 (0x14) + .maxstack 2 + IL_0000: ldarg.1 + IL_0001: brfalse.s IL_0012 + IL_0003: ldarg.0 + IL_0004: callvirt ""System.Type C.EqualityContract.get"" + IL_0009: ldarg.1 + IL_000a: callvirt ""System.Type C.EqualityContract.get"" + IL_000f: ceq + IL_0011: ret + IL_0012: ldc.i4.0 + IL_0013: ret +}"); + verifier.VerifyIL("C.Equals(object)", +@"{ + // Code size 13 (0xd) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: isinst ""C"" + IL_0007: callvirt ""bool C.Equals(C)"" + IL_000c: ret +}"); + } + + [Fact] + public void Equality_03() + { + var source = +@"using static System.Console; +record C +{ + private static int _nextId = 0; + private int _id; + public C() { _id = _nextId++; } +} +class Program +{ + static void Main() + { + var x = new C(); + var y = new C(); + WriteLine(x.Equals(x)); + WriteLine(x.Equals(y)); + WriteLine(y.Equals(y)); + } +}"; + var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); + comp.VerifyDiagnostics( + // PROTOTYPE: Is this warning expected? Where is it from? + // (2,1): warning CS1717: Assignment made to same variable; did you mean to assign something else? + // record C + Diagnostic(ErrorCode.WRN_AssignmentToSelf, @"record C +{ + private static int _nextId = 0; + private int _id; + public C() { _id = _nextId++; } +}").WithLocation(2, 1)); + var verifier = CompileAndVerify(comp, expectedOutput: +@"True +False +True"); + verifier.VerifyIL("C.Equals(C)", +@"{ + // Code size 42 (0x2a) + .maxstack 3 + IL_0000: ldarg.1 + IL_0001: brfalse.s IL_0028 + IL_0003: ldarg.0 + IL_0004: callvirt ""System.Type C.EqualityContract.get"" + IL_0009: ldarg.1 + IL_000a: callvirt ""System.Type C.EqualityContract.get"" + IL_000f: bne.un.s IL_0028 + IL_0011: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_0016: ldarg.0 + IL_0017: ldfld ""int C._id"" + IL_001c: ldarg.1 + IL_001d: ldfld ""int C._id"" + IL_0022: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_0027: ret + IL_0028: ldc.i4.0 + IL_0029: ret +}"); + } + + [Fact] + public void Equality_04() + { + var source = +@"using static System.Console; +record A; +record B1(int P) : A +{ + internal B1() : this(0) { } // PROTOTYPE: Remove + internal int P { get; set; } // PROTOTYPE: Remove +} +record B2(int P) : A +{ + internal B2() : this(0) { } // PROTOTYPE: Remove + internal int P { get; set; } // PROTOTYPE: Remove +} +class Program +{ + static B1 NewB1(int p) => new B1 { P = p }; // PROTOTYPE: Replace with new B1(P) + static B2 NewB2(int p) => new B2 { P = p }; // PROTOTYPE: Replace with new B2(P) + static void Main() + { + WriteLine(new A().Equals(NewB1(1))); + WriteLine(NewB1(1).Equals(new A())); + WriteLine(NewB1(1).Equals(NewB2(1))); + WriteLine(new A().Equals((A)NewB2(1))); + WriteLine(((A)NewB2(1)).Equals(new A())); + WriteLine(((A)NewB2(1)).Equals(NewB2(1))); + WriteLine(NewB2(1).Equals((A)NewB2(1))); + } +}"; + var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); + comp.VerifyDiagnostics(); + var verifier = CompileAndVerify(comp, expectedOutput: +@"False +False +False +False +False +True +True"); + verifier.VerifyIL("A.Equals(A)", +@"{ + // Code size 20 (0x14) + .maxstack 2 + IL_0000: ldarg.1 + IL_0001: brfalse.s IL_0012 + IL_0003: ldarg.0 + IL_0004: callvirt ""System.Type A.EqualityContract.get"" + IL_0009: ldarg.1 + IL_000a: callvirt ""System.Type A.EqualityContract.get"" + IL_000f: ceq + IL_0011: ret + IL_0012: ldc.i4.0 + IL_0013: ret +}"); + verifier.VerifyIL("B1.Equals(B1)", +@"{ + // Code size 34 (0x22) + .maxstack 3 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: call ""bool A.Equals(A)"" + IL_0007: brfalse.s IL_0020 + IL_0009: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_000e: ldarg.0 + IL_000f: ldfld ""int B1.

k__BackingField"" + IL_0014: ldarg.1 + IL_0015: ldfld ""int B1.

k__BackingField"" + IL_001a: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_001f: ret + IL_0020: ldc.i4.0 + IL_0021: ret +}"); + } + + [Fact] + public void Equality_05() + { + var source = +@"using static System.Console; +record A(int P) +{ + internal A() : this(0) { } // PROTOTYPE: Remove + internal int P { get; set; } // PROTOTYPE: Remove +} +record B1(int P) : A +{ + internal B1() : this(0) { } // PROTOTYPE: Remove +} +record B2(int P) : A +{ + internal B2() : this(0) { } // PROTOTYPE: Remove +} +class Program +{ + static A NewA(int p) => new A { P = p }; // PROTOTYPE: Replace with new A(P) + static B1 NewB1(int p) => new B1 { P = p }; // PROTOTYPE: Replace with new B1(P) + static B2 NewB2(int p) => new B2 { P = p }; // PROTOTYPE: Replace with new B2(P) + static void Main() + { + WriteLine(NewA(1).Equals(NewB1(1))); + WriteLine(NewB1(1).Equals(NewA(1))); + WriteLine(NewB1(1).Equals(NewB2(1))); + WriteLine(NewA(1).Equals((A)NewB2(1))); + WriteLine(((A)NewB2(1)).Equals(NewA(1))); + WriteLine(((A)NewB2(1)).Equals(NewB2(1))); + WriteLine(NewB2(1).Equals((A)NewB2(1))); + } +}"; + var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); + comp.VerifyDiagnostics(); + var verifier = CompileAndVerify(comp, expectedOutput: +@"False +False +False +False +False +True +True"); + verifier.VerifyIL("A.Equals(A)", +@"{ + // Code size 42 (0x2a) + .maxstack 3 + IL_0000: ldarg.1 + IL_0001: brfalse.s IL_0028 + IL_0003: ldarg.0 + IL_0004: callvirt ""System.Type A.EqualityContract.get"" + IL_0009: ldarg.1 + IL_000a: callvirt ""System.Type A.EqualityContract.get"" + IL_000f: bne.un.s IL_0028 + IL_0011: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_0016: ldarg.0 + IL_0017: ldfld ""int A.

k__BackingField"" + IL_001c: ldarg.1 + IL_001d: ldfld ""int A.

k__BackingField"" + IL_0022: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_0027: ret + IL_0028: ldc.i4.0 + IL_0029: ret +}"); + verifier.VerifyIL("B1.Equals(B1)", +@"{ + // Code size 8 (0x8) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: call ""bool A.Equals(A)"" + IL_0007: ret +}"); + } + + [Fact] + public void Equality_06() + { + var source = +@"using static System.Console; +record A; +class B : A { } +record C : B; +class Program +{ + static void Main() + { + WriteLine(new A().Equals(new A())); + WriteLine(new A().Equals(new B())); + WriteLine(new A().Equals(new C())); + WriteLine(new B().Equals(new A())); + WriteLine(new B().Equals(new B())); + WriteLine(new B().Equals(new C())); + WriteLine(new C().Equals(new A())); + WriteLine(new C().Equals(new B())); + WriteLine(new C().Equals(new C())); + WriteLine(((A)new C()).Equals(new A())); + WriteLine(((A)new C()).Equals(new B())); + WriteLine(((A)new C()).Equals(new C())); + WriteLine(new C().Equals((A)new C())); + } +}"; + var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); + comp.VerifyDiagnostics(); + var verifier = CompileAndVerify(comp, expectedOutput: +@"True +True +False +True +True +False +False +False +True +False +False +True +True"); + verifier.VerifyIL("A.Equals(A)", +@"{ + // Code size 20 (0x14) + .maxstack 2 + IL_0000: ldarg.1 + IL_0001: brfalse.s IL_0012 + IL_0003: ldarg.0 + IL_0004: callvirt ""System.Type A.EqualityContract.get"" + IL_0009: ldarg.1 + IL_000a: callvirt ""System.Type A.EqualityContract.get"" + IL_000f: ceq + IL_0011: ret + IL_0012: ldc.i4.0 + IL_0013: ret +}"); + verifier.VerifyIL("C.Equals(A)", +@"{ + // Code size 13 (0xd) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: isinst ""C"" + IL_0007: callvirt ""bool C.Equals(C)"" + IL_000c: ret +}"); + verifier.VerifyIL("C.Equals(C)", +@"{ + // Code size 8 (0x8) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: call ""bool A.Equals(A)"" + IL_0007: ret +}"); + } + + [Fact] + public void Equality_07() + { + var source = +@"using static System.Console; +record A; +record B : A; +record C : B; +class Program +{ + static void Main() + { + WriteLine(new A().Equals(new A())); + WriteLine(new A().Equals(new B())); + WriteLine(new A().Equals(new C())); + WriteLine(new B().Equals(new A())); + WriteLine(new B().Equals(new B())); + WriteLine(new B().Equals(new C())); + WriteLine(new C().Equals(new A())); + WriteLine(new C().Equals(new B())); + WriteLine(new C().Equals(new C())); + WriteLine(((A)new B()).Equals(new A())); + WriteLine(((A)new B()).Equals(new B())); + WriteLine(((A)new B()).Equals(new C())); + WriteLine(((A)new C()).Equals(new A())); + WriteLine(((A)new C()).Equals(new B())); + WriteLine(((A)new C()).Equals(new C())); + WriteLine(((B)new C()).Equals(new A())); + WriteLine(((B)new C()).Equals(new B())); + WriteLine(((B)new C()).Equals(new C())); + WriteLine(new C().Equals((A)new C())); + } +}"; + var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); + comp.VerifyDiagnostics(); + var verifier = CompileAndVerify(comp, expectedOutput: +@"True +False +False +False +True +False +False +False +True +False +True +False +False +False +True +False +False +True +True"); + verifier.VerifyIL("A.Equals(A)", +@"{ + // Code size 20 (0x14) + .maxstack 2 + IL_0000: ldarg.1 + IL_0001: brfalse.s IL_0012 + IL_0003: ldarg.0 + IL_0004: callvirt ""System.Type A.EqualityContract.get"" + IL_0009: ldarg.1 + IL_000a: callvirt ""System.Type A.EqualityContract.get"" + IL_000f: ceq + IL_0011: ret + IL_0012: ldc.i4.0 + IL_0013: ret +}"); + verifier.VerifyIL("B.Equals(A)", +@"{ + // Code size 13 (0xd) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: isinst ""B"" + IL_0007: callvirt ""bool B.Equals(B)"" + IL_000c: ret +}"); + verifier.VerifyIL("C.Equals(A)", +@"{ + // Code size 13 (0xd) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: isinst ""C"" + IL_0007: callvirt ""bool C.Equals(C)"" + IL_000c: ret +}"); + verifier.VerifyIL("C.Equals(B)", +@"{ + // Code size 13 (0xd) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: isinst ""C"" + IL_0007: callvirt ""bool C.Equals(C)"" + IL_000c: ret +}"); + verifier.VerifyIL("C.Equals(C)", +@"{ + // Code size 8 (0x8) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: call ""bool B.Equals(B)"" + IL_0007: ret +}"); + } + + [Fact] + public void Equality_08() + { + var source = +@"using static System.Console; +record A(int X) +{ + internal A() : this(0) { } // PROTOTYPE: Remove + internal int X { get; set; } // PROTOTYPE: Remove +} +class B : A +{ + internal B() { } // PROTOTYPE: Remove + internal B(int X, int Y) : base(X) { this.Y = Y; } + internal int Y { get; set; } +} +record C(int X, int Y, int Z) : B +{ + internal C() : this(0, 0, 0) { } // PROTOTYPE: Remove + internal int Z { get; set; } // PROTOTYPE: Remove +} +class Program +{ + static A NewA(int x) => new A { X = x }; // PROTOTYPE: Replace with new A(X), etc. + static B NewB(int x, int y) => new B { X = x, Y = y }; + static C NewC(int x, int y, int z) => new C { X = x, Y = y, Z = z }; + static void Main() + { + WriteLine(NewA(1).Equals(NewA(1))); + WriteLine(NewA(1).Equals(NewB(1, 2))); + WriteLine(NewA(1).Equals(NewC(1, 2, 3))); + WriteLine(NewB(1, 2).Equals(NewA(1))); + WriteLine(NewB(1, 2).Equals(NewB(1, 2))); + WriteLine(NewB(1, 2).Equals(NewC(1, 2, 3))); + WriteLine(NewC(1, 2, 3).Equals(NewA(1))); + WriteLine(NewC(1, 2, 3).Equals(NewB(1, 2))); + WriteLine(NewC(1, 2, 3).Equals(NewC(1, 2, 3))); + WriteLine(NewC(1, 2, 3).Equals(NewC(4, 2, 3))); + WriteLine(NewC(1, 2, 3).Equals(NewC(1, 4, 3))); + WriteLine(NewC(1, 2, 3).Equals(NewC(1, 4, 4))); + WriteLine(((A)NewB(1, 2)).Equals(NewA(1))); + WriteLine(((A)NewB(1, 2)).Equals(NewB(1, 2))); + WriteLine(((A)NewB(1, 2)).Equals(NewC(1, 2, 3))); + WriteLine(((A)NewC(1, 2, 3)).Equals(NewA(1))); + WriteLine(((A)NewC(1, 2, 3)).Equals(NewB(1, 2))); + WriteLine(((A)NewC(1, 2, 3)).Equals(NewC(1, 2, 3))); + WriteLine(((B)NewC(1, 2, 3)).Equals(NewA(1))); + WriteLine(((B)NewC(1, 2, 3)).Equals(NewB(1, 2))); + WriteLine(((B)NewC(1, 2, 3)).Equals(NewC(1, 2, 3))); + WriteLine(NewC(1, 2, 3).Equals((A)NewC(1, 2, 3))); + } +}"; + var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); + comp.VerifyDiagnostics(); + // PROTOTYPE: Compare B.Y in C.Equals() - expectedOutput is incorrect. + var verifier = CompileAndVerify(comp, expectedOutput: +@"True +True +False +True +True +False +False +False +True +False +True +False +True +True +False +False +False +True +False +False +True +True"); + verifier.VerifyIL("A.Equals(A)", +@"{ + // Code size 42 (0x2a) + .maxstack 3 + IL_0000: ldarg.1 + IL_0001: brfalse.s IL_0028 + IL_0003: ldarg.0 + IL_0004: callvirt ""System.Type A.EqualityContract.get"" + IL_0009: ldarg.1 + IL_000a: callvirt ""System.Type A.EqualityContract.get"" + IL_000f: bne.un.s IL_0028 + IL_0011: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_0016: ldarg.0 + IL_0017: ldfld ""int A.k__BackingField"" + IL_001c: ldarg.1 + IL_001d: ldfld ""int A.k__BackingField"" + IL_0022: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_0027: ret + IL_0028: ldc.i4.0 + IL_0029: ret +}"); + verifier.VerifyIL("C.Equals(A)", +@"{ + // Code size 13 (0xd) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: isinst ""C"" + IL_0007: callvirt ""bool C.Equals(C)"" + IL_000c: ret +}"); + // PROTOTYPE: Compare B.Y in C.Equals(). + verifier.VerifyIL("C.Equals(C)", +@"{ + // Code size 34 (0x22) + .maxstack 3 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: call ""bool A.Equals(A)"" + IL_0007: brfalse.s IL_0020 + IL_0009: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_000e: ldarg.0 + IL_000f: ldfld ""int C.k__BackingField"" + IL_0014: ldarg.1 + IL_0015: ldfld ""int C.k__BackingField"" + IL_001a: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_001f: ret + IL_0020: ldc.i4.0 + IL_0021: ret +}"); + } + + [Fact] + public void Equality_09() + { + var source = +@"using static System.Console; +record A(int X) +{ + internal A() : this(0) { } // PROTOTYPE: Remove + internal int X { get; set; } // PROTOTYPE: Remove +} +record B(int X, int Y) : A +{ + internal B() : this(0, 0) { } // PROTOTYPE: Remove + internal int Y { get; set; } +} +record C(int X, int Y, int Z) : B +{ + internal C() : this(0, 0, 0) { } // PROTOTYPE: Remove + internal int Z { get; set; } // PROTOTYPE: Remove +} +class Program +{ + static A NewA(int x) => new A { X = x }; // PROTOTYPE: Replace with new A(X), etc. + static B NewB(int x, int y) => new B { X = x, Y = y }; + static C NewC(int x, int y, int z) => new C { X = x, Y = y, Z = z }; + static void Main() + { + WriteLine(NewA(1).Equals(NewA(1))); + WriteLine(NewA(1).Equals(NewB(1, 2))); + WriteLine(NewA(1).Equals(NewC(1, 2, 3))); + WriteLine(NewB(1, 2).Equals(NewA(1))); + WriteLine(NewB(1, 2).Equals(NewB(1, 2))); + WriteLine(NewB(1, 2).Equals(NewC(1, 2, 3))); + WriteLine(NewC(1, 2, 3).Equals(NewA(1))); + WriteLine(NewC(1, 2, 3).Equals(NewB(1, 2))); + WriteLine(NewC(1, 2, 3).Equals(NewC(1, 2, 3))); + WriteLine(NewC(1, 2, 3).Equals(NewC(4, 2, 3))); + WriteLine(NewC(1, 2, 3).Equals(NewC(1, 4, 3))); + WriteLine(NewC(1, 2, 3).Equals(NewC(1, 4, 4))); + WriteLine(((A)NewB(1, 2)).Equals(NewA(1))); + WriteLine(((A)NewB(1, 2)).Equals(NewB(1, 2))); + WriteLine(((A)NewB(1, 2)).Equals(NewC(1, 2, 3))); + WriteLine(((A)NewC(1, 2, 3)).Equals(NewA(1))); + WriteLine(((A)NewC(1, 2, 3)).Equals(NewB(1, 2))); + WriteLine(((A)NewC(1, 2, 3)).Equals(NewC(1, 2, 3))); + WriteLine(((B)NewC(1, 2, 3)).Equals(NewA(1))); + WriteLine(((B)NewC(1, 2, 3)).Equals(NewB(1, 2))); + WriteLine(((B)NewC(1, 2, 3)).Equals(NewC(1, 2, 3))); + WriteLine(NewC(1, 2, 3).Equals((A)NewC(1, 2, 3))); + } +}"; + var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); + comp.VerifyDiagnostics(); + var verifier = CompileAndVerify(comp, expectedOutput: +@"True +False +False +False +True +False +False +False +True +False +False +False +False +True +False +False +False +True +False +False +True +True"); + verifier.VerifyIL("A.Equals(A)", +@"{ + // Code size 42 (0x2a) + .maxstack 3 + IL_0000: ldarg.1 + IL_0001: brfalse.s IL_0028 + IL_0003: ldarg.0 + IL_0004: callvirt ""System.Type A.EqualityContract.get"" + IL_0009: ldarg.1 + IL_000a: callvirt ""System.Type A.EqualityContract.get"" + IL_000f: bne.un.s IL_0028 + IL_0011: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_0016: ldarg.0 + IL_0017: ldfld ""int A.k__BackingField"" + IL_001c: ldarg.1 + IL_001d: ldfld ""int A.k__BackingField"" + IL_0022: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_0027: ret + IL_0028: ldc.i4.0 + IL_0029: ret +}"); + verifier.VerifyIL("B.Equals(A)", +@"{ + // Code size 13 (0xd) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: isinst ""B"" + IL_0007: callvirt ""bool B.Equals(B)"" + IL_000c: ret +}"); + verifier.VerifyIL("B.Equals(B)", +@"{ + // Code size 34 (0x22) + .maxstack 3 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: call ""bool A.Equals(A)"" + IL_0007: brfalse.s IL_0020 + IL_0009: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_000e: ldarg.0 + IL_000f: ldfld ""int B.k__BackingField"" + IL_0014: ldarg.1 + IL_0015: ldfld ""int B.k__BackingField"" + IL_001a: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_001f: ret + IL_0020: ldc.i4.0 + IL_0021: ret +}"); + verifier.VerifyIL("C.Equals(A)", +@"{ + // Code size 13 (0xd) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: isinst ""C"" + IL_0007: callvirt ""bool C.Equals(C)"" + IL_000c: ret +}"); + verifier.VerifyIL("C.Equals(B)", +@"{ + // Code size 13 (0xd) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: isinst ""C"" + IL_0007: callvirt ""bool C.Equals(C)"" + IL_000c: ret +}"); + verifier.VerifyIL("C.Equals(C)", +@"{ + // Code size 34 (0x22) + .maxstack 3 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: call ""bool B.Equals(B)"" + IL_0007: brfalse.s IL_0020 + IL_0009: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_000e: ldarg.0 + IL_000f: ldfld ""int C.k__BackingField"" + IL_0014: ldarg.1 + IL_0015: ldfld ""int C.k__BackingField"" + IL_001a: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_001f: ret + IL_0020: ldc.i4.0 + IL_0021: ret +}"); + } + + [Fact] + public void Equality_10() + { + var source = +@"using static System.Console; +abstract class A +{ + internal virtual int P { get; set; } +} +abstract record B(int P, int Q) : A +{ + internal abstract int Q { get; set; } +} +class C1 : B +{ + internal override int Q { get; set; } +} +class C2 : B +{ + internal override int P { get; set; } + internal override int Q => 2; +} +class Program +{ + static void Main() + { + var x = new C1(1, 2); + var y = new C1(1, 3); + var z = new C2(1, 3); + WriteLine(x.Equals(x)); + WriteLine(y.Equals(y)); + WriteLine(z.Equals(z)); + WriteLine(x.Equals(y)); + WriteLine(y.Equals(z)); + } +}"; + var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); + comp.VerifyDiagnostics(); + var verifier = CompileAndVerify(comp, expectedOutput: +@"True +True +True +False +False"); + verifier.VerifyIL("C.Equals(C)", +@"{ + // Code size 52 (0x34) + .maxstack 3 + IL_0000: ldarg.1 + IL_0001: brfalse.s IL_0032 + IL_0003: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_0008: ldarg.0 + IL_0009: ldfld ""int C.

k__BackingField"" + IL_000e: ldarg.1 + IL_000f: ldfld ""int C.

k__BackingField"" + IL_0014: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_0019: brfalse.s IL_0032 + IL_001b: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_0020: ldarg.0 + IL_0021: ldfld ""object C.k__BackingField"" + IL_0026: ldarg.1 + IL_0027: ldfld ""object C.k__BackingField"" + IL_002c: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(object, object)"" + IL_0031: ret + IL_0032: ldc.i4.0 + IL_0033: ret +}"); + } + + [Fact] + public void Equality_00() + { + // PROTOTYPE: + // - Test record deriving from type with non-virtual or sealed EqualityContract. Should report an error. + // - Test record deriving from non-record where base does not implement Equals(Base). Derived should compare *accessible fields on Base*. + // - Test record deriving from non-record where base implements EqualityContract and Equals(Base). + // - Test non-record deriving from record where derived overrides EqualityContract but not Equals(Base). + // - Test multiple non-record derived types with a distinct EqualityContract value. Should not compare Equals. + // - Test multiple non-record derived types with a shared base EqualityContract value. Should compare Equals. + Assert.False(true); + } } } diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/Source/RecordTests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/Source/RecordTests.cs index ca577d822489b..7a9d498c69ed7 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/Source/RecordTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/Source/RecordTests.cs @@ -313,31 +313,36 @@ .maxstack 2 IL_0000: ldarg.0 IL_0001: ldarg.1 IL_0002: isinst ""C"" - IL_0007: call ""bool C.Equals(C)"" + IL_0007: callvirt ""bool C.Equals(C)"" IL_000c: ret }"); verifier.VerifyIL("C.Equals(C)", @" { - // Code size 52 (0x34) + // Code size 66 (0x42) .maxstack 3 IL_0000: ldarg.1 - IL_0001: brfalse.s IL_0032 - IL_0003: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" - IL_0008: ldarg.0 - IL_0009: ldfld ""int C.k__BackingField"" - IL_000e: ldarg.1 - IL_000f: ldfld ""int C.k__BackingField"" - IL_0014: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" - IL_0019: brfalse.s IL_0032 - IL_001b: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" - IL_0020: ldarg.0 - IL_0021: ldfld ""int C.k__BackingField"" - IL_0026: ldarg.1 - IL_0027: ldfld ""int C.k__BackingField"" - IL_002c: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" - IL_0031: ret - IL_0032: ldc.i4.0 - IL_0033: ret + IL_0001: brfalse.s IL_0040 + IL_0003: ldarg.0 + IL_0004: callvirt ""System.Type C.EqualityContract.get"" + IL_0009: ldarg.1 + IL_000a: callvirt ""System.Type C.EqualityContract.get"" + IL_000f: bne.un.s IL_0040 + IL_0011: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_0016: ldarg.0 + IL_0017: ldfld ""int C.k__BackingField"" + IL_001c: ldarg.1 + IL_001d: ldfld ""int C.k__BackingField"" + IL_0022: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_0027: brfalse.s IL_0040 + IL_0029: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_002e: ldarg.0 + IL_002f: ldfld ""int C.k__BackingField"" + IL_0034: ldarg.1 + IL_0035: ldfld ""int C.k__BackingField"" + IL_003a: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_003f: ret + IL_0040: ldc.i4.0 + IL_0041: ret }"); } @@ -406,33 +411,38 @@ public static void Main() True"); verifier.VerifyIL("C.Equals(C)", @" { - // Code size 76 (0x4c) + // Code size 90 (0x5a) .maxstack 3 IL_0000: ldarg.1 - IL_0001: brfalse.s IL_004a - IL_0003: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" - IL_0008: ldarg.0 - IL_0009: ldfld ""int C.k__BackingField"" - IL_000e: ldarg.1 - IL_000f: ldfld ""int C.k__BackingField"" - IL_0014: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" - IL_0019: brfalse.s IL_004a - IL_001b: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" - IL_0020: ldarg.0 - IL_0021: ldfld ""int C.k__BackingField"" - IL_0026: ldarg.1 - IL_0027: ldfld ""int C.k__BackingField"" - IL_002c: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" - IL_0031: brfalse.s IL_004a - IL_0033: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" - IL_0038: ldarg.0 - IL_0039: ldfld ""int C.Z"" - IL_003e: ldarg.1 - IL_003f: ldfld ""int C.Z"" - IL_0044: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" - IL_0049: ret - IL_004a: ldc.i4.0 - IL_004b: ret + IL_0001: brfalse.s IL_0058 + IL_0003: ldarg.0 + IL_0004: callvirt ""System.Type C.EqualityContract.get"" + IL_0009: ldarg.1 + IL_000a: callvirt ""System.Type C.EqualityContract.get"" + IL_000f: bne.un.s IL_0058 + IL_0011: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_0016: ldarg.0 + IL_0017: ldfld ""int C.k__BackingField"" + IL_001c: ldarg.1 + IL_001d: ldfld ""int C.k__BackingField"" + IL_0022: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_0027: brfalse.s IL_0058 + IL_0029: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_002e: ldarg.0 + IL_002f: ldfld ""int C.k__BackingField"" + IL_0034: ldarg.1 + IL_0035: ldfld ""int C.k__BackingField"" + IL_003a: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_003f: brfalse.s IL_0058 + IL_0041: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_0046: ldarg.0 + IL_0047: ldfld ""int C.Z"" + IL_004c: ldarg.1 + IL_004d: ldfld ""int C.Z"" + IL_0052: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_0057: ret + IL_0058: ldc.i4.0 + IL_0059: ret }"); } @@ -488,26 +498,31 @@ public static void Main() True"); verifier.VerifyIL("C.Equals(C)", @" { - // Code size 52 (0x34) + // Code size 66 (0x42) .maxstack 3 IL_0000: ldarg.1 - IL_0001: brfalse.s IL_0032 - IL_0003: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" - IL_0008: ldarg.0 - IL_0009: ldfld ""int C.k__BackingField"" - IL_000e: ldarg.1 - IL_000f: ldfld ""int C.k__BackingField"" - IL_0014: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" - IL_0019: brfalse.s IL_0032 - IL_001b: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" - IL_0020: ldarg.0 - IL_0021: ldfld ""int C.k__BackingField"" - IL_0026: ldarg.1 - IL_0027: ldfld ""int C.k__BackingField"" - IL_002c: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" - IL_0031: ret - IL_0032: ldc.i4.0 - IL_0033: ret + IL_0001: brfalse.s IL_0040 + IL_0003: ldarg.0 + IL_0004: callvirt ""System.Type C.EqualityContract.get"" + IL_0009: ldarg.1 + IL_000a: callvirt ""System.Type C.EqualityContract.get"" + IL_000f: bne.un.s IL_0040 + IL_0011: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_0016: ldarg.0 + IL_0017: ldfld ""int C.k__BackingField"" + IL_001c: ldarg.1 + IL_001d: ldfld ""int C.k__BackingField"" + IL_0022: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_0027: brfalse.s IL_0040 + IL_0029: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_002e: ldarg.0 + IL_002f: ldfld ""int C.k__BackingField"" + IL_0034: ldarg.1 + IL_0035: ldfld ""int C.k__BackingField"" + IL_003a: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_003f: ret + IL_0040: ldc.i4.0 + IL_0041: ret }"); } @@ -539,26 +554,31 @@ public static void Main() True"); verifier.VerifyIL("C.Equals(C)", @" { - // Code size 52 (0x34) + // Code size 66 (0x42) .maxstack 3 IL_0000: ldarg.1 - IL_0001: brfalse.s IL_0032 - IL_0003: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" - IL_0008: ldarg.0 - IL_0009: ldfld ""int C.k__BackingField"" - IL_000e: ldarg.1 - IL_000f: ldfld ""int C.k__BackingField"" - IL_0014: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" - IL_0019: brfalse.s IL_0032 - IL_001b: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" - IL_0020: ldarg.0 - IL_0021: ldfld ""int C.k__BackingField"" - IL_0026: ldarg.1 - IL_0027: ldfld ""int C.k__BackingField"" - IL_002c: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" - IL_0031: ret - IL_0032: ldc.i4.0 - IL_0033: ret + IL_0001: brfalse.s IL_0040 + IL_0003: ldarg.0 + IL_0004: callvirt ""System.Type C.EqualityContract.get"" + IL_0009: ldarg.1 + IL_000a: callvirt ""System.Type C.EqualityContract.get"" + IL_000f: bne.un.s IL_0040 + IL_0011: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_0016: ldarg.0 + IL_0017: ldfld ""int C.k__BackingField"" + IL_001c: ldarg.1 + IL_001d: ldfld ""int C.k__BackingField"" + IL_0022: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_0027: brfalse.s IL_0040 + IL_0029: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_002e: ldarg.0 + IL_002f: ldfld ""int C.k__BackingField"" + IL_0034: ldarg.1 + IL_0035: ldfld ""int C.k__BackingField"" + IL_003a: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_003f: ret + IL_0040: ldc.i4.0 + IL_0041: ret }"); } @@ -589,33 +609,38 @@ public static void Main() True"); verifier.VerifyIL("C.Equals(C)", @" { - // Code size 76 (0x4c) + // Code size 90 (0x5a) .maxstack 3 IL_0000: ldarg.1 - IL_0001: brfalse.s IL_004a - IL_0003: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" - IL_0008: ldarg.0 - IL_0009: ldfld ""int C.k__BackingField"" - IL_000e: ldarg.1 - IL_000f: ldfld ""int C.k__BackingField"" - IL_0014: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" - IL_0019: brfalse.s IL_004a - IL_001b: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" - IL_0020: ldarg.0 - IL_0021: ldfld ""int C.k__BackingField"" - IL_0026: ldarg.1 - IL_0027: ldfld ""int C.k__BackingField"" - IL_002c: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" - IL_0031: brfalse.s IL_004a - IL_0033: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" - IL_0038: ldarg.0 - IL_0039: ldfld ""System.Action C.E"" - IL_003e: ldarg.1 - IL_003f: ldfld ""System.Action C.E"" - IL_0044: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(System.Action, System.Action)"" - IL_0049: ret - IL_004a: ldc.i4.0 - IL_004b: ret + IL_0001: brfalse.s IL_0058 + IL_0003: ldarg.0 + IL_0004: callvirt ""System.Type C.EqualityContract.get"" + IL_0009: ldarg.1 + IL_000a: callvirt ""System.Type C.EqualityContract.get"" + IL_000f: bne.un.s IL_0058 + IL_0011: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_0016: ldarg.0 + IL_0017: ldfld ""int C.k__BackingField"" + IL_001c: ldarg.1 + IL_001d: ldfld ""int C.k__BackingField"" + IL_0022: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_0027: brfalse.s IL_0058 + IL_0029: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_002e: ldarg.0 + IL_002f: ldfld ""int C.k__BackingField"" + IL_0034: ldarg.1 + IL_0035: ldfld ""int C.k__BackingField"" + IL_003a: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_003f: brfalse.s IL_0058 + IL_0041: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_0046: ldarg.0 + IL_0047: ldfld ""System.Action C.E"" + IL_004c: ldarg.1 + IL_004d: ldfld ""System.Action C.E"" + IL_0052: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(System.Action, System.Action)"" + IL_0057: ret + IL_0058: ldc.i4.0 + IL_0059: ret }"); } @@ -894,38 +919,43 @@ .maxstack 2 IL_0000: ldarg.0 IL_0001: ldarg.1 IL_0002: isinst ""C"" - IL_0007: call ""bool C.Equals(C)"" + IL_0007: callvirt ""bool C.Equals(C)"" IL_000c: ret }"); verifier.VerifyIL("C.Equals(C)", @" { - // Code size 76 (0x4c) + // Code size 90 (0x5a) .maxstack 3 IL_0000: ldarg.1 - IL_0001: brfalse.s IL_004a - IL_0003: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" - IL_0008: ldarg.0 - IL_0009: ldfld ""int C.X"" - IL_000e: ldarg.1 - IL_000f: ldfld ""int C.X"" - IL_0014: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" - IL_0019: brfalse.s IL_004a - IL_001b: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" - IL_0020: ldarg.0 - IL_0021: ldfld ""int C.k__BackingField"" - IL_0026: ldarg.1 - IL_0027: ldfld ""int C.k__BackingField"" - IL_002c: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" - IL_0031: brfalse.s IL_004a - IL_0033: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" - IL_0038: ldarg.0 - IL_0039: ldfld ""System.Action C.E"" - IL_003e: ldarg.1 - IL_003f: ldfld ""System.Action C.E"" - IL_0044: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(System.Action, System.Action)"" - IL_0049: ret - IL_004a: ldc.i4.0 - IL_004b: ret + IL_0001: brfalse.s IL_0058 + IL_0003: ldarg.0 + IL_0004: callvirt ""System.Type C.EqualityContract.get"" + IL_0009: ldarg.1 + IL_000a: callvirt ""System.Type C.EqualityContract.get"" + IL_000f: bne.un.s IL_0058 + IL_0011: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_0016: ldarg.0 + IL_0017: ldfld ""int C.X"" + IL_001c: ldarg.1 + IL_001d: ldfld ""int C.X"" + IL_0022: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_0027: brfalse.s IL_0058 + IL_0029: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_002e: ldarg.0 + IL_002f: ldfld ""int C.k__BackingField"" + IL_0034: ldarg.1 + IL_0035: ldfld ""int C.k__BackingField"" + IL_003a: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_003f: brfalse.s IL_0058 + IL_0041: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_0046: ldarg.0 + IL_0047: ldfld ""System.Action C.E"" + IL_004c: ldarg.1 + IL_004d: ldfld ""System.Action C.E"" + IL_0052: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(System.Action, System.Action)"" + IL_0057: ret + IL_0058: ldc.i4.0 + IL_0059: ret }"); } @@ -964,6 +994,8 @@ record C var members = comp.GlobalNamespace.GetTypeMember("C").GetMembers(); AssertEx.Equal(new[] { "C! C.Clone()", + "System.Type! C.EqualityContract.get", + "System.Type! C.EqualityContract { get; }", "System.Int32 C.k__BackingField", "System.Int32 C.X { get; init; }", "System.Int32 C.X.get", @@ -972,9 +1004,9 @@ record C "System.String! C.Y { get; init; }", "System.String! C.Y.get", "void C.Y.init", - "System.Boolean C.Equals(C? )", - "System.Boolean C.Equals(System.Object? )", "System.Int32 C.GetHashCode()", + "System.Boolean C.Equals(System.Object? )", + "System.Boolean C.Equals(C? )", "C.C(C! )", "C.C()", }, members.Select(m => m.ToTestDisplayString(includeNonNullable: true))); From 7974b1bb96a96f98efeceb70ac870039164b801f Mon Sep 17 00:00:00 2001 From: Charles Stoner Date: Fri, 5 Jun 2020 09:05:15 -0700 Subject: [PATCH 2/7] Fix tests --- .../Semantic/Semantics/InitOnlyMemberTests.cs | 10 ++- .../Semantic/Semantics/LookupPositionTests.cs | 5 +- .../Test/Semantic/Semantics/RecordTests.cs | 88 +++++++++---------- 3 files changed, 49 insertions(+), 54 deletions(-) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InitOnlyMemberTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InitOnlyMemberTests.cs index fe6ad4080bc03..cd751c733b856 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InitOnlyMemberTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InitOnlyMemberTests.cs @@ -2173,16 +2173,18 @@ void M() { } var cMembers = comp.GlobalNamespace.GetMember("C").GetMembers(); AssertEx.SetEqual(new[] { "C C.Clone()", + "System.Type C.EqualityContract.get", + "System.Type C.EqualityContract { get; }", + "C..ctor(System.Int32 i)", "System.Int32 C.k__BackingField", "System.Int32 C.i.get", "void modreq(System.Runtime.CompilerServices.IsExternalInit) C.i.init", "System.Int32 C.i { get; init; }", "void C.M()", - "System.Boolean C.Equals(C? )", - "System.Boolean C.Equals(System.Object? )", "System.Int32 C.GetHashCode()", - "C..ctor(C )", - "C..ctor(System.Int32 i)" }, cMembers.ToTestDisplayStrings()); + "System.Boolean C.Equals(System.Object? )", + "System.Boolean C.Equals(C? )", + "C..ctor(C )" }, cMembers.ToTestDisplayStrings()); foreach (var member in cMembers) { diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/LookupPositionTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/LookupPositionTests.cs index 7c5bbfdb76323..065baad83dd50 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/LookupPositionTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/LookupPositionTests.cs @@ -6,10 +6,8 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Test.Extensions; -using Microsoft.CodeAnalysis.Text; using Roslyn.Test.Utilities; using Xunit; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; @@ -49,6 +47,7 @@ record C(int x, int y) "System.Int32 System.Object.GetHashCode()", "System.Object System.Object.MemberwiseClone()", "System.String System.Object.ToString()", + "System.Type C.EqualityContract { get; }", "System.Type System.Object.GetType()", "void System.Object.Finalize()"), s_pop @@ -83,6 +82,7 @@ public void PositionalRecord2() "System.Object System.Object.MemberwiseClone()", "void System.Object.Finalize()", "System.String System.Object.ToString()", + "System.Type C.EqualityContract { get; }", "System.Type System.Object.GetType()"), s_pop ); @@ -113,6 +113,7 @@ public void NominalRecord() "System.Object System.Object.MemberwiseClone()", "void System.Object.Finalize()", "System.String System.Object.ToString()", + "System.Type C.EqualityContract { get; }", "System.Type System.Object.GetType()", }; var expectedNames = MakeExpectedSymbols( diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs index 2d7852356df15..a8d17da987a05 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs @@ -3810,8 +3810,8 @@ static void Main() } }"; var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); + // https://github.com/dotnet/roslyn/issues/44879: Copy constructor copies static field. comp.VerifyDiagnostics( - // PROTOTYPE: Is this warning expected? Where is it from? // (2,1): warning CS1717: Assignment made to same variable; did you mean to assign something else? // record C Diagnostic(ErrorCode.WRN_AssignmentToSelf, @"record C @@ -4492,78 +4492,70 @@ public void Equality_10() abstract class A { internal virtual int P { get; set; } + internal abstract int Q { get; set; } } -abstract record B(int P, int Q) : A +record B(int P, int Q) : A { - internal abstract int Q { get; set; } + internal B() : this(0, 0) { } // PROTOTYPE: Remove + internal override int Q { get; set; } } class C1 : B { - internal override int Q { get; set; } + internal C1(int p, int q) { P = p; Q = q; } + internal override int P { get; set; } } class C2 : B { - internal override int P { get; set; } - internal override int Q => 2; + internal C2(int p, int q) { P = p; Q = q; } + internal override int Q { get; set; } } class Program { static void Main() { - var x = new C1(1, 2); - var y = new C1(1, 3); - var z = new C2(1, 3); - WriteLine(x.Equals(x)); - WriteLine(y.Equals(y)); - WriteLine(z.Equals(z)); - WriteLine(x.Equals(y)); - WriteLine(y.Equals(z)); + WriteLine(new C1(1, 0).Equals(new C2(0, 0))); + WriteLine(new C1(0, 2).Equals(new C2(0, 0))); + WriteLine(new C1(0, 0).Equals(new C2(3, 0))); + WriteLine(new C1(0, 0).Equals(new C2(0, 4))); } }"; var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); comp.VerifyDiagnostics(); + // PROTOTYPE: Compare A.P in B.Equals() - expectedOutput is incorrect - new C1(0, 0).Equals(new C2(3, 0)) should be False. var verifier = CompileAndVerify(comp, expectedOutput: @"True -True -True False -False"); - verifier.VerifyIL("C.Equals(C)", +True +True"); + verifier.VerifyIL("B.Equals(B)", @"{ - // Code size 52 (0x34) + // Code size 42 (0x2a) .maxstack 3 IL_0000: ldarg.1 - IL_0001: brfalse.s IL_0032 - IL_0003: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" - IL_0008: ldarg.0 - IL_0009: ldfld ""int C.

k__BackingField"" - IL_000e: ldarg.1 - IL_000f: ldfld ""int C.

k__BackingField"" - IL_0014: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" - IL_0019: brfalse.s IL_0032 - IL_001b: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" - IL_0020: ldarg.0 - IL_0021: ldfld ""object C.k__BackingField"" - IL_0026: ldarg.1 - IL_0027: ldfld ""object C.k__BackingField"" - IL_002c: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(object, object)"" - IL_0031: ret - IL_0032: ldc.i4.0 - IL_0033: ret + IL_0001: brfalse.s IL_0028 + IL_0003: ldarg.0 + IL_0004: callvirt ""System.Type B.EqualityContract.get"" + IL_0009: ldarg.1 + IL_000a: callvirt ""System.Type B.EqualityContract.get"" + IL_000f: bne.un.s IL_0028 + IL_0011: call ""System.Collections.Generic.EqualityComparer System.Collections.Generic.EqualityComparer.Default.get"" + IL_0016: ldarg.0 + IL_0017: ldfld ""int B.k__BackingField"" + IL_001c: ldarg.1 + IL_001d: ldfld ""int B.k__BackingField"" + IL_0022: callvirt ""bool System.Collections.Generic.EqualityComparer.Equals(int, int)"" + IL_0027: ret + IL_0028: ldc.i4.0 + IL_0029: ret }"); } - [Fact] - public void Equality_00() - { - // PROTOTYPE: - // - Test record deriving from type with non-virtual or sealed EqualityContract. Should report an error. - // - Test record deriving from non-record where base does not implement Equals(Base). Derived should compare *accessible fields on Base*. - // - Test record deriving from non-record where base implements EqualityContract and Equals(Base). - // - Test non-record deriving from record where derived overrides EqualityContract but not Equals(Base). - // - Test multiple non-record derived types with a distinct EqualityContract value. Should not compare Equals. - // - Test multiple non-record derived types with a shared base EqualityContract value. Should compare Equals. - Assert.False(true); - } + // PROTOTYPE: + // - Test record deriving from type with non-virtual or sealed EqualityContract. Should report an error. + // - Test record deriving from non-record where base does not implement Equals(Base). Derived should compare *accessible fields on Base*. + // - Test record deriving from non-record where base implements EqualityContract and Equals(Base). + // - Test non-record deriving from record where derived overrides EqualityContract but not Equals(Base). + // - Test multiple non-record derived types with a distinct EqualityContract value. Should not compare Equals. + // - Test multiple non-record derived types with a shared base EqualityContract value. Should compare Equals. } } From 64de377ff43ab66750262e87ed2175782589e4e2 Mon Sep 17 00:00:00 2001 From: Charles Stoner Date: Fri, 5 Jun 2020 11:49:50 -0700 Subject: [PATCH 3/7] Address some PROTOTYPE comments --- .../Records/SynthesizedRecordEquals.cs | 2 +- .../Test/Semantic/Semantics/RecordTests.cs | 124 +++++++++++++++++- 2 files changed, 122 insertions(+), 4 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEquals.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEquals.cs index 3e84c821b19ab..c2da9e9541342 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEquals.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEquals.cs @@ -232,7 +232,7 @@ internal override void GenerateMethodBody(TypeCompilationState compilationState, } // field1 == other.field1 && ... && fieldN == other.fieldN - // PROTOTYPE: Should compare accessible fields from all non-record base types as well. + // https://github.com/dotnet/roslyn/issues/44895: Should compare fields from non-record base classes. var fields = ArrayBuilder.GetInstance(); foreach (var f in ContainingType.GetFieldsToEmit()) { diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs index a8d17da987a05..06e02fd5106cd 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs @@ -4189,6 +4189,7 @@ .maxstack 2 }"); } + [WorkItem(44895, "https://github.com/dotnet/roslyn/issues/44895")] [Fact] public void Equality_08() { @@ -4243,7 +4244,7 @@ static void Main() }"; var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); comp.VerifyDiagnostics(); - // PROTOTYPE: Compare B.Y in C.Equals() - expectedOutput is incorrect. + // https://github.com/dotnet/roslyn/issues/44895: C.Equals() should compare B.Y. var verifier = CompileAndVerify(comp, expectedOutput: @"True True @@ -4484,6 +4485,7 @@ .maxstack 3 }"); } + [WorkItem(44895, "https://github.com/dotnet/roslyn/issues/44895")] [Fact] public void Equality_10() { @@ -4521,7 +4523,7 @@ static void Main() }"; var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); comp.VerifyDiagnostics(); - // PROTOTYPE: Compare A.P in B.Equals() - expectedOutput is incorrect - new C1(0, 0).Equals(new C2(3, 0)) should be False. + // https://github.com/dotnet/roslyn/issues/44895: B.Equals() should compare A.P and report False for new C1(0, 0).Equals(new C2(3, 0)). var verifier = CompileAndVerify(comp, expectedOutput: @"True False @@ -4550,8 +4552,124 @@ .maxstack 3 }"); } + [Fact] + public void Equality_11() + { + var source = +@"using System; +record A +{ + public virtual Type EqualityContract => typeof(object); +} +record B1(object P) : A; +record B2(object P) : A; +class Program +{ + static void Main() + { + Console.WriteLine(new A().Equals(new A())); + Console.WriteLine(new A().Equals(new B1((object)null))); + Console.WriteLine(new B1((object)null).Equals(new A())); + Console.WriteLine(new B1((object)null).Equals(new B1((object)null))); + Console.WriteLine(new B1((object)null).Equals(new B2((object)null))); + } +}"; + var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); + comp.VerifyDiagnostics( + // (2,8): error CS0102: The type 'A' already contains a definition for 'EqualityContract' + // record A + Diagnostic(ErrorCode.ERR_DuplicateNameInClass, "A").WithArguments("A", "EqualityContract").WithLocation(2, 8)); + // If error above is no longer reported, should verify execution: +// CompileAndVerify(comp, expectedOutput: +//@"True +//False +//False +//True +//False"); + } + + [Fact] + public void Equality_12() + { + var source = +@"using System; +abstract record A +{ + public A() { } + public abstract Type EqualityContract { get; } +} +record B1(object P) : A; +record B2(object P) : A; +class Program +{ + static void Main() + { + var b1 = new B1((object)null); + var b2 = new B2((object)null); + Console.WriteLine(b1.Equals(b1)); + Console.WriteLine(b1.Equals(b2)); + Console.WriteLine(((A)b1).Equals(b1)); + Console.WriteLine(((A)b1).Equals(b2)); + } +}"; + var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); + comp.VerifyDiagnostics( + // (2,17): error CS0102: The type 'A' already contains a definition for 'EqualityContract' + // abstract record A + Diagnostic(ErrorCode.ERR_DuplicateNameInClass, "A").WithArguments("A", "EqualityContract").WithLocation(2, 17)); + // If error above is no longer reported, should verify execution: +// CompileAndVerify(comp, expectedOutput: +//@"True +//False +//True +//False"); + } + + [Fact] + public void Equality_13() + { + var source = +@"record A +{ + public System.Type EqualityContract => typeof(A); +} +record B : A; +"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (1,8): error CS0102: The type 'A' already contains a definition for 'EqualityContract' + // record A + Diagnostic(ErrorCode.ERR_DuplicateNameInClass, "A").WithArguments("A", "EqualityContract").WithLocation(1, 8), + // (5,8): error CS0506: 'B.EqualityContract': cannot override inherited member 'A.EqualityContract' because it is not marked virtual, abstract, or override + // record B : A; + Diagnostic(ErrorCode.ERR_CantOverrideNonVirtual, "B").WithArguments("B.EqualityContract", "A.EqualityContract").WithLocation(5, 8)); + } + + [Fact] + public void Equality_14() + { + var source = +@"record A; +record B +{ + public sealed override System.Type EqualityContract => typeof(B); +} +record C : B; +"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (2,8): error CS0102: The type 'B' already contains a definition for 'EqualityContract' + // record B + Diagnostic(ErrorCode.ERR_DuplicateNameInClass, "B").WithArguments("B", "EqualityContract").WithLocation(2, 8), + // (4,40): error CS0115: 'B.EqualityContract': no suitable method found to override + // public sealed override System.Type EqualityContract => typeof(B); + Diagnostic(ErrorCode.ERR_OverrideNotExpected, "EqualityContract").WithArguments("B.EqualityContract").WithLocation(4, 40), + // (6,8): error CS0239: 'C.EqualityContract': cannot override inherited member 'B.EqualityContract' because it is sealed + // record C : B; + Diagnostic(ErrorCode.ERR_CantOverrideSealed, "C").WithArguments("C.EqualityContract", "B.EqualityContract").WithLocation(6, 8)); + } + // PROTOTYPE: - // - Test record deriving from type with non-virtual or sealed EqualityContract. Should report an error. // - Test record deriving from non-record where base does not implement Equals(Base). Derived should compare *accessible fields on Base*. // - Test record deriving from non-record where base implements EqualityContract and Equals(Base). // - Test non-record deriving from record where derived overrides EqualityContract but not Equals(Base). From 42f1b095409f948bbcd4eb96edda0953b979a13d Mon Sep 17 00:00:00 2001 From: Charles Stoner Date: Fri, 5 Jun 2020 14:26:05 -0700 Subject: [PATCH 4/7] More PROTOTYPE comments --- .../Source/SourceMemberContainerSymbol.cs | 33 ++-- .../Test/Semantic/Semantics/RecordTests.cs | 161 +++++++++++------- 2 files changed, 126 insertions(+), 68 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs index 09d1432ea534b..075c8fd004b78 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs @@ -2992,7 +2992,7 @@ private void AddSynthesizedRecordMembersIfNecessary(MembersAndInitializersBuilde addCloneMethod(); var equalityContract = addEqualityContract(); - var otherEqualsMethods = ArrayBuilder.GetInstance(); // PROTOTYPE: We don't need to hold onto the other Equals methods. The values aren't used. + var otherEqualsMethods = ArrayBuilder.GetInstance(); getOtherEquals(otherEqualsMethods, equalityContract); var thisEquals = addThisEquals(equalityContract, otherEqualsMethods.Count == 0 ? null : otherEqualsMethods[0]); @@ -3114,19 +3114,30 @@ void addHashCode() } } + static PropertySymbol? getInheritedEqualityContract(NamedTypeSymbol type) + { + while ((type = type.BaseTypeNoUseSiteDiagnostics) is object) + { + var members = type.GetMembers(SynthesizedRecordEqualityContractProperty.PropertyName); + // PROTOTYPE: This ignores accessibility and instance/static. + if (members.FirstOrDefault(m => m is PropertySymbol property && property.ParameterCount == 0) is PropertySymbol property) + { + return property; + } + } + return null; + } + PropertySymbol addEqualityContract() { - var property = new SynthesizedRecordEqualityContractProperty(this, isOverride: false); - // PROTOTYPE: Test with explicit EqualityContract property from source. + var property = new SynthesizedRecordEqualityContractProperty(this, isOverride: getInheritedEqualityContract(this) is object); // PROTOTYPE: Handle inherited member of unexpected member kind or unexpected // property signature (distinct type, not virtual, sealed, etc.) - if (getInheritedMember(property, this) is PropertySymbol inheritedProperty) + if (!memberSignatures.ContainsKey(property)) { - // PROTOTYPE: Should not have to re-create property! - property = new SynthesizedRecordEqualityContractProperty(this, isOverride: true); + members.Add(property); + members.Add(property.GetMethod); } - members.Add(property); - members.Add(property.GetMethod); return property; } @@ -3145,13 +3156,13 @@ static void getOtherEquals(ArrayBuilder otherEqualsMethods, Proper { while ((equalityContract = equalityContract.OverriddenProperty) is object) { - var containingType = equalityContract.ContainingType; - var member = containingType.GetMembers("Equals").FirstOrDefault(m => + var member = equalityContract.ContainingType.GetMembers("Equals").FirstOrDefault(m => { + // PROTOTYPE: This ignores accessibility and instance/static. if (m is MethodSymbol method) { var parameters = method.Parameters; - if (parameters.Length == 1 && parameters[0].Type.Equals(containingType, TypeCompareKind.AllIgnoreOptions)) + if (parameters.Length == 1 && parameters[0].Type.Equals(m.ContainingType, TypeCompareKind.AllIgnoreOptions)) { return true; } diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs index 06e02fd5106cd..df8638b1816f2 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs @@ -3855,13 +3855,13 @@ public void Equality_04() record A; record B1(int P) : A { - internal B1() : this(0) { } // PROTOTYPE: Remove - internal int P { get; set; } // PROTOTYPE: Remove + internal B1() : this(0) { } // Use record base call syntax instead + internal int P { get; set; } // Use record base call syntax instead } record B2(int P) : A { - internal B2() : this(0) { } // PROTOTYPE: Remove - internal int P { get; set; } // PROTOTYPE: Remove + internal B2() : this(0) { } // Use record base call syntax instead + internal int P { get; set; } // Use record base call syntax instead } class Program { @@ -3930,16 +3930,16 @@ public void Equality_05() @"using static System.Console; record A(int P) { - internal A() : this(0) { } // PROTOTYPE: Remove - internal int P { get; set; } // PROTOTYPE: Remove + internal A() : this(0) { } // Use record base call syntax instead + internal int P { get; set; } // Use record base call syntax instead } record B1(int P) : A { - internal B1() : this(0) { } // PROTOTYPE: Remove + internal B1() : this(0) { } // Use record base call syntax instead } record B2(int P) : A { - internal B2() : this(0) { } // PROTOTYPE: Remove + internal B2() : this(0) { } // Use record base call syntax instead } class Program { @@ -4197,19 +4197,19 @@ public void Equality_08() @"using static System.Console; record A(int X) { - internal A() : this(0) { } // PROTOTYPE: Remove - internal int X { get; set; } // PROTOTYPE: Remove + internal A() : this(0) { } // Use record base call syntax instead + internal int X { get; set; } // Use record base call syntax instead } class B : A { - internal B() { } // PROTOTYPE: Remove + internal B() { } // Use record base call syntax instead internal B(int X, int Y) : base(X) { this.Y = Y; } internal int Y { get; set; } } record C(int X, int Y, int Z) : B { - internal C() : this(0, 0, 0) { } // PROTOTYPE: Remove - internal int Z { get; set; } // PROTOTYPE: Remove + internal C() : this(0, 0, 0) { } // Use record base call syntax instead + internal int Z { get; set; } // Use record base call syntax instead } class Program { @@ -4299,7 +4299,7 @@ .maxstack 2 IL_0007: callvirt ""bool C.Equals(C)"" IL_000c: ret }"); - // PROTOTYPE: Compare B.Y in C.Equals(). + // https://github.com/dotnet/roslyn/issues/44895: C.Equals() should compare B.Y. verifier.VerifyIL("C.Equals(C)", @"{ // Code size 34 (0x22) @@ -4327,18 +4327,18 @@ public void Equality_09() @"using static System.Console; record A(int X) { - internal A() : this(0) { } // PROTOTYPE: Remove - internal int X { get; set; } // PROTOTYPE: Remove + internal A() : this(0) { } // Use record base call syntax instead + internal int X { get; set; } // Use record base call syntax instead } record B(int X, int Y) : A { - internal B() : this(0, 0) { } // PROTOTYPE: Remove + internal B() : this(0, 0) { } // Use record base call syntax instead internal int Y { get; set; } } record C(int X, int Y, int Z) : B { - internal C() : this(0, 0, 0) { } // PROTOTYPE: Remove - internal int Z { get; set; } // PROTOTYPE: Remove + internal C() : this(0, 0, 0) { } // Use record base call syntax instead + internal int Z { get; set; } // Use record base call syntax instead } class Program { @@ -4498,7 +4498,7 @@ abstract class A } record B(int P, int Q) : A { - internal B() : this(0, 0) { } // PROTOTYPE: Remove + internal B() : this(0, 0) { } // Use record base call syntax instead internal override int Q { get; set; } } class C1 : B @@ -4575,17 +4575,14 @@ static void Main() } }"; var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); - comp.VerifyDiagnostics( - // (2,8): error CS0102: The type 'A' already contains a definition for 'EqualityContract' - // record A - Diagnostic(ErrorCode.ERR_DuplicateNameInClass, "A").WithArguments("A", "EqualityContract").WithLocation(2, 8)); - // If error above is no longer reported, should verify execution: -// CompileAndVerify(comp, expectedOutput: -//@"True -//False -//False -//True -//False"); + comp.VerifyDiagnostics(); + // init-only is unverifiable + CompileAndVerify(comp, verify: Verification.Skipped, expectedOutput: +@"True +False +False +True +False"); } [Fact] @@ -4613,16 +4610,13 @@ static void Main() } }"; var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); - comp.VerifyDiagnostics( - // (2,17): error CS0102: The type 'A' already contains a definition for 'EqualityContract' - // abstract record A - Diagnostic(ErrorCode.ERR_DuplicateNameInClass, "A").WithArguments("A", "EqualityContract").WithLocation(2, 17)); - // If error above is no longer reported, should verify execution: -// CompileAndVerify(comp, expectedOutput: -//@"True -//False -//True -//False"); + comp.VerifyDiagnostics(); + // init-only is unverifiable + CompileAndVerify(comp, verify: Verification.Skipped, expectedOutput: +@"True +False +True +False"); } [Fact] @@ -4637,9 +4631,6 @@ record B : A; "; var comp = CreateCompilation(source); comp.VerifyDiagnostics( - // (1,8): error CS0102: The type 'A' already contains a definition for 'EqualityContract' - // record A - Diagnostic(ErrorCode.ERR_DuplicateNameInClass, "A").WithArguments("A", "EqualityContract").WithLocation(1, 8), // (5,8): error CS0506: 'B.EqualityContract': cannot override inherited member 'A.EqualityContract' because it is not marked virtual, abstract, or override // record B : A; Diagnostic(ErrorCode.ERR_CantOverrideNonVirtual, "B").WithArguments("B.EqualityContract", "A.EqualityContract").WithLocation(5, 8)); @@ -4650,7 +4641,7 @@ public void Equality_14() { var source = @"record A; -record B +record B : A { public sealed override System.Type EqualityContract => typeof(B); } @@ -4658,22 +4649,78 @@ record C : B; "; var comp = CreateCompilation(source); comp.VerifyDiagnostics( - // (2,8): error CS0102: The type 'B' already contains a definition for 'EqualityContract' - // record B - Diagnostic(ErrorCode.ERR_DuplicateNameInClass, "B").WithArguments("B", "EqualityContract").WithLocation(2, 8), - // (4,40): error CS0115: 'B.EqualityContract': no suitable method found to override - // public sealed override System.Type EqualityContract => typeof(B); - Diagnostic(ErrorCode.ERR_OverrideNotExpected, "EqualityContract").WithArguments("B.EqualityContract").WithLocation(4, 40), // (6,8): error CS0239: 'C.EqualityContract': cannot override inherited member 'B.EqualityContract' because it is sealed // record C : B; Diagnostic(ErrorCode.ERR_CantOverrideSealed, "C").WithArguments("C.EqualityContract", "B.EqualityContract").WithLocation(6, 8)); } - // PROTOTYPE: - // - Test record deriving from non-record where base does not implement Equals(Base). Derived should compare *accessible fields on Base*. - // - Test record deriving from non-record where base implements EqualityContract and Equals(Base). - // - Test non-record deriving from record where derived overrides EqualityContract but not Equals(Base). - // - Test multiple non-record derived types with a distinct EqualityContract value. Should not compare Equals. - // - Test multiple non-record derived types with a shared base EqualityContract value. Should compare Equals. + [Fact] + public void Equality_15() + { + var source = +@"using System; +record A; +class B1 : A +{ + public B1(int p) { P = p; } + public int P { get; set; } +} +class B2 : A +{ + public B2(int p) { P = p; } + public int P { get; set; } + public override Type EqualityContract => typeof(B2); +} +class Program +{ + static void Main() + { + Console.WriteLine(new B1(1).Equals(new B1(2))); + Console.WriteLine(new B1(1).Equals(new B2(1))); + Console.WriteLine(new B2(1).Equals(new B2(2))); + } +}"; + var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: +@"True +False +True"); + } + + [Fact] + public void Equality_16() + { + var source = +@"using System; +record A; +class B1 : A +{ + public B1(int p) { P = p; } + public int P { get; set; } + public override Type EqualityContract => typeof(string); +} +class B2 : A +{ + public B2(int p) { P = p; } + public int P { get; set; } + public override Type EqualityContract => typeof(string); +} +class Program +{ + static void Main() + { + Console.WriteLine(new B1(1).Equals(new B1(2))); + Console.WriteLine(new B1(1).Equals(new B2(2))); + Console.WriteLine(new B2(1).Equals(new B2(2))); + } +}"; + var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: +@"True +True +True"); + } } } From fa59d0abefe03d58082f64e31b2451bdfa1e2e8d Mon Sep 17 00:00:00 2001 From: Charles Stoner Date: Fri, 5 Jun 2020 15:40:19 -0700 Subject: [PATCH 5/7] Remove remaining PROTOTYPE comments added in PR --- .../Source/SourceMemberContainerSymbol.cs | 14 ++-- .../Records/SynthesizedRecordObjEquals.cs | 3 +- .../Test/Semantic/Semantics/RecordTests.cs | 75 +++++++++++++++++++ 3 files changed, 83 insertions(+), 9 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs index 075c8fd004b78..236a318d0b597 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs @@ -3119,7 +3119,7 @@ void addHashCode() while ((type = type.BaseTypeNoUseSiteDiagnostics) is object) { var members = type.GetMembers(SynthesizedRecordEqualityContractProperty.PropertyName); - // PROTOTYPE: This ignores accessibility and instance/static. + // https://github.com/dotnet/roslyn/issues/44903: Check explicit member has expected signature. if (members.FirstOrDefault(m => m is PropertySymbol property && property.ParameterCount == 0) is PropertySymbol property) { return property; @@ -3131,8 +3131,7 @@ void addHashCode() PropertySymbol addEqualityContract() { var property = new SynthesizedRecordEqualityContractProperty(this, isOverride: getInheritedEqualityContract(this) is object); - // PROTOTYPE: Handle inherited member of unexpected member kind or unexpected - // property signature (distinct type, not virtual, sealed, etc.) + // https://github.com/dotnet/roslyn/issues/44903: Check explicit member has expected signature. if (!memberSignatures.ContainsKey(property)) { members.Add(property); @@ -3158,7 +3157,6 @@ static void getOtherEquals(ArrayBuilder otherEqualsMethods, Proper { var member = equalityContract.ContainingType.GetMembers("Equals").FirstOrDefault(m => { - // PROTOTYPE: This ignores accessibility and instance/static. if (m is MethodSymbol method) { var parameters = method.Parameters; @@ -3169,7 +3167,7 @@ static void getOtherEquals(ArrayBuilder otherEqualsMethods, Proper } return false; }); - // PROTOTYPE: Test with missing or unexpected Equals(Base) methods on base type. + // https://github.com/dotnet/roslyn/issues/44903: Check explicit member has expected signature. if (member is MethodSymbol method) { otherEqualsMethods.Add(method); @@ -3188,8 +3186,10 @@ void addOtherEquals(ArrayBuilder otherEqualsMethods, PropertySymbo equalityContract, otherEqualsMethod: thisEquals, memberOffset: members.Count); - // PROTOTYPE: Test with explicit strongly-typed Equals(Base) methods on derived record type. - members.Add(method); + if (!memberSignatures.ContainsKey(method)) + { + members.Add(method); + } } } } diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordObjEquals.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordObjEquals.cs index e118bee3a8d7f..da6a9df90aab0 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordObjEquals.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordObjEquals.cs @@ -142,10 +142,9 @@ internal override void GenerateMethodBody(TypeCompilationState compilationState, } else { - // PROTOTYPE: Shouldn't have to bind to "Equals". The caller has the actual method in hand. // For classes: // return this.Equals(param as ContainingType); - expression = F.InstanceCall(F.This(), "Equals", F.As(paramAccess, ContainingType)); + expression = F.Call(F.This(), _typedRecordEquals, F.As(paramAccess, ContainingType)); } F.CloseMethod(F.Block(ImmutableArray.Create(F.Return(expression)))); diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs index df8638b1816f2..863e371a4f18c 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs @@ -4652,6 +4652,21 @@ record C : B; // (6,8): error CS0239: 'C.EqualityContract': cannot override inherited member 'B.EqualityContract' because it is sealed // record C : B; Diagnostic(ErrorCode.ERR_CantOverrideSealed, "C").WithArguments("C.EqualityContract", "B.EqualityContract").WithLocation(6, 8)); + + var actualMembers = comp.GetMember("B").GetMembers().ToTestDisplayStrings(); + var expectedMembers = new[] + { + "B B.Clone()", + "System.Type B.EqualityContract { get; }", + "System.Type B.EqualityContract.get", + "System.Int32 B.GetHashCode()", + "System.Boolean B.Equals(System.Object? )", + "System.Boolean B.Equals(A? )", + "System.Boolean B.Equals(B? )", + "B..ctor(B )", + "B..ctor()", + }; + AssertEx.Equal(expectedMembers, actualMembers); } [Fact] @@ -4722,5 +4737,65 @@ static void Main() True True"); } + + [Fact] + public void Equality_17() + { + var source = +@"using static System.Console; +record A; +record B1(int P) : A +{ + public override bool Equals(A other) => false; +} +record B2(int P) : A +{ + public override bool Equals(A other) => true; +} +class Program +{ + static void Main() + { + WriteLine(new B1(1).Equals(new B1(1))); + WriteLine(new B1(1).Equals(new B1(2))); + WriteLine(new B2(3).Equals(new B2(3))); + WriteLine(new B2(3).Equals(new B2(4))); + WriteLine(((A)new B1(1)).Equals(new B1(1))); + WriteLine(((A)new B1(1)).Equals(new B1(2))); + WriteLine(((A)new B2(3)).Equals(new B2(3))); + WriteLine(((A)new B2(3)).Equals(new B2(4))); + } +}"; + var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); + comp.VerifyDiagnostics(); + // init-only is unverifiable + CompileAndVerify(comp, verify: Verification.Skipped, expectedOutput: +@"True +False +True +False +False +False +True +True"); + var actualMembers = comp.GetMember("B1").GetMembers().ToTestDisplayStrings(); + var expectedMembers = new[] + { + "B1 B1.Clone()", + "System.Type B1.EqualityContract.get", + "System.Type B1.EqualityContract { get; }", + "B1..ctor(System.Int32 P)", + "System.Int32 B1.

k__BackingField", + "System.Int32 B1.P.get", + "void modreq(System.Runtime.CompilerServices.IsExternalInit) B1.P.init", + "System.Int32 B1.P { get; init; }", + "System.Boolean B1.Equals(A other)", + "System.Int32 B1.GetHashCode()", + "System.Boolean B1.Equals(System.Object? )", + "System.Boolean B1.Equals(B1? )", + "B1..ctor(B1 )", + }; + AssertEx.Equal(expectedMembers, actualMembers); + } } } From be62ec18a15756294b8349c7824da8cd5bfcdb69 Mon Sep 17 00:00:00 2001 From: Charles Stoner Date: Fri, 5 Jun 2020 17:24:56 -0700 Subject: [PATCH 6/7] PR feedback --- .../Source/SourceMemberContainerSymbol.cs | 15 ++++--- ...nthesizedRecordEqualityContractProperty.cs | 4 +- .../Records/SynthesizedRecordEquals.cs | 2 +- .../Test/Semantic/Semantics/RecordTests.cs | 42 +++++++++++++++---- 4 files changed, 45 insertions(+), 18 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs index b231f28178194..961dae4f8513f 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs @@ -2989,11 +2989,11 @@ private void AddSynthesizedRecordMembersIfNecessary(MembersAndInitializersBuilde addCopyCtor(); addCloneMethod(); - var equalityContract = addEqualityContract(); + PropertySymbol equalityContract = addEqualityContract(); var otherEqualsMethods = ArrayBuilder.GetInstance(); getOtherEquals(otherEqualsMethods, equalityContract); - var thisEquals = addThisEquals(equalityContract, otherEqualsMethods.Count == 0 ? null : otherEqualsMethods[0]); + var thisEquals = addThisEquals(equalityContract, otherEqualsMethod: otherEqualsMethods.Count == 0 ? null : otherEqualsMethods[0]); addOtherEquals(otherEqualsMethods, equalityContract, thisEquals); addObjectEquals(thisEquals); addHashCode(); @@ -3044,7 +3044,7 @@ void addProperties(ImmutableArray recordParameters) { var property = new SynthesizedRecordPropertySymbol(this, param, diagnostics); if (!memberSignatures.ContainsKey(property) && - getInheritedMember(property, this) is null) + !hidesInheritedMember(property, this)) { members.Add(property); members.Add(property.GetMethod); @@ -3066,7 +3066,7 @@ void addProperties(ImmutableArray recordParameters) #endif } - static Symbol? getInheritedMember(Symbol symbol, NamedTypeSymbol type) + static bool hidesInheritedMember(Symbol symbol, NamedTypeSymbol type) { while ((type = type.BaseTypeNoUseSiteDiagnostics) is object) { @@ -3080,16 +3080,15 @@ void addProperties(ImmutableArray recordParameters) out var hiddenBuilder); if (hiddenBuilder is object) { - var result = hiddenBuilder[0]; hiddenBuilder.Free(); - return result; + return true; } if (bestMatch is object) { - return bestMatch; + return true; } } - return null; + return false; } void addObjectEquals(MethodSymbol thisEquals) diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityContractProperty.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityContractProperty.cs index caf859edb1a86..19306372b28a8 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityContractProperty.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityContractProperty.cs @@ -50,7 +50,7 @@ public SynthesizedRecordEqualityContractProperty(NamedTypeSymbol containingType, public override ImmutableArray DeclaringSyntaxReferences => ImmutableArray.Empty; - public override Accessibility DeclaredAccessibility => Accessibility.Public; + public override Accessibility DeclaredAccessibility => Accessibility.Protected; public override bool IsStatic => false; @@ -165,7 +165,7 @@ internal override ImmutableArray GetAppliedConditionalSymbols() internal override IEnumerable GetSecurityInformation() => Array.Empty(); - internal override bool IsMetadataNewSlot(bool ignoreInterfaceImplementationChanges = false) => false; + internal override bool IsMetadataNewSlot(bool ignoreInterfaceImplementationChanges = false) => !IsOverride; internal override bool IsMetadataVirtual(bool ignoreInterfaceImplementationChanges = false) => true; diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEquals.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEquals.cs index c2da9e9541342..604b5a20b9bd4 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEquals.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEquals.cs @@ -140,7 +140,7 @@ internal override ImmutableArray GetAppliedConditionalSymbols() internal override IEnumerable GetSecurityInformation() => Array.Empty(); - internal override bool IsMetadataNewSlot(bool ignoreInterfaceImplementationChanges = false) => false; + internal override bool IsMetadataNewSlot(bool ignoreInterfaceImplementationChanges = false) => !IsOverride; internal override bool IsMetadataVirtual(bool ignoreInterfaceImplementationChanges = false) => true; diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs index 625e6c5753672..14e9e88c3a63c 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs @@ -4830,6 +4830,34 @@ .maxstack 2 IL_0002: call ""bool B.Equals(B)"" IL_0007: ret }"); + + verifyMethod(comp.GetMember("A.get_EqualityContract"), isOverride: false); + verifyMethod(comp.GetMember("B.get_EqualityContract"), isOverride: true); + verifyMethod(comp.GetMember("C.get_EqualityContract"), isOverride: true); + + verifyMethods(comp.GetMembers("A.Equals"), ("System.Boolean A.Equals(A? )", false), ("System.Boolean A.Equals(System.Object? )", true)); + verifyMethods(comp.GetMembers("B.Equals"), ("System.Boolean B.Equals(B? )", false), ("System.Boolean B.Equals(A? )", true), ("System.Boolean B.Equals(System.Object? )", true)); + verifyMethods(comp.GetMembers("C.Equals"), ("System.Boolean C.Equals(C? )", false), ("System.Boolean C.Equals(B? )", true), ("System.Boolean C.Equals(A? )", true), ("System.Boolean C.Equals(System.Object? )", true)); + + static void verifyMethods(ImmutableArray members, params (string, bool)[] values) + { + Assert.Equal(members.Length, values.Length); + for (int i = 0; i < members.Length; i++) + { + var method = (MethodSymbol)members[i]; + (string name, bool isOverride) = values[i]; + Assert.Equal(name, method.ToTestDisplayString(includeNonNullable: true)); + verifyMethod(method, isOverride); + } + } + + static void verifyMethod(MethodSymbol method, bool isOverride) + { + Assert.True(method.IsVirtual); + Assert.Equal(isOverride, method.IsOverride); + Assert.True(method.IsMetadataVirtual()); + Assert.Equal(!isOverride, method.IsMetadataNewSlot()); + } } [WorkItem(44895, "https://github.com/dotnet/roslyn/issues/44895")] @@ -5202,7 +5230,7 @@ public void Equality_11() @"using System; record A { - public virtual Type EqualityContract => typeof(object); + protected virtual Type EqualityContract => typeof(object); } record B1(object P) : A; record B2(object P) : A; @@ -5236,7 +5264,7 @@ public void Equality_12() abstract record A { public A() { } - public abstract Type EqualityContract { get; } + protected abstract Type EqualityContract { get; } } record B1(object P) : A; record B2(object P) : A; @@ -5268,7 +5296,7 @@ public void Equality_13() var source = @"record A { - public System.Type EqualityContract => typeof(A); + protected System.Type EqualityContract => typeof(A); } record B : A; "; @@ -5286,7 +5314,7 @@ public void Equality_14() @"record A; record B : A { - public sealed override System.Type EqualityContract => typeof(B); + protected sealed override System.Type EqualityContract => typeof(B); } record C : B; "; @@ -5327,7 +5355,7 @@ class B2 : A { public B2(int p) { P = p; } public int P { get; set; } - public override Type EqualityContract => typeof(B2); + protected override Type EqualityContract => typeof(B2); } class Program { @@ -5356,13 +5384,13 @@ class B1 : A { public B1(int p) { P = p; } public int P { get; set; } - public override Type EqualityContract => typeof(string); + protected override Type EqualityContract => typeof(string); } class B2 : A { public B2(int p) { P = p; } public int P { get; set; } - public override Type EqualityContract => typeof(string); + protected override Type EqualityContract => typeof(string); } class Program { From 233988fa260d17d906b4649b92c7b53436720195 Mon Sep 17 00:00:00 2001 From: Charles Stoner Date: Fri, 5 Jun 2020 18:01:04 -0700 Subject: [PATCH 7/7] Fix tests --- .../Test/Semantic/Semantics/LookupPositionTests.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/LookupPositionTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/LookupPositionTests.cs index e27672e4cdea2..539540a1a7c10 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/LookupPositionTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/LookupPositionTests.cs @@ -1719,6 +1719,7 @@ record C(int X) : Base`(X`) "System.Int32 X", "System.Object System.Object.MemberwiseClone()", "System.String System.Object.ToString()", + "System.Type C.EqualityContract { get; }", "System.Type System.Object.GetType()", "void System.Object.Finalize()"), s_pop, @@ -1734,6 +1735,7 @@ record C(int X) : Base`(X`) "System.Int32 System.Object.GetHashCode()", "System.Object System.Object.MemberwiseClone()", "System.String System.Object.ToString()", + "System.Type C.EqualityContract { get; }", "System.Type System.Object.GetType()", "void System.Object.Finalize()"), s_pop @@ -1766,6 +1768,7 @@ record C : Base(X) "System.Int32 System.Object.GetHashCode()", "System.Object System.Object.MemberwiseClone()", "System.String System.Object.ToString()", + "System.Type C.EqualityContract { get; }", "System.Type System.Object.GetType()", "void System.Object.Finalize()"), s_pop @@ -1803,6 +1806,7 @@ partial record C : Base(X, Y) "System.Int32 System.Object.GetHashCode()", "System.Object System.Object.MemberwiseClone()", "System.String System.Object.ToString()", + "System.Type C.EqualityContract { get; }", "System.Type System.Object.GetType()", "void System.Object.Finalize()"), s_pop, @@ -1818,6 +1822,7 @@ partial record C : Base(X, Y) "System.Int32 System.Object.GetHashCode()", "System.Object System.Object.MemberwiseClone()", "System.String System.Object.ToString()", + "System.Type C.EqualityContract { get; }", "System.Type System.Object.GetType()", "void System.Object.Finalize()"), s_pop @@ -1856,6 +1861,7 @@ partial record C : Base(X) "System.Int32 X", "System.Object System.Object.MemberwiseClone()", "System.String System.Object.ToString()", + "System.Type C.EqualityContract { get; }", "System.Type System.Object.GetType()", "void System.Object.Finalize()"), s_pop, @@ -1871,6 +1877,7 @@ partial record C : Base(X) "System.Int32 System.Object.GetHashCode()", "System.Object System.Object.MemberwiseClone()", "System.String System.Object.ToString()", + "System.Type C.EqualityContract { get; }", "System.Type System.Object.GetType()", "void System.Object.Finalize()"), s_pop, @@ -1886,6 +1893,7 @@ partial record C : Base(X) "System.Int32 System.Object.GetHashCode()", "System.Object System.Object.MemberwiseClone()", "System.String System.Object.ToString()", + "System.Type C.EqualityContract { get; }", "System.Type System.Object.GetType()", "void System.Object.Finalize()"), s_pop @@ -1923,6 +1931,7 @@ partial record C(int X) : Base`(X`) "System.Int32 System.Object.GetHashCode()", "System.Object System.Object.MemberwiseClone()", "System.String System.Object.ToString()", + "System.Type C.EqualityContract { get; }", "System.Type System.Object.GetType()", "void System.Object.Finalize()"), s_pop, @@ -1938,6 +1947,7 @@ partial record C(int X) : Base`(X`) "System.Int32 X", "System.Object System.Object.MemberwiseClone()", "System.String System.Object.ToString()", + "System.Type C.EqualityContract { get; }", "System.Type System.Object.GetType()", "void System.Object.Finalize()"), s_pop, @@ -1953,6 +1963,7 @@ partial record C(int X) : Base`(X`) "System.Int32 System.Object.GetHashCode()", "System.Object System.Object.MemberwiseClone()", "System.String System.Object.ToString()", + "System.Type C.EqualityContract { get; }", "System.Type System.Object.GetType()", "void System.Object.Finalize()"), s_pop