Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Treat record structs as records #2009

Merged
merged 16 commits into from Jan 11, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 12 additions & 6 deletions Src/FluentAssertions/Common/TypeExtensions.cs
Expand Up @@ -584,12 +584,18 @@ private static bool IsAnonymousType(this Type type)
public static bool IsRecord(this Type type)
{
return TypeIsRecordCache.GetOrAdd(type, static t =>
t.GetMethod("<Clone>$") is not null &&
t.GetTypeInfo()
.DeclaredProperties
.FirstOrDefault(p => p.Name == "EqualityContract")?
.GetMethod?
.GetCustomAttribute(typeof(CompilerGeneratedAttribute)) is not null);
{
bool isRecord = t.GetMethod("<Clone>$") is not null &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 For readability I would prefer to clean-up the layout a little bit and keep an empty line between the two boolean statements
🤔 Any links to background material that we can add here for future reference?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No real background material actually, besides the link already posted in the linked issue (https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/record-structs#open-questions) where it is stated that recognizing record structs is an open point.
This pull request is based on common sense and heuristic testing, apparently giving good results but not supported by official documentation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would mention exactly using in-line comments

t.GetTypeInfo()
.DeclaredProperties
.FirstOrDefault(p => p.Name == "EqualityContract")?
.GetMethod?
.GetCustomAttribute(typeof(CompilerGeneratedAttribute)) is not null;
bool isRecordStruct = t.BaseType == typeof(ValueType) &&
t.GetMethods().Where(m => m.Name == "op_Inequality").SelectMany(m => m.GetCustomAttributes(typeof(CompilerGeneratedAttribute))).Any() &&
t.GetMethods().Where(m => m.Name == "op_Equality").SelectMany(m => m.GetCustomAttributes(typeof(CompilerGeneratedAttribute))).Any();
return isRecord || isRecordStruct;
});
}

private static bool IsKeyValuePair(Type type)
Expand Down
15 changes: 15 additions & 0 deletions Tests/FluentAssertions.Equivalency.Specs/RecordSpecs.cs
Expand Up @@ -16,6 +16,16 @@ public void When_the_subject_is_a_record_it_should_compare_it_by_its_members()
actual.Should().BeEquivalentTo(expected);
}

[Fact]
public void When_the_subject_is_a_readonly_record_struct_it_should_compare_it_by_its_members()
jnyrup marked this conversation as resolved.
Show resolved Hide resolved
{
var actual = new MyReadonlyRecordStruct("foo", new[] { "bar", "zip", "foo" });
dennisdoomen marked this conversation as resolved.
Show resolved Hide resolved

var expected = new MyReadonlyRecordStruct("foo", new[] { "bar", "zip", "foo" });

actual.Should().BeEquivalentTo(expected);
}

[Fact]
public void When_the_subject_is_a_record_it_should_mention_that_in_the_configuration_output()
{
Expand Down Expand Up @@ -90,4 +100,9 @@ private record MyRecord

public string[] CollectionProperty { get; init; }
}

private readonly record struct MyReadonlyRecordStruct(string StringField, string[] CollectionProperty)
{
public readonly string StringField = StringField;
}
}
66 changes: 66 additions & 0 deletions Tests/FluentAssertions.Specs/Types/TypeExtensionsSpecs.cs
@@ -1,4 +1,5 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using FluentAssertions.Common;
Expand Down Expand Up @@ -124,6 +125,33 @@ public void When_getting_fake_implicit_conversion_operator_from_a_type_with_fake
result.Should().NotBeNull();
}

[Theory]
[InlineData(typeof(MyRecord), true)]
[InlineData(typeof(MyRecordStruct), true)]
[InlineData(typeof(MyRecordStructWithOverriddenEquality), true)]
[InlineData(typeof(MyReadonlyRecordStruct), true)]
[InlineData(typeof(MyStruct), false)]
[InlineData(typeof(MyStructWithOverriddenEquality), false)]
[InlineData(typeof(MyClass), false)]
[InlineData(typeof(int), false)]
[InlineData(typeof(string), false)]
public void IsRecord_should_detect_records_correctly(Type type, bool expected)
dennisdoomen marked this conversation as resolved.
Show resolved Hide resolved
{
type.IsRecord().Should().Be(expected);
}

[Fact]
public void When_checking_if_anonymous_type_is_record_it_should_return_false()
{
new { Value = 42 }.GetType().IsRecord().Should().Be(false);
jnyrup marked this conversation as resolved.
Show resolved Hide resolved
}

[Fact]
public void When_checking_if_class_with_multiple_equality_methods_is_record_it_should_return_false()
{
typeof(ImmutableArray<int>).IsRecord().Should().Be(false);
jnyrup marked this conversation as resolved.
Show resolved Hide resolved
}

private static MethodInfo GetFakeConversionOperator(Type type, string name, BindingFlags bindingAttr, Type returnType)
{
MethodInfo[] methods = type.GetMethods(bindingAttr);
Expand Down Expand Up @@ -153,4 +181,42 @@ private TypeWithFakeConversionOperators(int value)
public static byte op_Explicit(TypeWithFakeConversionOperators typeWithFakeConversionOperators) => (byte)typeWithFakeConversionOperators.value;
#pragma warning restore SA1300, IDE1006
}

private record MyRecord(int Value);

private record struct MyRecordStruct(int Value);

private record struct MyRecordStructWithOverriddenEquality(int Value)
{
public bool Equals(MyRecordStructWithOverriddenEquality other) => Value == other.Value;

public override int GetHashCode() => Value;
}

private readonly record struct MyReadonlyRecordStruct(int Value);

private struct MyStruct
{
public int Value { get; set; }
}

private struct MyStructWithOverriddenEquality : IEquatable<MyStructWithOverriddenEquality>
{
public int Value { get; set; }

public bool Equals(MyStructWithOverriddenEquality other) => Value == other.Value;

public override bool Equals(object obj) => obj is MyStructWithOverriddenEquality other && Equals(other);

public override int GetHashCode() => Value;

public static bool operator ==(MyStructWithOverriddenEquality left, MyStructWithOverriddenEquality right) => left.Equals(right);

public static bool operator !=(MyStructWithOverriddenEquality left, MyStructWithOverriddenEquality right) => !left.Equals(right);
}

private class MyClass
{
public int Value { get; set; }
}
}