From 520c324e016c4768973ac7b08f6a6c5ce70acbcc Mon Sep 17 00:00:00 2001 From: AlekseyTs Date: Sat, 1 Aug 2020 11:21:01 -0700 Subject: [PATCH 1/3] Synthesize equality operators for records. Closes #46381. --- .../Compilation/SyntaxTreeSemanticModel.cs | 2 +- .../Source/SourceMemberContainerSymbol.cs | 14 + .../Source/SourceOrdinaryMethodSymbolBase.cs | 2 +- .../SourceUserDefinedConversionSymbol.cs | 30 +- .../Source/SourceUserDefinedOperatorSymbol.cs | 36 +- .../SourceUserDefinedOperatorSymbolBase.cs | 92 +-- .../Records/SynthesizedRecordClone.cs | 36 +- .../SynthesizedRecordEqualityOperator.cs | 75 +++ .../SynthesizedRecordEqualityOperatorBase.cs | 71 +++ .../SynthesizedRecordInequalityOperator.cs | 51 ++ .../Semantic/Semantics/InitOnlyMemberTests.cs | 2 + .../Test/Semantic/Semantics/RecordTests.cs | 522 +++++++++++++++--- .../Test/Symbol/Symbols/Source/RecordTests.cs | 2 + 13 files changed, 792 insertions(+), 143 deletions(-) create mode 100644 src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperator.cs create mode 100644 src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperatorBase.cs create mode 100644 src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordInequalityOperator.cs diff --git a/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs b/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs index e65a222115a66..0c38cf34216b5 100644 --- a/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs @@ -178,7 +178,7 @@ internal override IOperation GetOperationWorker(CSharpSyntaxNode node, Cancellat case AccessorDeclarationSyntax accessor: model = (accessor.Body != null || accessor.ExpressionBody != null) ? GetOrAddModel(node) : null; break; - case RecordDeclarationSyntax { ParameterList: { }, PrimaryConstructorBaseType: { } } recordDeclaration when TryGetSynthesizedRecordConstructor(recordDeclaration) is SynthesizedRecordConstructor ctor: + case RecordDeclarationSyntax { ParameterList: { }, PrimaryConstructorBaseType: { } } recordDeclaration when TryGetSynthesizedRecordConstructor(recordDeclaration) is SynthesizedRecordConstructor: model = GetOrAddModel(recordDeclaration); break; default: diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs index d8cc5215be73d..2e11d7e0a0687 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs @@ -2067,6 +2067,13 @@ private void CheckForEqualityAndGetHashCode(DiagnosticBag diagnostics) return; } + if (IsRecord) + { + // For records the warnings reported below are simply going to eco record specific errors, + // producing more noise. + return; + } + bool hasOp = this.GetOperators(WellKnownMemberNames.EqualityOperatorName).Any() || this.GetOperators(WellKnownMemberNames.InequalityOperatorName).Any(); bool overridesEquals = this.TypeOverridesObjectMethod("Equals"); @@ -3023,6 +3030,7 @@ private void AddSynthesizedRecordMembersIfNecessary(MembersAndInitializersBuilde addOtherEquals(); addObjectEquals(thisEquals); addHashCode(equalityContract); + addEqualityOperators(); memberSignatures.Free(); @@ -3355,6 +3363,12 @@ void addOtherEquals() members.Add(new SynthesizedRecordBaseEquals(this, memberOffset: members.Count, diagnostics)); } } + + void addEqualityOperators() + { + members.Add(new SynthesizedRecordEqualityOperator(this, memberOffset: members.Count, diagnostics)); + members.Add(new SynthesizedRecordInequalityOperator(this, memberOffset: members.Count, diagnostics)); + } } private void AddSynthesizedConstructorsIfNecessary(ArrayBuilder members, ArrayBuilder> staticInitializers, DiagnosticBag diagnostics) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceOrdinaryMethodSymbolBase.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceOrdinaryMethodSymbolBase.cs index ec232e0b57d26..eb0e7f1c2d18a 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceOrdinaryMethodSymbolBase.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceOrdinaryMethodSymbolBase.cs @@ -311,7 +311,7 @@ public override ImmutableArray Locations } } - internal override int ParameterCount + internal sealed override int ParameterCount { get { diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceUserDefinedConversionSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceUserDefinedConversionSymbol.cs index 3c5b7b6c494d9..ea7f73a70e931 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceUserDefinedConversionSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceUserDefinedConversionSymbol.cs @@ -3,10 +3,12 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Immutable; using System.Diagnostics; using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.Symbols { @@ -43,6 +45,10 @@ private SourceUserDefinedConversionSymbol( containingType, location, syntax, + MakeDeclarationModifiers(syntax, location, diagnostics), + hasBody: syntax.HasAnyBody(), + isExpressionBodied: syntax.Body == null && syntax.ExpressionBody != null, + isIterator: SyntaxFacts.HasYieldOperations(syntax.Body), diagnostics) { CheckForBlockAndExpressionBody( @@ -54,25 +60,22 @@ private SourceUserDefinedConversionSymbol( } } - internal new ConversionOperatorDeclarationSyntax GetSyntax() + internal ConversionOperatorDeclarationSyntax GetSyntax() { Debug.Assert(syntaxReferenceOpt != null); return (ConversionOperatorDeclarationSyntax)syntaxReferenceOpt.GetSyntax(); } - protected override ParameterListSyntax ParameterListSyntax + protected override int GetParameterCountFromSyntax() { - get - { - return GetSyntax().ParameterList; - } + return GetSyntax().ParameterList.ParameterCount; } - protected override TypeSyntax ReturnTypeSyntax + protected override Location ReturnTypeLocation { get { - return GetSyntax().Type; + return GetSyntax().Type.Location; } } @@ -80,5 +83,16 @@ internal override bool GenerateDebugInfo { get { return true; } } + + internal sealed override OneOrMany> GetAttributeDeclarations() + { + return OneOrMany.Create(this.GetSyntax().AttributeLists); + } + + protected override (TypeWithAnnotations ReturnType, ImmutableArray Parameters) MakeParametersAndBindReturnType(DiagnosticBag diagnostics) + { + ConversionOperatorDeclarationSyntax declarationSyntax = GetSyntax(); + return MakeParametersAndBindReturnType(declarationSyntax, declarationSyntax.Type, diagnostics); + } } } diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceUserDefinedOperatorSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceUserDefinedOperatorSymbol.cs index 33163793b69cc..aa3c54e1fdcd6 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceUserDefinedOperatorSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceUserDefinedOperatorSymbol.cs @@ -3,8 +3,10 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Immutable; using System.Diagnostics; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.Symbols { @@ -20,8 +22,7 @@ public static SourceUserDefinedOperatorSymbol CreateUserDefinedOperatorSymbol( string name = OperatorFacts.OperatorNameFromDeclaration(syntax); return new SourceUserDefinedOperatorSymbol( - containingType, name, location, syntax, diagnostics, - syntax.Body == null && syntax.ExpressionBody != null); + containingType, name, location, syntax, diagnostics); } // NOTE: no need to call WithUnsafeRegionIfNecessary, since the signature @@ -32,14 +33,17 @@ private SourceUserDefinedOperatorSymbol( string name, Location location, OperatorDeclarationSyntax syntax, - DiagnosticBag diagnostics, - bool isExpressionBodied) : + DiagnosticBag diagnostics) : base( MethodKind.UserDefinedOperator, name, containingType, location, syntax, + MakeDeclarationModifiers(syntax, location, diagnostics), + hasBody: syntax.HasAnyBody(), + isExpressionBodied: syntax.Body == null && syntax.ExpressionBody != null, + isIterator: SyntaxFacts.HasYieldOperations(syntax.Body), diagnostics) { CheckForBlockAndExpressionBody( @@ -51,25 +55,22 @@ private SourceUserDefinedOperatorSymbol( } } - internal new OperatorDeclarationSyntax GetSyntax() + internal OperatorDeclarationSyntax GetSyntax() { Debug.Assert(syntaxReferenceOpt != null); return (OperatorDeclarationSyntax)syntaxReferenceOpt.GetSyntax(); } - protected override ParameterListSyntax ParameterListSyntax + protected override int GetParameterCountFromSyntax() { - get - { - return GetSyntax().ParameterList; - } + return GetSyntax().ParameterList.ParameterCount; } - protected override TypeSyntax ReturnTypeSyntax + protected override Location ReturnTypeLocation { get { - return GetSyntax().ReturnType; + return GetSyntax().ReturnType.Location; } } @@ -77,5 +78,16 @@ internal override bool GenerateDebugInfo { get { return true; } } + + internal sealed override OneOrMany> GetAttributeDeclarations() + { + return OneOrMany.Create(this.GetSyntax().AttributeLists); + } + + protected override (TypeWithAnnotations ReturnType, ImmutableArray Parameters) MakeParametersAndBindReturnType(DiagnosticBag diagnostics) + { + OperatorDeclarationSyntax declarationSyntax = GetSyntax(); + return MakeParametersAndBindReturnType(declarationSyntax, declarationSyntax.ReturnType, diagnostics); + } } } diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceUserDefinedOperatorSymbolBase.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceUserDefinedOperatorSymbolBase.cs index 08c217d20fb8a..7e90545144a83 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceUserDefinedOperatorSymbolBase.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceUserDefinedOperatorSymbolBase.cs @@ -26,23 +26,16 @@ protected SourceUserDefinedOperatorSymbolBase( string name, SourceMemberContainerTypeSymbol containingType, Location location, - BaseMethodDeclarationSyntax syntax, + CSharpSyntaxNode syntax, + DeclarationModifiers declarationModifiers, + bool hasBody, + bool isExpressionBodied, + bool isIterator, DiagnosticBag diagnostics) : - base(containingType, syntax.GetReference(), location, isIterator: SyntaxFacts.HasYieldOperations(syntax.Body)) + base(containingType, syntax.GetReference(), location, isIterator) { _name = name; - _isExpressionBodied = syntax.Body == null && syntax.ExpressionBody != null; - - var defaultAccess = DeclarationModifiers.Private; - var allowedModifiers = - DeclarationModifiers.AccessibilityMask | - DeclarationModifiers.Static | - DeclarationModifiers.Extern | - DeclarationModifiers.Unsafe; - - bool modifierErrors; - var declarationModifiers = ModifierUtils.MakeAndCheckNontypeMemberModifiers( - syntax.Modifiers, defaultAccess, allowedModifiers, location, diagnostics, out modifierErrors); + _isExpressionBodied = isExpressionBodied; this.CheckUnsafeModifier(declarationModifiers, diagnostics); @@ -81,7 +74,6 @@ protected SourceUserDefinedOperatorSymbolBase( // SPEC: its operator body consists of a semicolon. For expression-bodied // SPEC: operators, the body is an expression. For all other operators, // SPEC: the operator body consists of a block... - bool hasBody = syntax.HasAnyBody(); if (hasBody && IsExtern) { diagnostics.Add(ErrorCode.ERR_ExternHasBody, location, this); @@ -103,28 +95,37 @@ protected SourceUserDefinedOperatorSymbolBase( } } - internal BaseMethodDeclarationSyntax GetSyntax() + protected static DeclarationModifiers MakeDeclarationModifiers(BaseMethodDeclarationSyntax syntax, Location location, DiagnosticBag diagnostics) { - Debug.Assert(syntaxReferenceOpt != null); - return (BaseMethodDeclarationSyntax)syntaxReferenceOpt.GetSyntax(); + var defaultAccess = DeclarationModifiers.Private; + var allowedModifiers = + DeclarationModifiers.AccessibilityMask | + DeclarationModifiers.Static | + DeclarationModifiers.Extern | + DeclarationModifiers.Unsafe; + + return ModifierUtils.MakeAndCheckNontypeMemberModifiers( + syntax.Modifiers, defaultAccess, allowedModifiers, location, diagnostics, modifierErrors: out _); } - abstract protected ParameterListSyntax ParameterListSyntax { get; } - abstract protected TypeSyntax ReturnTypeSyntax { get; } + protected abstract Location ReturnTypeLocation { get; } - protected override void MethodChecks(DiagnosticBag diagnostics) + protected (TypeWithAnnotations ReturnType, ImmutableArray Parameters) MakeParametersAndBindReturnType(BaseMethodDeclarationSyntax declarationSyntax, TypeSyntax returnTypeSyntax, DiagnosticBag diagnostics) { + TypeWithAnnotations returnType; + ImmutableArray parameters; + var binder = this.DeclaringCompilation. - GetBinderFactory(syntaxReferenceOpt.SyntaxTree).GetBinder(ReturnTypeSyntax, GetSyntax(), this); + GetBinderFactory(declarationSyntax.SyntaxTree).GetBinder(returnTypeSyntax, declarationSyntax, this); SyntaxToken arglistToken; var signatureBinder = binder.WithAdditionalFlags(BinderFlags.SuppressConstraintChecks); - _lazyParameters = ParameterHelpers.MakeParameters( + parameters = ParameterHelpers.MakeParameters( signatureBinder, this, - ParameterListSyntax, + declarationSyntax.ParameterList, out arglistToken, allowRefOrOut: true, allowThis: false, @@ -142,22 +143,29 @@ protected override void MethodChecks(DiagnosticBag diagnostics) // the operator method as being a varargs method. } - _lazyReturnType = signatureBinder.BindType(ReturnTypeSyntax, diagnostics); + returnType = signatureBinder.BindType(returnTypeSyntax, diagnostics); // restricted types cannot be returned. // NOTE: Span-like types can be returned (if expression is returnable). - if (_lazyReturnType.IsRestrictedType(ignoreSpanLikeTypes: true)) + if (returnType.IsRestrictedType(ignoreSpanLikeTypes: true)) { // The return type of a method, delegate, or function pointer cannot be '{0}' - diagnostics.Add(ErrorCode.ERR_MethodReturnCantBeRefAny, ReturnTypeSyntax.Location, _lazyReturnType.Type); + diagnostics.Add(ErrorCode.ERR_MethodReturnCantBeRefAny, returnTypeSyntax.Location, returnType.Type); } - if (_lazyReturnType.Type.IsStatic) + if (returnType.Type.IsStatic) { // '{0}': static types cannot be used as return types - diagnostics.Add(ErrorCode.ERR_ReturnTypeIsStaticClass, ReturnTypeSyntax.Location, _lazyReturnType.Type); + diagnostics.Add(ErrorCode.ERR_ReturnTypeIsStaticClass, returnTypeSyntax.Location, returnType.Type); } + return (returnType, parameters); + } + + protected override void MethodChecks(DiagnosticBag diagnostics) + { + (_lazyReturnType, _lazyParameters) = MakeParametersAndBindReturnType(diagnostics); + this.SetReturnsVoid(_lazyReturnType.IsVoidType()); // If we have a conversion/equality/inequality operator in an interface or static class then we already @@ -177,6 +185,8 @@ protected override void MethodChecks(DiagnosticBag diagnostics) CheckOperatorSignatures(diagnostics); } + protected abstract (TypeWithAnnotations ReturnType, ImmutableArray Parameters) MakeParametersAndBindReturnType(DiagnosticBag diagnostics); + private void CheckValueParameters(DiagnosticBag diagnostics) { // SPEC: The parameters of an operator must be value parameters. @@ -604,10 +614,19 @@ internal sealed override int ParameterCount { get { - return !_lazyParameters.IsDefault ? _lazyParameters.Length : GetSyntax().ParameterList.ParameterCount; + if (!_lazyParameters.IsDefault) + { + int result = _lazyParameters.Length; + Debug.Assert(result == GetParameterCountFromSyntax()); + return result; + } + + return GetParameterCountFromSyntax(); } } + protected abstract int GetParameterCountFromSyntax(); + public sealed override ImmutableArray Parameters { get @@ -625,7 +644,7 @@ public sealed override ImmutableArray TypeParameters public sealed override ImmutableArray GetTypeParameterConstraintClauses() => ImmutableArray.Empty; - public override RefKind RefKind + public sealed override RefKind RefKind { get { return RefKind.None; } } @@ -639,16 +658,11 @@ public sealed override TypeWithAnnotations ReturnTypeWithAnnotations } } - internal override bool IsExpressionBodied + internal sealed override bool IsExpressionBodied { get { return _isExpressionBodied; } } - internal sealed override OneOrMany> GetAttributeDeclarations() - { - return OneOrMany.Create(this.GetSyntax().AttributeLists); - } - internal sealed override void AfterAddingTypeMembersChecks(ConversionsBase conversions, DiagnosticBag diagnostics) { // Check constraints on return type and parameters. Note: Dev10 uses the @@ -668,7 +682,7 @@ internal sealed override void AfterAddingTypeMembersChecks(ConversionsBase conve if (ReturnType.ContainsNativeInteger()) { - compilation.EnsureNativeIntegerAttributeExists(diagnostics, ReturnTypeSyntax.Location, modifyCompilation: true); + compilation.EnsureNativeIntegerAttributeExists(diagnostics, ReturnTypeLocation, modifyCompilation: true); } ParameterHelpers.EnsureNativeIntegerAttributeExists(compilation, Parameters, diagnostics, modifyCompilation: true); @@ -676,7 +690,7 @@ internal sealed override void AfterAddingTypeMembersChecks(ConversionsBase conve if (compilation.ShouldEmitNullableAttributes(this) && ReturnTypeWithAnnotations.NeedsNullableAttribute()) { - compilation.EnsureNullableAttributeExists(diagnostics, ReturnTypeSyntax.Location, modifyCompilation: true); + compilation.EnsureNullableAttributeExists(diagnostics, ReturnTypeLocation, modifyCompilation: true); } ParameterHelpers.EnsureNullableAttributeExists(compilation, this, Parameters, diagnostics, modifyCompilation: true); diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordClone.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordClone.cs index a07a42912c877..556395778dd94 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordClone.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordClone.cs @@ -118,25 +118,33 @@ internal override void GenerateMethodBody(TypeCompilationState compilationState, var F = new SyntheticBoundNodeFactory(this, ContainingType.GetNonNullSyntaxNode(), compilationState, diagnostics); - if (ReturnType.IsErrorType()) + try { - F.CloseMethod(F.ThrowNull()); - return; - } - - var members = ContainingType.InstanceConstructors; - foreach (var member in members) - { - var ctor = (MethodSymbol)member; - if (ctor.ParameterCount == 1 && ctor.Parameters[0].RefKind == RefKind.None && - ctor.Parameters[0].Type.Equals(ContainingType, TypeCompareKind.AllIgnoreOptions)) + if (ReturnType.IsErrorType()) { - F.CloseMethod(F.Return(F.New(ctor, F.This()))); + F.CloseMethod(F.ThrowNull()); return; } - } - throw ExceptionUtilities.Unreachable; + var members = ContainingType.InstanceConstructors; + foreach (var member in members) + { + var ctor = (MethodSymbol)member; + if (ctor.ParameterCount == 1 && ctor.Parameters[0].RefKind == RefKind.None && + ctor.Parameters[0].Type.Equals(ContainingType, TypeCompareKind.AllIgnoreOptions)) + { + F.CloseMethod(F.Return(F.New(ctor, F.This()))); + return; + } + } + + throw ExceptionUtilities.Unreachable; + } + catch (SyntheticBoundNodeFactory.MissingPredefinedMember ex) + { + diagnostics.Add(ex.Diagnostic); + F.CloseMethod(F.ThrowNull()); + } } // Note: this method was replicated in SymbolDisplayVisitor.FindValidCloneMethod diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperator.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperator.cs new file mode 100644 index 0000000000000..cf73d5834077f --- /dev/null +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperator.cs @@ -0,0 +1,75 @@ +// 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.Diagnostics; +using System.Globalization; +using System.Threading; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.Symbols +{ + /// + /// The record type includes synthesized '==' and '!=' operators equivalent to operators declared as follows: + /// + /// public static bool operator==(R? r1, R? r2) + /// => (object) r1 == r2 || (r1?.Equals(r2) ?? false); + /// public static bool operator !=(R? r1, R? r2) + /// => !(r1 == r2); + /// + ///The 'Equals' method called by the '==' operator is the 'Equals(R? other)' (). + ///The '!=' operator delegates to the '==' operator. It is an error if the operators are declared explicitly. + /// + internal sealed class SynthesizedRecordEqualityOperator : SynthesizedRecordEqualityOperatorBase + { + public SynthesizedRecordEqualityOperator(SourceMemberContainerTypeSymbol containingType, int memberOffset, DiagnosticBag diagnostics) + : base(containingType, WellKnownMemberNames.EqualityOperatorName, memberOffset, diagnostics) + { + } + + internal override void GenerateMethodBody(TypeCompilationState compilationState, DiagnosticBag diagnostics) + { + var F = new SyntheticBoundNodeFactory(this, ContainingType.GetNonNullSyntaxNode(), compilationState, diagnostics); + + try + { + // => (object)r1 == r2 || (r1?.Equals(r2) ?? false); + MethodSymbol? equals = null; + foreach (var member in ContainingType.GetMembers(WellKnownMemberNames.ObjectEquals)) + { + if (member is MethodSymbol candidate && candidate.ParameterCount == 1 && candidate.Parameters[0].RefKind == RefKind.None && + candidate.ReturnType.SpecialType == SpecialType.System_Boolean && !candidate.IsStatic && + candidate.Parameters[0].Type.Equals(ContainingType, TypeCompareKind.AllIgnoreOptions)) + { + equals = candidate; + break; + } + } + + if (equals is null) + { + // Unable to locate expected method, an error was reported elsewhere + F.CloseMethod(F.ThrowNull()); + return; + } + + var r1 = F.Parameter(Parameters[0]); + var r2 = F.Parameter(Parameters[1]); + + BoundExpression objectEqual = F.ObjectEqual(r1, r2); + BoundExpression recordEquals = F.LogicalAnd(F.ObjectNotEqual(r1, F.Null(F.SpecialType(SpecialType.System_Object))), + F.Call(r1, equals, r2)); + + F.CloseMethod(F.Block(F.Return(F.LogicalOr(objectEqual, recordEquals)))); + } + catch (SyntheticBoundNodeFactory.MissingPredefinedMember ex) + { + diagnostics.Add(ex.Diagnostic); + F.CloseMethod(F.ThrowNull()); + } + } + } +} diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperatorBase.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperatorBase.cs new file mode 100644 index 0000000000000..99a4b8003a666 --- /dev/null +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperatorBase.cs @@ -0,0 +1,71 @@ +// 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 System.Globalization; +using System.Threading; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.Symbols +{ + /// + /// The record type includes synthesized '==' and '!=' operators equivalent to operators declared as follows: + /// + /// public static bool operator==(R? r1, R? r2) + /// => (object) r1 == r2 || (r1?.Equals(r2) ?? false); + /// public static bool operator !=(R? r1, R? r2) + /// => !(r1 == r2); + /// + ///The 'Equals' method called by the '==' operator is the 'Equals(R? other)' (). + ///The '!=' operator delegates to the '==' operator. It is an error if the operators are declared explicitly. + /// + internal abstract class SynthesizedRecordEqualityOperatorBase : SourceUserDefinedOperatorSymbolBase + { + private readonly int _memberOffset; + + protected SynthesizedRecordEqualityOperatorBase(SourceMemberContainerTypeSymbol containingType, string name, int memberOffset, DiagnosticBag diagnostics) + : base(MethodKind.UserDefinedOperator, name, containingType, containingType.Locations[0], (CSharpSyntaxNode)containingType.SyntaxReferences[0].GetSyntax(), + DeclarationModifiers.Public | DeclarationModifiers.Static, hasBody: true, isExpressionBodied: false, isIterator: false, diagnostics) + { + Debug.Assert(name == WellKnownMemberNames.EqualityOperatorName || name == WellKnownMemberNames.InequalityOperatorName); + _memberOffset = memberOffset; + } + + public sealed override bool IsImplicitlyDeclared => true; + + protected sealed override Location ReturnTypeLocation => Locations[0]; + + internal sealed override LexicalSortKey GetLexicalSortKey() => LexicalSortKey.GetSynthesizedMemberKey(_memberOffset); + + protected sealed override SourceMemberMethodSymbol? BoundAttributesSource => null; + + internal sealed override OneOrMany> GetAttributeDeclarations() => OneOrMany.Create(default(SyntaxList)); + + public sealed override string? GetDocumentationCommentXml(CultureInfo? preferredCulture = null, bool expandIncludes = false, CancellationToken cancellationToken = default) => null; + + internal sealed override bool GenerateDebugInfo => false; + + internal sealed override bool SynthesizesLoweredBoundBody => true; + + protected sealed override (TypeWithAnnotations ReturnType, ImmutableArray Parameters) MakeParametersAndBindReturnType(DiagnosticBag diagnostics) + { + var compilation = DeclaringCompilation; + var location = ReturnTypeLocation; + return (ReturnType: TypeWithAnnotations.Create(Binder.GetSpecialType(compilation, SpecialType.System_Boolean, location, diagnostics)), + Parameters: ImmutableArray.Create( + new SourceSimpleParameterSymbol(owner: this, + TypeWithAnnotations.Create(ContainingType, NullableAnnotation.Annotated), + ordinal: 0, RefKind.None, "r1", isDiscard: false, Locations), + new SourceSimpleParameterSymbol(owner: this, + TypeWithAnnotations.Create(ContainingType, NullableAnnotation.Annotated), + ordinal: 1, RefKind.None, "r2", isDiscard: false, Locations))); + } + + protected override int GetParameterCountFromSyntax() => 2; + } +} diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordInequalityOperator.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordInequalityOperator.cs new file mode 100644 index 0000000000000..96913d8690876 --- /dev/null +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordInequalityOperator.cs @@ -0,0 +1,51 @@ +// 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.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.Symbols +{ + /// + /// The record type includes synthesized '==' and '!=' operators equivalent to operators declared as follows: + /// + /// public static bool operator==(R? r1, R? r2) + /// => (object) r1 == r2 || (r1?.Equals(r2) ?? false); + /// public static bool operator !=(R? r1, R? r2) + /// => !(r1 == r2); + /// + ///The 'Equals' method called by the '==' operator is the 'Equals(R? other)' (). + ///The '!=' operator delegates to the '==' operator. It is an error if the operators are declared explicitly. + /// + internal sealed class SynthesizedRecordInequalityOperator : SynthesizedRecordEqualityOperatorBase + { + public SynthesizedRecordInequalityOperator(SourceMemberContainerTypeSymbol containingType, int memberOffset, DiagnosticBag diagnostics) + : base(containingType, WellKnownMemberNames.InequalityOperatorName, memberOffset, diagnostics) + { + } + + internal override void GenerateMethodBody(TypeCompilationState compilationState, DiagnosticBag diagnostics) + { + var F = new SyntheticBoundNodeFactory(this, ContainingType.GetNonNullSyntaxNode(), compilationState, diagnostics); + + try + { + // => !(r1 == r2); + F.CloseMethod(F.Block(F.Return(F.Not(F.Call(receiver: null, ContainingType.GetMembers(WellKnownMemberNames.EqualityOperatorName).OfType().Single(), + F.Parameter(Parameters[0]), F.Parameter(Parameters[1])))))); + } + catch (SyntheticBoundNodeFactory.MissingPredefinedMember ex) + { + diagnostics.Add(ex.Diagnostic); + F.CloseMethod(F.ThrowNull()); + } + } + } +} diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InitOnlyMemberTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InitOnlyMemberTests.cs index 28c9ca6abe033..90895342e7a3b 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InitOnlyMemberTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InitOnlyMemberTests.cs @@ -2182,6 +2182,8 @@ void M() { } "void modreq(System.Runtime.CompilerServices.IsExternalInit) C.i.init", "System.Int32 C.i { get; init; }", "void C.M()", + "System.Boolean C.op_Inequality(C? r1, C? r2)", + "System.Boolean C.op_Equality(C? r1, C? r2)", "System.Int32 C.GetHashCode()", "System.Boolean C.Equals(System.Object? obj)", "System.Boolean C.Equals(C? other)", diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs index c773704ed041d..7fb6364845457 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs @@ -610,6 +610,8 @@ 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.op_Inequality(C? r1, C? r2)", + "System.Boolean C.op_Equality(C? r1, C? r2)", "System.Int32 C.GetHashCode()", "System.Boolean C.Equals(System.Object? obj)", "System.Boolean C.Equals(C? other)", @@ -5431,6 +5433,8 @@ public void Inheritance_09() "System.Int32 C.k__BackingField", "System.Int32 C.Y { get; }", "System.Int32 C.Y.get", + "System.Boolean C.op_Inequality(C? r1, C? r2)", + "System.Boolean C.op_Equality(C? r1, C? r2)", "System.Int32 C.GetHashCode()", "System.Boolean C.Equals(System.Object? obj)", "System.Boolean C.Equals(C? other)", @@ -5944,6 +5948,8 @@ 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.op_Inequality(B? r1, B? r2)", + "System.Boolean B.op_Equality(B? r1, B? r2)", "System.Int32 B.GetHashCode()", "System.Boolean B.Equals(System.Object? obj)", "System.Boolean B.Equals(A? other)", @@ -5965,6 +5971,8 @@ record C(object P) "System.Object C.P { get; init; }", "System.Object C.get_P()", "System.Object C.set_Q()", + "System.Boolean C.op_Inequality(C? r1, C? r2)", + "System.Boolean C.op_Equality(C? r1, C? r2)", "System.Int32 C.GetHashCode()", "System.Boolean C.Equals(System.Object? obj)", "System.Boolean C.Equals(C? other)", @@ -10485,6 +10493,8 @@ 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.op_Inequality(B? r1, B? r2)", + "System.Boolean B.op_Equality(B? r1, B? r2)", "System.Int32 B.GetHashCode()", "System.Boolean B.Equals(System.Object? obj)", "System.Boolean B.Equals(A? other)", @@ -10536,6 +10546,8 @@ 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.op_Inequality(B? r1, B? r2)", + "System.Boolean B.op_Equality(B? r1, B? r2)", "System.Int32 B.GetHashCode()", "System.Boolean B.Equals(System.Object? obj)", "System.Boolean B.Equals(A? other)", @@ -11122,17 +11134,11 @@ public record C : B { comp.VerifyEmitDiagnostics( // (2,15): error CS8869: 'B.GetHashCode()' does not override expected method from 'object'. // public record B : A { - Diagnostic(ErrorCode.ERR_DoesNotOverrideMethodFromObject, "B").WithArguments("B.GetHashCode()").WithLocation(2, 15), - // (2,15): warning CS0659: 'B' overrides Object.Equals(object o) but does not override Object.GetHashCode() - // public record B : A { - Diagnostic(ErrorCode.WRN_EqualsWithoutGetHashCode, "B").WithArguments("B").WithLocation(2, 15) + Diagnostic(ErrorCode.ERR_DoesNotOverrideMethodFromObject, "B").WithArguments("B.GetHashCode()").WithLocation(2, 15) ); comp = CreateCompilationWithIL(new[] { source2, IsExternalInitTypeDefinition }, ilSource: ilSource, parseOptions: TestOptions.RegularPreview); comp.VerifyEmitDiagnostics( - // (2,15): warning CS0659: 'B' overrides Object.Equals(object o) but does not override Object.GetHashCode() - // public record B : A { - Diagnostic(ErrorCode.WRN_EqualsWithoutGetHashCode, "B").WithArguments("B").WithLocation(2, 15), // (3,25): error CS8869: 'B.GetHashCode()' does not override expected method from 'object'. // public override int GetHashCode() => 0; Diagnostic(ErrorCode.ERR_DoesNotOverrideMethodFromObject, "GetHashCode").WithArguments("B.GetHashCode()").WithLocation(3, 25) @@ -11142,52 +11148,28 @@ public record C : B { comp.VerifyEmitDiagnostics( // (2,15): error CS8869: 'B.GetHashCode()' does not override expected method from 'object'. // public record B : A { - Diagnostic(ErrorCode.ERR_DoesNotOverrideMethodFromObject, "B").WithArguments("B.GetHashCode()").WithLocation(2, 15), - // (2,15): warning CS0659: 'B' overrides Object.Equals(object o) but does not override Object.GetHashCode() - // public record B : A { - Diagnostic(ErrorCode.WRN_EqualsWithoutGetHashCode, "B").WithArguments("B").WithLocation(2, 15), - // (4,15): warning CS0659: 'C' overrides Object.Equals(object o) but does not override Object.GetHashCode() - // public record C : B { - Diagnostic(ErrorCode.WRN_EqualsWithoutGetHashCode, "C").WithArguments("C").WithLocation(4, 15) + Diagnostic(ErrorCode.ERR_DoesNotOverrideMethodFromObject, "B").WithArguments("B.GetHashCode()").WithLocation(2, 15) ); comp = CreateCompilationWithIL(new[] { source1 + source4, IsExternalInitTypeDefinition }, ilSource: ilSource, parseOptions: TestOptions.RegularPreview); comp.VerifyEmitDiagnostics( // (2,15): error CS8869: 'B.GetHashCode()' does not override expected method from 'object'. // public record B : A { - Diagnostic(ErrorCode.ERR_DoesNotOverrideMethodFromObject, "B").WithArguments("B.GetHashCode()").WithLocation(2, 15), - // (2,15): warning CS0659: 'B' overrides Object.Equals(object o) but does not override Object.GetHashCode() - // public record B : A { - Diagnostic(ErrorCode.WRN_EqualsWithoutGetHashCode, "B").WithArguments("B").WithLocation(2, 15), - // (4,15): warning CS0659: 'C' overrides Object.Equals(object o) but does not override Object.GetHashCode() - // public record C : B { - Diagnostic(ErrorCode.WRN_EqualsWithoutGetHashCode, "C").WithArguments("C").WithLocation(4, 15) + Diagnostic(ErrorCode.ERR_DoesNotOverrideMethodFromObject, "B").WithArguments("B.GetHashCode()").WithLocation(2, 15) ); comp = CreateCompilationWithIL(new[] { source2 + source3, IsExternalInitTypeDefinition }, ilSource: ilSource, parseOptions: TestOptions.RegularPreview); comp.VerifyEmitDiagnostics( - // (2,15): warning CS0659: 'B' overrides Object.Equals(object o) but does not override Object.GetHashCode() - // public record B : A { - Diagnostic(ErrorCode.WRN_EqualsWithoutGetHashCode, "B").WithArguments("B").WithLocation(2, 15), // (3,25): error CS8869: 'B.GetHashCode()' does not override expected method from 'object'. // public override int GetHashCode() => 0; - Diagnostic(ErrorCode.ERR_DoesNotOverrideMethodFromObject, "GetHashCode").WithArguments("B.GetHashCode()").WithLocation(3, 25), - // (5,15): warning CS0659: 'C' overrides Object.Equals(object o) but does not override Object.GetHashCode() - // public record C : B { - Diagnostic(ErrorCode.WRN_EqualsWithoutGetHashCode, "C").WithArguments("C").WithLocation(5, 15) + Diagnostic(ErrorCode.ERR_DoesNotOverrideMethodFromObject, "GetHashCode").WithArguments("B.GetHashCode()").WithLocation(3, 25) ); comp = CreateCompilationWithIL(new[] { source2 + source4, IsExternalInitTypeDefinition }, ilSource: ilSource, parseOptions: TestOptions.RegularPreview); comp.VerifyEmitDiagnostics( - // (2,15): warning CS0659: 'B' overrides Object.Equals(object o) but does not override Object.GetHashCode() - // public record B : A { - Diagnostic(ErrorCode.WRN_EqualsWithoutGetHashCode, "B").WithArguments("B").WithLocation(2, 15), // (3,25): error CS8869: 'B.GetHashCode()' does not override expected method from 'object'. // public override int GetHashCode() => 0; - Diagnostic(ErrorCode.ERR_DoesNotOverrideMethodFromObject, "GetHashCode").WithArguments("B.GetHashCode()").WithLocation(3, 25), - // (5,15): warning CS0659: 'C' overrides Object.Equals(object o) but does not override Object.GetHashCode() - // public record C : B { - Diagnostic(ErrorCode.WRN_EqualsWithoutGetHashCode, "C").WithArguments("C").WithLocation(5, 15) + Diagnostic(ErrorCode.ERR_DoesNotOverrideMethodFromObject, "GetHashCode").WithArguments("B.GetHashCode()").WithLocation(3, 25) ); } @@ -11281,10 +11263,7 @@ public record B : A { comp.VerifyEmitDiagnostics( // (2,15): error CS0506: 'B.GetHashCode()': cannot override inherited member 'A.GetHashCode()' because it is not marked virtual, abstract, or override // public record B : A { - Diagnostic(ErrorCode.ERR_CantOverrideNonVirtual, "B").WithArguments("B.GetHashCode()", "A.GetHashCode()").WithLocation(2, 15), - // (2,15): warning CS0659: 'B' overrides Object.Equals(object o) but does not override Object.GetHashCode() - // public record B : A { - Diagnostic(ErrorCode.WRN_EqualsWithoutGetHashCode, "B").WithArguments("B").WithLocation(2, 15) + Diagnostic(ErrorCode.ERR_CantOverrideNonVirtual, "B").WithArguments("B.GetHashCode()", "A.GetHashCode()").WithLocation(2, 15) ); var source2 = @" @@ -11293,9 +11272,6 @@ public record B : A { }"; comp = CreateCompilationWithIL(new[] { source2, IsExternalInitTypeDefinition }, ilSource: ilSource, parseOptions: TestOptions.RegularPreview); comp.VerifyEmitDiagnostics( - // (2,15): warning CS0659: 'B' overrides Object.Equals(object o) but does not override Object.GetHashCode() - // public record B : A { - Diagnostic(ErrorCode.WRN_EqualsWithoutGetHashCode, "B").WithArguments("B").WithLocation(2, 15), // (3,25): error CS0506: 'B.GetHashCode()': cannot override inherited member 'A.GetHashCode()' because it is not marked virtual, abstract, or override // public override int GetHashCode() => throw null; Diagnostic(ErrorCode.ERR_CantOverrideNonVirtual, "GetHashCode").WithArguments("B.GetHashCode()", "A.GetHashCode()").WithLocation(3, 25) @@ -11392,10 +11368,7 @@ public record B : A { comp.VerifyEmitDiagnostics( // (2,15): error CS0508: 'B.GetHashCode()': return type must be 'bool' to match overridden member 'A.GetHashCode()' // public record B : A { - Diagnostic(ErrorCode.ERR_CantChangeReturnTypeOnOverride, "B").WithArguments("B.GetHashCode()", "A.GetHashCode()", "bool").WithLocation(2, 15), - // (2,15): warning CS0659: 'B' overrides Object.Equals(object o) but does not override Object.GetHashCode() - // public record B : A { - Diagnostic(ErrorCode.WRN_EqualsWithoutGetHashCode, "B").WithArguments("B").WithLocation(2, 15) + Diagnostic(ErrorCode.ERR_CantChangeReturnTypeOnOverride, "B").WithArguments("B.GetHashCode()", "A.GetHashCode()", "bool").WithLocation(2, 15) ); var source2 = @" @@ -11404,9 +11377,6 @@ public record B : A { }"; comp = CreateCompilationWithIL(new[] { source2, IsExternalInitTypeDefinition }, ilSource: ilSource, parseOptions: TestOptions.RegularPreview); comp.VerifyEmitDiagnostics( - // (2,15): warning CS0659: 'B' overrides Object.Equals(object o) but does not override Object.GetHashCode() - // public record B : A { - Diagnostic(ErrorCode.WRN_EqualsWithoutGetHashCode, "B").WithArguments("B").WithLocation(2, 15), // (3,25): error CS0508: 'B.GetHashCode()': return type must be 'bool' to match overridden member 'A.GetHashCode()' // public override int GetHashCode() => throw null; Diagnostic(ErrorCode.ERR_CantChangeReturnTypeOnOverride, "GetHashCode").WithArguments("B.GetHashCode()", "A.GetHashCode()", "bool").WithLocation(3, 25) @@ -11441,9 +11411,6 @@ public void ObjectGetHashCode_05() "; var comp = CreateCompilation(source); comp.VerifyEmitDiagnostics( - // (1,8): warning CS0659: 'A' overrides Object.Equals(object o) but does not override Object.GetHashCode() - // record A - Diagnostic(ErrorCode.WRN_EqualsWithoutGetHashCode, "A").WithArguments("A").WithLocation(1, 8), // (3,20): error CS8869: 'A.GetHashCode()' does not override expected method from 'object'. // public new int GetHashCode() => throw null; Diagnostic(ErrorCode.ERR_DoesNotOverrideMethodFromObject, "GetHashCode").WithArguments("A.GetHashCode()").WithLocation(3, 20) @@ -11463,18 +11430,12 @@ record B : A; "; var comp = CreateCompilation(source); comp.VerifyEmitDiagnostics( - // (1,8): warning CS0659: 'A' overrides Object.Equals(object o) but does not override Object.GetHashCode() - // record A - Diagnostic(ErrorCode.WRN_EqualsWithoutGetHashCode, "A").WithArguments("A").WithLocation(1, 8), // (3,27): error CS8869: 'A.GetHashCode()' does not override expected method from 'object'. // public static new int GetHashCode() => throw null; Diagnostic(ErrorCode.ERR_DoesNotOverrideMethodFromObject, "GetHashCode").WithArguments("A.GetHashCode()").WithLocation(3, 27), // (6,8): error CS0506: 'B.GetHashCode()': cannot override inherited member 'A.GetHashCode()' because it is not marked virtual, abstract, or override // record B : A; - Diagnostic(ErrorCode.ERR_CantOverrideNonVirtual, "B").WithArguments("B.GetHashCode()", "A.GetHashCode()").WithLocation(6, 8), - // (6,8): warning CS0659: 'B' overrides Object.Equals(object o) but does not override Object.GetHashCode() - // record B : A; - Diagnostic(ErrorCode.WRN_EqualsWithoutGetHashCode, "B").WithArguments("B").WithLocation(6, 8) + Diagnostic(ErrorCode.ERR_CantOverrideNonVirtual, "B").WithArguments("B.GetHashCode()", "A.GetHashCode()").WithLocation(6, 8) ); } @@ -11506,9 +11467,6 @@ public void ObjectGetHashCode_08() "; var comp = CreateCompilation(source); comp.VerifyEmitDiagnostics( - // (1,8): warning CS0659: 'A' overrides Object.Equals(object o) but does not override Object.GetHashCode() - // record A - Diagnostic(ErrorCode.WRN_EqualsWithoutGetHashCode, "A").WithArguments("A").WithLocation(1, 8), // (3,21): error CS8869: 'A.GetHashCode()' does not override expected method from 'object'. // public new void GetHashCode() => throw null; Diagnostic(ErrorCode.ERR_DoesNotOverrideMethodFromObject, "GetHashCode").WithArguments("A.GetHashCode()").WithLocation(3, 21) @@ -11756,9 +11714,6 @@ public record B : A { }"; var comp = CreateCompilationWithIL(new[] { source2, IsExternalInitTypeDefinition }, ilSource: ilSource, parseOptions: TestOptions.RegularPreview); comp.VerifyEmitDiagnostics( - // (2,15): warning CS0659: 'B' overrides Object.Equals(object o) but does not override Object.GetHashCode() - // public record B : A { - Diagnostic(ErrorCode.WRN_EqualsWithoutGetHashCode, "B").WithArguments("B").WithLocation(2, 15), // (3,23): error CS8869: 'B.GetHashCode()' does not override expected method from 'object'. // public override A GetHashCode() => default; Diagnostic(ErrorCode.ERR_DoesNotOverrideMethodFromObject, "GetHashCode").WithArguments("B.GetHashCode()").WithLocation(3, 23) @@ -11854,9 +11809,6 @@ public record B : A { }"; var comp = CreateCompilationWithIL(new[] { source2, IsExternalInitTypeDefinition }, ilSource: ilSource, parseOptions: TestOptions.RegularPreview); comp.VerifyEmitDiagnostics( - // (2,15): warning CS0659: 'B' overrides Object.Equals(object o) but does not override Object.GetHashCode() - // public record B : A { - Diagnostic(ErrorCode.WRN_EqualsWithoutGetHashCode, "B").WithArguments("B").WithLocation(2, 15), // (3,23): error CS8830: 'B.GetHashCode()': Target runtime doesn't support covariant return types in overrides. Return type must be 'A' to match overridden member 'A.GetHashCode()' // public override B GetHashCode() => default; Diagnostic(ErrorCode.ERR_RuntimeDoesNotSupportCovariantReturnsOfClasses, "GetHashCode").WithArguments("B.GetHashCode()", "A.GetHashCode()", "A").WithLocation(3, 23) @@ -13054,6 +13006,48 @@ public virtual bool Equals(A other) var comp = CreateCompilation(source, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseDll); comp.MakeTypeMissing(SpecialType.System_Boolean); comp.VerifyEmitDiagnostics( + // (2,1): error CS0518: Predefined type 'System.Boolean' is not defined or imported + // record A + Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, @"record A +{ + public virtual bool Equals(A other) + => throw null; + + System.Boolean System.IEquatable.Equals(A x) => throw null; +}").WithArguments("System.Boolean").WithLocation(2, 1), + // (2,1): error CS0518: Predefined type 'System.Boolean' is not defined or imported + // record A + Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, @"record A +{ + public virtual bool Equals(A other) + => throw null; + + System.Boolean System.IEquatable.Equals(A x) => throw null; +}").WithArguments("System.Boolean").WithLocation(2, 1), + // (2,1): error CS0518: Predefined type 'System.Boolean' is not defined or imported + // record A + Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, @"record A +{ + public virtual bool Equals(A other) + => throw null; + + System.Boolean System.IEquatable.Equals(A x) => throw null; +}").WithArguments("System.Boolean").WithLocation(2, 1), + // (2,1): error CS0518: Predefined type 'System.Boolean' is not defined or imported + // record A + Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, @"record A +{ + public virtual bool Equals(A other) + => throw null; + + System.Boolean System.IEquatable.Equals(A x) => throw null; +}").WithArguments("System.Boolean").WithLocation(2, 1), + // (2,8): error CS0518: Predefined type 'System.Boolean' is not defined or imported + // record A + Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "A").WithArguments("System.Boolean").WithLocation(2, 8), + // (2,8): error CS0518: Predefined type 'System.Boolean' is not defined or imported + // record A + Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "A").WithArguments("System.Boolean").WithLocation(2, 8), // (2,8): error CS0518: Predefined type 'System.Boolean' is not defined or imported // record A Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "A").WithArguments("System.Boolean").WithLocation(2, 8), @@ -14571,6 +14565,376 @@ record B : A; ); } + [Fact] + public void EqualityOperators_01() + { + var source = +@" +record A(int X) +{ + public virtual bool Equals(ref A other) + => throw null; + + static void Main() + { + Test(null, null); + Test(null, new A(0)); + Test(new A(1), new A(1)); + Test(new A(2), new A(3)); + Test(new A(4), new B(4, 5)); + Test(new B(6, 7), new B(6, 7)); + Test(new B(8, 9), new B(8, 10)); + } + + static void Test(A a1, A a2) + { + System.Console.WriteLine(""{0} {1} {2} {3}"", a1 == a2, a2 == a1, a1 != a2, a2 != a1); + } +} + +record B(int X, int Y) : A(X); +"; + var verifier = CompileAndVerify(source, expectedOutput: +@" +True True False False +False False True True +True True False False +False False True True +False False True True +True True False False +False False True True +").VerifyDiagnostics(); + + var comp = (CSharpCompilation)verifier.Compilation; + MethodSymbol op = comp.GetMembers("A." + WellKnownMemberNames.EqualityOperatorName).OfType().Single(); + Assert.Equal("System.Boolean A.op_Equality(A? r1, A? r2)", op.ToTestDisplayString()); + Assert.Equal(Accessibility.Public, op.DeclaredAccessibility); + Assert.True(op.IsStatic); + Assert.False(op.IsAbstract); + Assert.False(op.IsVirtual); + Assert.False(op.IsOverride); + Assert.False(op.IsSealed); + Assert.True(op.IsImplicitlyDeclared); + + op = comp.GetMembers("A." + WellKnownMemberNames.InequalityOperatorName).OfType().Single(); + Assert.Equal("System.Boolean A.op_Inequality(A? r1, A? r2)", op.ToTestDisplayString()); + Assert.Equal(Accessibility.Public, op.DeclaredAccessibility); + Assert.True(op.IsStatic); + Assert.False(op.IsAbstract); + Assert.False(op.IsVirtual); + Assert.False(op.IsOverride); + Assert.False(op.IsSealed); + Assert.True(op.IsImplicitlyDeclared); + + verifier.VerifyIL("bool A.op_Equality(A, A)", @" +{ + // Code size 19 (0x13) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: beq.s IL_0011 + IL_0004: ldarg.0 + IL_0005: brfalse.s IL_000f + IL_0007: ldarg.0 + IL_0008: ldarg.1 + IL_0009: callvirt ""bool A.Equals(A)"" + IL_000e: ret + IL_000f: ldc.i4.0 + IL_0010: ret + IL_0011: ldc.i4.1 + IL_0012: ret +} +"); + + verifier.VerifyIL("bool A.op_Inequality(A, A)", @" + +{ + // Code size 11 (0xb) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: call ""bool A.op_Equality(A, A)"" + IL_0007: ldc.i4.0 + IL_0008: ceq + IL_000a: ret +} +"); + } + + [Fact] + public void EqualityOperators_02() + { + var source = +@" +record B; + +record A(int X) : B +{ + public virtual bool Equals(A other) + { + System.Console.WriteLine(""Equals(A other)""); + return base.Equals(other) && X == other.X; + } + + static void Main() + { + Test(null, null); + Test(null, new A(0)); + Test(new A(1), new A(1)); + Test(new A(2), new A(3)); + } + + static void Test(A a1, A a2) + { + System.Console.WriteLine(""{0} {1} {2} {3}"", a1 == a2, a2 == a1, a1 != a2, a2 != a1); + } +} +"; + var verifier = CompileAndVerify(source, expectedOutput: +@" +True True False False +Equals(A other) +Equals(A other) +False False True True +Equals(A other) +Equals(A other) +Equals(A other) +Equals(A other) +True True False False +Equals(A other) +Equals(A other) +Equals(A other) +Equals(A other) +False False True True +").VerifyDiagnostics(); + + var comp = (CSharpCompilation)verifier.Compilation; + MethodSymbol op = comp.GetMembers("A." + WellKnownMemberNames.EqualityOperatorName).OfType().Single(); + Assert.Equal("System.Boolean A.op_Equality(A? r1, A? r2)", op.ToTestDisplayString()); + Assert.Equal(Accessibility.Public, op.DeclaredAccessibility); + Assert.True(op.IsStatic); + Assert.False(op.IsAbstract); + Assert.False(op.IsVirtual); + Assert.False(op.IsOverride); + Assert.False(op.IsSealed); + Assert.True(op.IsImplicitlyDeclared); + + op = comp.GetMembers("A." + WellKnownMemberNames.InequalityOperatorName).OfType().Single(); + Assert.Equal("System.Boolean A.op_Inequality(A? r1, A? r2)", op.ToTestDisplayString()); + Assert.Equal(Accessibility.Public, op.DeclaredAccessibility); + Assert.True(op.IsStatic); + Assert.False(op.IsAbstract); + Assert.False(op.IsVirtual); + Assert.False(op.IsOverride); + Assert.False(op.IsSealed); + Assert.True(op.IsImplicitlyDeclared); + + verifier.VerifyIL("bool A.op_Equality(A, A)", @" +{ + // Code size 19 (0x13) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: beq.s IL_0011 + IL_0004: ldarg.0 + IL_0005: brfalse.s IL_000f + IL_0007: ldarg.0 + IL_0008: ldarg.1 + IL_0009: callvirt ""bool A.Equals(A)"" + IL_000e: ret + IL_000f: ldc.i4.0 + IL_0010: ret + IL_0011: ldc.i4.1 + IL_0012: ret +} +"); + + verifier.VerifyIL("bool A.op_Inequality(A, A)", @" + +{ + // Code size 11 (0xb) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: call ""bool A.op_Equality(A, A)"" + IL_0007: ldc.i4.0 + IL_0008: ceq + IL_000a: ret +} +"); + } + + [Fact] + public void EqualityOperators_03() + { + var source = +@" +record A +{ + public static bool operator==(A r1, A r2) + => throw null; + public static bool operator==(A r1, string r2) + => throw null; + public static bool operator!=(A r1, string r2) + => throw null; +} +"; + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (4,32): error CS0111: Type 'A' already defines a member called 'op_Equality' with the same parameter types + // public static bool operator==(A r1, A r2) + Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "==").WithArguments("op_Equality", "A").WithLocation(4, 32) + ); + } + + [Fact] + public void EqualityOperators_04() + { + var source = +@" +record A +{ + public static bool operator!=(A r1, A r2) + => throw null; + public static bool operator!=(string r1, A r2) + => throw null; + public static bool operator==(string r1, A r2) + => throw null; +} +"; + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (4,32): error CS0111: Type 'A' already defines a member called 'op_Inequality' with the same parameter types + // public static bool operator!=(A r1, A r2) + Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "!=").WithArguments("op_Inequality", "A").WithLocation(4, 32) + ); + } + + [Fact] + public void EqualityOperators_05() + { + var source = +@" +record A +{ + public static bool op_Equality(A r1, A r2) + => throw null; + public static bool op_Equality(string r1, A r2) + => throw null; +} +"; + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (4,24): error CS0111: Type 'A' already defines a member called 'op_Equality' with the same parameter types + // public static bool op_Equality(A r1, A r2) + Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "op_Equality").WithArguments("op_Equality", "A").WithLocation(4, 24) + ); + } + + [Fact] + public void EqualityOperators_06() + { + var source = +@" +record A +{ + public static bool op_Inequality(A r1, A r2) + => throw null; + public static bool op_Inequality(A r1, string r2) + => throw null; +} +"; + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (4,24): error CS0111: Type 'A' already defines a member called 'op_Inequality' with the same parameter types + // public static bool op_Inequality(A r1, A r2) + Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "op_Inequality").WithArguments("op_Inequality", "A").WithLocation(4, 24) + ); + } + + [Fact] + public void EqualityOperators_07() + { + var source = +@" +record A +{ + public static bool Equals(A other) + => throw null; +} +"; + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (2,8): error CS0736: 'A' does not implement interface member 'IEquatable.Equals(A)'. 'A.Equals(A)' cannot implement an interface member because it is static. + // record A + Diagnostic(ErrorCode.ERR_CloseUnimplementedInterfaceMemberStatic, "A").WithArguments("A", "System.IEquatable.Equals(A)", "A.Equals(A)").WithLocation(2, 8), + // (4,24): error CS8872: 'A.Equals(A)' must allow overriding because the containing record is not sealed. + // public static bool Equals(A other) + Diagnostic(ErrorCode.ERR_NotOverridableAPIInRecord, "Equals").WithArguments("A.Equals(A)").WithLocation(4, 24) + ); + } + + [Fact] + public void EqualityOperators_08() + { + var source = +@" +record A +{ + public virtual string Equals(A other) + => throw null; +} +"; + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (2,8): error CS0738: 'A' does not implement interface member 'IEquatable.Equals(A)'. 'A.Equals(A)' cannot implement 'IEquatable.Equals(A)' because it does not have the matching return type of 'bool'. + // record A + Diagnostic(ErrorCode.ERR_CloseUnimplementedInterfaceMemberWrongReturnType, "A").WithArguments("A", "System.IEquatable.Equals(A)", "A.Equals(A)", "bool").WithLocation(2, 8), + // (4,27): error CS8874: Record member 'A.Equals(A)' must return 'bool'. + // public virtual string Equals(A other) + Diagnostic(ErrorCode.ERR_SignatureMismatchInRecord, "Equals").WithArguments("A.Equals(A)", "bool").WithLocation(4, 27) + ); + } + + [Theory] + [CombinatorialData] + public void EqualityOperators_09(bool useImageReference) + { + var source1 = +@" +public record A(int X) +{ +} +"; + var comp1 = CreateCompilation(source1); + + var source2 = +@" +class Program +{ + static void Main() + { + Test(null, null); + Test(null, new A(0)); + Test(new A(1), new A(1)); + Test(new A(2), new A(3)); + } + + static void Test(A a1, A a2) + { + System.Console.WriteLine(""{0} {1} {2} {3}"", a1 == a2, a2 == a1, a1 != a2, a2 != a1); + } +} +"; + CompileAndVerify(source2, references: new[] { useImageReference ? comp1.EmitToImageReference() : comp1.ToMetadataReference() }, expectedOutput: +@" +True True False False +False False True True +True True False False +False False True True +").VerifyDiagnostics(); + } + [WorkItem(44692, "https://github.com/dotnet/roslyn/issues/44692")] [Fact] public void DuplicateProperty_01() @@ -17288,6 +17652,8 @@ record C : B; { "System.Type B.EqualityContract { get; }", "System.Type B.EqualityContract.get", + "System.Boolean B.op_Inequality(B? r1, B? r2)", + "System.Boolean B.op_Equality(B? r1, B? r2)", "System.Int32 B.GetHashCode()", "System.Boolean B.Equals(System.Object? obj)", "System.Boolean B.Equals(A? other)", @@ -17418,6 +17784,8 @@ static void Main() "System.Int32 B1.P.get", "void modreq(System.Runtime.CompilerServices.IsExternalInit) B1.P.init", "System.Int32 B1.P { get; init; }", + "System.Boolean B1.op_Inequality(B1? r1, B1? r2)", + "System.Boolean B1.op_Equality(B1? r1, B1? r2)", "System.Int32 B1.GetHashCode()", "System.Boolean B1.Equals(System.Object? obj)", "System.Boolean B1.Equals(A? other)", @@ -19412,6 +19780,12 @@ sealed static record R; // (2,22): error CS0708: 'R.EqualityContract': cannot declare instance members in a static class // sealed static record R; Diagnostic(ErrorCode.ERR_InstanceMemberInStaticClass, "R").WithArguments("R.EqualityContract").WithLocation(2, 22), + // (2,22): error CS0715: 'R.operator ==(R?, R?)': static classes cannot contain user-defined operators + // sealed static record R; + Diagnostic(ErrorCode.ERR_OperatorInStaticClass, "R").WithArguments("R.operator ==(R?, R?)").WithLocation(2, 22), + // (2,22): error CS0715: 'R.operator !=(R?, R?)': static classes cannot contain user-defined operators + // sealed static record R; + Diagnostic(ErrorCode.ERR_OperatorInStaticClass, "R").WithArguments("R.operator !=(R?, R?)").WithLocation(2, 22), // (2,22): error CS0708: 'GetHashCode': cannot declare instance members in a static class // sealed static record R; Diagnostic(ErrorCode.ERR_InstanceMemberInStaticClass, "R").WithArguments("GetHashCode").WithLocation(2, 22), @@ -19545,6 +19919,12 @@ public partial record C1 // (2,15): error CS0708: 'NV.EqualityContract': cannot declare instance members in a static class // static record NV Diagnostic(ErrorCode.ERR_InstanceMemberInStaticClass, "NV").WithArguments("NV.EqualityContract").WithLocation(2, 15), + // (2,15): error CS0715: 'NV.operator ==(NV?, NV?)': static classes cannot contain user-defined operators + // static record NV + Diagnostic(ErrorCode.ERR_OperatorInStaticClass, "NV").WithArguments("NV.operator ==(NV?, NV?)").WithLocation(2, 15), + // (2,15): error CS0715: 'NV.operator !=(NV?, NV?)': static classes cannot contain user-defined operators + // static record NV + Diagnostic(ErrorCode.ERR_OperatorInStaticClass, "NV").WithArguments("NV.operator !=(NV?, NV?)").WithLocation(2, 15), // (2,15): error CS0722: 'NV': static types cannot be used as return types // static record NV Diagnostic(ErrorCode.ERR_ReturnTypeIsStaticClass, "NV").WithArguments("NV").WithLocation(2, 15), @@ -19599,6 +19979,12 @@ static record R(int I) // (2,15): error CS0708: 'R.EqualityContract': cannot declare instance members in a static class // static record R(int I) Diagnostic(ErrorCode.ERR_InstanceMemberInStaticClass, "R").WithArguments("R.EqualityContract").WithLocation(2, 15), + // (2,15): error CS0715: 'R.operator ==(R?, R?)': static classes cannot contain user-defined operators + // static record R(int I) + Diagnostic(ErrorCode.ERR_OperatorInStaticClass, "R").WithArguments("R.operator ==(R?, R?)").WithLocation(2, 15), + // (2,15): error CS0715: 'R.operator !=(R?, R?)': static classes cannot contain user-defined operators + // static record R(int I) + Diagnostic(ErrorCode.ERR_OperatorInStaticClass, "R").WithArguments("R.operator !=(R?, R?)").WithLocation(2, 15), // (2,15): error CS0708: 'Deconstruct': cannot declare instance members in a static class // static record R(int I) Diagnostic(ErrorCode.ERR_InstanceMemberInStaticClass, "R").WithArguments("Deconstruct").WithLocation(2, 15), diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/Source/RecordTests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/Source/RecordTests.cs index 49b26f6790879..3335ad5576090 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/Source/RecordTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/Source/RecordTests.cs @@ -1076,6 +1076,8 @@ record C "System.String! C.Y { get; init; }", "System.String! C.Y.get", "void C.Y.init", + "System.Boolean C.operator !=(C? r1, C? r2)", + "System.Boolean C.operator ==(C? r1, C? r2)", "System.Int32 C.GetHashCode()", "System.Boolean C.Equals(System.Object? obj)", "System.Boolean C.Equals(C? other)", From 316c4459a3cc3acab28328169c6955b8260792a1 Mon Sep 17 00:00:00 2001 From: AlekseyTs Date: Mon, 3 Aug 2020 19:07:05 -0700 Subject: [PATCH 2/3] PR feedback --- .../Symbols/Source/SourceMemberContainerSymbol.cs | 2 +- .../Records/SynthesizedRecordEqualityOperator.cs | 4 ++-- .../SynthesizedRecordEqualityOperatorBase.cs | 2 +- .../SynthesizedRecordInequalityOperator.cs | 2 +- .../CSharp/Test/Semantic/Semantics/RecordTests.cs | 15 +++++++++++++++ 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs index 2e11d7e0a0687..3da0341866367 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs @@ -2069,7 +2069,7 @@ private void CheckForEqualityAndGetHashCode(DiagnosticBag diagnostics) if (IsRecord) { - // For records the warnings reported below are simply going to eco record specific errors, + // For records the warnings reported below are simply going to echo record specific errors, // producing more noise. return; } diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperator.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperator.cs index cf73d5834077f..1986cb1a34dee 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperator.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperator.cs @@ -16,7 +16,7 @@ namespace Microsoft.CodeAnalysis.CSharp.Symbols /// The record type includes synthesized '==' and '!=' operators equivalent to operators declared as follows: /// /// public static bool operator==(R? r1, R? r2) - /// => (object) r1 == r2 || (r1?.Equals(r2) ?? false); + /// => (object) r1 == r2 || ((object)r1 != null && r1.Equals(r2)); /// public static bool operator !=(R? r1, R? r2) /// => !(r1 == r2); /// @@ -36,7 +36,7 @@ internal override void GenerateMethodBody(TypeCompilationState compilationState, try { - // => (object)r1 == r2 || (r1?.Equals(r2) ?? false); + // => (object)r1 == r2 || ((object)r1 != null && r1.Equals(r2)); MethodSymbol? equals = null; foreach (var member in ContainingType.GetMembers(WellKnownMemberNames.ObjectEquals)) { diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperatorBase.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperatorBase.cs index 99a4b8003a666..d6004136ba931 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperatorBase.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperatorBase.cs @@ -17,7 +17,7 @@ namespace Microsoft.CodeAnalysis.CSharp.Symbols /// The record type includes synthesized '==' and '!=' operators equivalent to operators declared as follows: /// /// public static bool operator==(R? r1, R? r2) - /// => (object) r1 == r2 || (r1?.Equals(r2) ?? false); + /// => (object) r1 == r2 || ((object)r1 != null && r1.Equals(r2)); /// public static bool operator !=(R? r1, R? r2) /// => !(r1 == r2); /// diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordInequalityOperator.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordInequalityOperator.cs index 96913d8690876..3d1657801e025 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordInequalityOperator.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordInequalityOperator.cs @@ -17,7 +17,7 @@ namespace Microsoft.CodeAnalysis.CSharp.Symbols /// The record type includes synthesized '==' and '!=' operators equivalent to operators declared as follows: /// /// public static bool operator==(R? r1, R? r2) - /// => (object) r1 == r2 || (r1?.Equals(r2) ?? false); + /// => (object) r1 == r2 || ((object)r1 != null && r1.Equals(r2)); /// public static bool operator !=(R? r1, R? r2) /// => !(r1 == r2); /// diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs index 7fb6364845457..cbdc842a30e9d 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs @@ -14584,6 +14584,8 @@ static void Main() Test(new A(4), new B(4, 5)); Test(new B(6, 7), new B(6, 7)); Test(new B(8, 9), new B(8, 10)); + var a = new A(11); + Test(a, a); } static void Test(A a1, A a2) @@ -14603,6 +14605,7 @@ False False True True False False True True True True False False False False True True +True True False False ").VerifyDiagnostics(); var comp = (CSharpCompilation)verifier.Compilation; @@ -14682,12 +14685,20 @@ static void Main() Test(null, new A(0)); Test(new A(1), new A(1)); Test(new A(2), new A(3)); + var a = new A(11); + Test(a, a); + Test(new A(3), new B()); } static void Test(A a1, A a2) { System.Console.WriteLine(""{0} {1} {2} {3}"", a1 == a2, a2 == a1, a1 != a2, a2 != a1); } + + static void Test(A a1, B b2) + { + System.Console.WriteLine(""{0} {1} {2} {3}"", a1 == b2, b2 == a1, a1 != b2, b2 != a1); + } } "; var verifier = CompileAndVerify(source, expectedOutput: @@ -14706,6 +14717,10 @@ True True False False Equals(A other) Equals(A other) False False True True +True True False False +Equals(A other) +Equals(A other) +False False True True ").VerifyDiagnostics(); var comp = (CSharpCompilation)verifier.Compilation; From 66bfb89c67aa7a7e0426972d197f8a7a172bf139 Mon Sep 17 00:00:00 2001 From: AlekseyTs Date: Mon, 3 Aug 2020 19:43:03 -0700 Subject: [PATCH 3/3] Fix up doc comments --- .../Synthesized/Records/SynthesizedRecordEqualityOperator.cs | 2 +- .../Records/SynthesizedRecordEqualityOperatorBase.cs | 2 +- .../Synthesized/Records/SynthesizedRecordInequalityOperator.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperator.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperator.cs index 1986cb1a34dee..a564f7b4969a3 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperator.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperator.cs @@ -16,7 +16,7 @@ namespace Microsoft.CodeAnalysis.CSharp.Symbols /// The record type includes synthesized '==' and '!=' operators equivalent to operators declared as follows: /// /// public static bool operator==(R? r1, R? r2) - /// => (object) r1 == r2 || ((object)r1 != null && r1.Equals(r2)); + /// => (object) r1 == r2 || ((object)r1 != null && r1.Equals(r2)); /// public static bool operator !=(R? r1, R? r2) /// => !(r1 == r2); /// diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperatorBase.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperatorBase.cs index d6004136ba931..282a3bff75fbf 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperatorBase.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityOperatorBase.cs @@ -17,7 +17,7 @@ namespace Microsoft.CodeAnalysis.CSharp.Symbols /// The record type includes synthesized '==' and '!=' operators equivalent to operators declared as follows: /// /// public static bool operator==(R? r1, R? r2) - /// => (object) r1 == r2 || ((object)r1 != null && r1.Equals(r2)); + /// => (object) r1 == r2 || ((object)r1 != null && r1.Equals(r2)); /// public static bool operator !=(R? r1, R? r2) /// => !(r1 == r2); /// diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordInequalityOperator.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordInequalityOperator.cs index 3d1657801e025..1ef1cdb724d71 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordInequalityOperator.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordInequalityOperator.cs @@ -17,7 +17,7 @@ namespace Microsoft.CodeAnalysis.CSharp.Symbols /// The record type includes synthesized '==' and '!=' operators equivalent to operators declared as follows: /// /// public static bool operator==(R? r1, R? r2) - /// => (object) r1 == r2 || ((object)r1 != null && r1.Equals(r2)); + /// => (object) r1 == r2 || ((object)r1 != null && r1.Equals(r2)); /// public static bool operator !=(R? r1, R? r2) /// => !(r1 == r2); ///