Skip to content

Commit 392d3b0

Browse files
committed
add ApiListWriterOptions.WriterOptions.OmitCompilerGeneratedRecordEqualityMethods to improve generating record types
1 parent 19f75be commit 392d3b0

File tree

7 files changed

+259
-2
lines changed

7 files changed

+259
-2
lines changed

src/Smdn.Reflection.ReverseGenerating.ListApi.Core/Smdn.Reflection.ReverseGenerating.ListApi.Core.Common.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ SPDX-License-Identifier: MIT
2727
</PropertyGroup>
2828

2929
<ItemGroup>
30+
<Compile Include="$(MSBuildThisFileDirectory)Smdn.Reflection\*.cs" />
3031
<Compile Include="$(MSBuildThisFileDirectory)Smdn.Reflection.ReverseGenerating.ListApi\*.cs" />
3132
<Compile Include="$(MSBuildThisFileDirectory)..\Common\System.Runtime.CompilerServices\IsExternalInit.cs" />
3233
</ItemGroup>

src/Smdn.Reflection.ReverseGenerating.ListApi.Core/Smdn.Reflection.ReverseGenerating.ListApi.Core.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ SPDX-License-Identifier: MIT
1414
</PropertyGroup>
1515

1616
<ItemGroup>
17-
<ProjectOrPackageReference ReferencePackageVersion="[1.3.0,2.0.0)" Include="..\Smdn.Reflection.ReverseGenerating\Smdn.Reflection.ReverseGenerating.csproj" />
17+
<ProjectOrPackageReference ReferencePackageVersion="[1.4.0,2.0.0)" Include="..\Smdn.Reflection.ReverseGenerating\Smdn.Reflection.ReverseGenerating.csproj" />
1818
</ItemGroup>
1919
</Project>

src/Smdn.Reflection.ReverseGenerating.ListApi.Core/Smdn.Reflection.ReverseGenerating.ListApi/ApiListWriter.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,7 @@ private static string GenerateTypeContentDeclarations(
499499

500500
const BindingFlags MembersBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
501501

502+
var isRecord = options.TypeDeclaration.EnableRecordTypes && t.IsRecord();
502503
var members = t.GetMembers(MembersBindingFlags).OrderBy(static f => f.Name, StringComparer.Ordinal).ToList();
503504
var exceptingMembers = new List<MemberInfo>();
504505
var nestedTypes = new List<Type>();
@@ -516,6 +517,16 @@ private static string GenerateTypeContentDeclarations(
516517
exceptingMembers.Add(nestedType);
517518
nestedTypes.Add(nestedType);
518519
}
520+
521+
if (
522+
options.Writer.OmitCompilerGeneratedRecordEqualityMethods &&
523+
isRecord &&
524+
member is MethodInfo m &&
525+
m.IsCompilerGeneratedRecordEqualityMethod()
526+
) {
527+
// remove compiler-generated record equality methods
528+
exceptingMembers.Add(m);
529+
}
519530
}
520531

521532
var ret = new StringBuilder(1024);

src/Smdn.Reflection.ReverseGenerating.ListApi.Core/Smdn.Reflection.ReverseGenerating.ListApi/ApiListWriterOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ public class ApiListWriterOptions : GeneratorOptions {
77

88
public class WriterOptions {
99
public bool OrderStaticMembersFirst { get; set; } = false;
10+
public bool OmitCompilerGeneratedRecordEqualityMethods { get; set; } = false;
1011
public bool ThrowIfForwardedTypesCouldNotLoaded { get; set; } = false;
1112
public bool WriteNullableAnnotationDirective { get; set; } = true;
1213

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// SPDX-FileCopyrightText: 2025 smdn <smdn@smdn.jp>
2+
// SPDX-License-Identifier: MIT
3+
using System;
4+
using System.Reflection;
5+
6+
using Smdn.Reflection.Attributes;
7+
8+
using ROCType = Smdn.Reflection.ReverseGenerating.ListApi.ROCType;
9+
10+
namespace Smdn.Reflection;
11+
12+
internal static class MethodInfoRecordExtensions {
13+
internal static bool IsCompilerGeneratedRecordEqualityMethod(this MethodInfo m)
14+
{
15+
if (m is null)
16+
throw new ArgumentNullException(nameof(m));
17+
18+
if (m.GetAccessibility() != Accessibility.Public)
19+
return false;
20+
if (!m.HasCompilerGeneratedAttribute())
21+
return false;
22+
23+
var parameters = m.GetParameters();
24+
25+
// is "[CompilerGenerated] public bool Equals" ?
26+
if (
27+
string.Equals(m.Name, nameof(Equals), StringComparison.Ordinal) &&
28+
ROCType.Equals(m.ReturnType, typeof(bool))
29+
) {
30+
if (parameters.Length != 1)
31+
return false;
32+
33+
var firstParameterType = parameters[0].ParameterType;
34+
35+
// is "[CompilerGenerated] public bool Equals(object? obj)" ?
36+
if (ROCType.Equals(firstParameterType, typeof(object)))
37+
return true;
38+
39+
// is "[CompilerGenerated] public bool Equals(TRecord? obj)" ?
40+
if (firstParameterType == m.DeclaringType)
41+
return true;
42+
43+
// is "[CompilerGenerated] public bool Equals(TRecordBase? obj)" ?
44+
if (
45+
m.DeclaringType?.BaseType is Type baseType &&
46+
baseType.IsRecord() &&
47+
firstParameterType == baseType
48+
) {
49+
return true;
50+
}
51+
}
52+
53+
// is "[CompilerGenerated] public int GetHashCode()" ?
54+
if (
55+
string.Equals(m.Name, nameof(GetHashCode), StringComparison.Ordinal) &&
56+
ROCType.Equals(m.ReturnType, typeof(int)) &&
57+
parameters.Length == 0
58+
) {
59+
return true;
60+
}
61+
62+
// is "[CompilerGenerated] public static bool ??? (T left, T right)" ?
63+
if (
64+
m.IsStatic &&
65+
ROCType.Equals(m.ReturnType, typeof(bool)) &&
66+
parameters.Length == 2
67+
) {
68+
// is not "(TRecord left, TRecord right)" ?
69+
if (!(parameters[0].ParameterType == m.DeclaringType && parameters[1].ParameterType == m.DeclaringType))
70+
return false;
71+
72+
// [CompilerGenerated] public static bool operator ==(TRecord left, TRecord right)
73+
if (m.Name.Equals("op_Equality", StringComparison.Ordinal))
74+
return true;
75+
76+
// [CompilerGenerated] public static bool operator !=(TRecord left, TRecord right)
77+
if (m.Name.Equals("op_Inequality", StringComparison.Ordinal))
78+
return true;
79+
}
80+
81+
return false;
82+
}
83+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// SPDX-FileCopyrightText: 2025 smdn <smdn@smdn.jp>
2+
// SPDX-License-Identifier: MIT
3+
#nullable enable
4+
5+
using System;
6+
using System.IO;
7+
using System.Runtime.CompilerServices;
8+
9+
using NUnit.Framework;
10+
11+
using Smdn.IO;
12+
13+
namespace Smdn.Reflection.ReverseGenerating.ListApi;
14+
15+
#pragma warning disable IDE0040
16+
partial class ApiListWriterTests {
17+
#pragma warning restore IDE0040
18+
private void WriteExportedTypes_RecordTypes_OmitCompilerGeneratedRecordEqualityMethods(
19+
string sourceCode,
20+
string expectedMemberDeclaration,
21+
bool enableRecordTypes,
22+
bool omitCompilerGeneratedRecordEqualityMethods,
23+
bool shouldWritten
24+
)
25+
{
26+
var options = new ApiListWriterOptions();
27+
28+
options.TypeDeclaration.EnableRecordTypes = enableRecordTypes;
29+
options.Writer.OmitCompilerGeneratedRecordEqualityMethods = omitCompilerGeneratedRecordEqualityMethods;
30+
options.Writer.WriteHeader = false;
31+
options.Writer.WriteFooter = false;
32+
options.AttributeDeclaration.TypeFilter = static (attrType, _) => {
33+
return string.Equals(attrType.Name, nameof(CompilerGeneratedAttribute), StringComparison.Ordinal);
34+
};
35+
options.Indent = string.Empty;
36+
37+
var output = new StringReader(
38+
WriteApiListFromSourceCode(
39+
sourceCode,
40+
options,
41+
referenceAssemblyFileNames: [
42+
typeof(CompilerGeneratedAttribute).Assembly.GetName().Name + ".dll",
43+
typeof(IEquatable<>).Assembly.GetName().Name + ".dll",
44+
]
45+
)
46+
).ReadAllLines();
47+
48+
var joinedOutput = string.Join("\n", output);
49+
50+
//Console.WriteLine(joinedOutput);
51+
52+
if (shouldWritten)
53+
Assert.That(joinedOutput, Does.Contain(expectedMemberDeclaration));
54+
else
55+
Assert.That(joinedOutput, Does.Not.Contain(expectedMemberDeclaration));
56+
}
57+
58+
[TestCase("public int X { [CompilerGenerated] get; [CompilerGenerated] init; }", true, true)]
59+
[TestCase("public int X { [CompilerGenerated] get; [CompilerGenerated] init; }", false, true)]
60+
[TestCase("public R(int X) {}", true, true)]
61+
[TestCase("public R(int X) {}", false, true)]
62+
[TestCase("[CompilerGenerated]\npublic override bool Equals(object? obj) {}", true, false)]
63+
[TestCase("[CompilerGenerated]\npublic override bool Equals(object? obj) {}", false, true)]
64+
[TestCase("[CompilerGenerated]\npublic virtual bool Equals(R? other) {}", true, false)]
65+
[TestCase("[CompilerGenerated]\npublic virtual bool Equals(R? other) {}", false, true)]
66+
[TestCase("[CompilerGenerated]\npublic override int GetHashCode() {}", true, false)]
67+
[TestCase("[CompilerGenerated]\npublic override int GetHashCode() {}", false, true)]
68+
[TestCase("[CompilerGenerated]\npublic static bool operator == (R? left, R? right) {}", false, true)]
69+
[TestCase("[CompilerGenerated]\npublic static bool operator == (R? left, R? right) {}", true, false)]
70+
[TestCase("[CompilerGenerated]\npublic static bool operator != (R? left, R? right) {}", false, true)]
71+
[TestCase("[CompilerGenerated]\npublic static bool operator != (R? left, R? right) {}", true, false)]
72+
public void WriteExportedTypes_RecordTypes_OmitCompilerGeneratedRecordEqualityMethods(
73+
string expectedMemberDeclaration,
74+
bool omitCompilerGeneratedRecordEqualityMethods,
75+
bool shouldWritten
76+
)
77+
=> WriteExportedTypes_RecordTypes_OmitCompilerGeneratedRecordEqualityMethods(
78+
sourceCode: @"public record R(int X) {}",
79+
expectedMemberDeclaration: expectedMemberDeclaration,
80+
enableRecordTypes: true,
81+
omitCompilerGeneratedRecordEqualityMethods: omitCompilerGeneratedRecordEqualityMethods,
82+
shouldWritten: shouldWritten
83+
);
84+
85+
[TestCase("public int X { [CompilerGenerated] get; [CompilerGenerated] init; }", true, true)]
86+
[TestCase("public int X { [CompilerGenerated] get; [CompilerGenerated] init; }", false, true)]
87+
[TestCase("public R(int X) {}", true, true)]
88+
[TestCase("public R(int X) {}", false, true)]
89+
[TestCase("[CompilerGenerated]\npublic override bool Equals(object? obj) {}", true, true)]
90+
[TestCase("[CompilerGenerated]\npublic override bool Equals(object? obj) {}", false, true)]
91+
[TestCase("[CompilerGenerated]\npublic virtual bool Equals(R? other) {}", true, true)]
92+
[TestCase("[CompilerGenerated]\npublic virtual bool Equals(R? other) {}", false, true)]
93+
[TestCase("[CompilerGenerated]\npublic override int GetHashCode() {}", true, true)]
94+
[TestCase("[CompilerGenerated]\npublic override int GetHashCode() {}", false, true)]
95+
[TestCase("[CompilerGenerated]\npublic static bool operator == (R? left, R? right) {}", false, true)]
96+
[TestCase("[CompilerGenerated]\npublic static bool operator == (R? left, R? right) {}", true, true)]
97+
[TestCase("[CompilerGenerated]\npublic static bool operator != (R? left, R? right) {}", false, true)]
98+
[TestCase("[CompilerGenerated]\npublic static bool operator != (R? left, R? right) {}", true, true)]
99+
public void WriteExportedTypes_RecordTypes_OmitCompilerGeneratedRecordEqualityMethods_DisableRecordTypes(
100+
string expectedMemberDeclaration,
101+
bool omitCompilerGeneratedRecordEqualityMethods,
102+
bool shouldWritten
103+
)
104+
=> WriteExportedTypes_RecordTypes_OmitCompilerGeneratedRecordEqualityMethods(
105+
sourceCode: @"public record R(int X) {}",
106+
expectedMemberDeclaration: expectedMemberDeclaration,
107+
enableRecordTypes: false,
108+
omitCompilerGeneratedRecordEqualityMethods: omitCompilerGeneratedRecordEqualityMethods,
109+
shouldWritten: shouldWritten
110+
);
111+
112+
[TestCase("public virtual bool Equals(R? other) {}", true, true)]
113+
[TestCase("public virtual bool Equals(R? other) {}", false, true)]
114+
[TestCase("public override int GetHashCode() {}", true, true)]
115+
[TestCase("public override int GetHashCode() {}", false, true)]
116+
public void WriteExportedTypes_RecordTypes_OmitCompilerGeneratedRecordEqualityMethods_ExplicitlyImplemented(
117+
string expectedMemberDeclaration,
118+
bool omitCompilerGeneratedRecordEqualityMethods,
119+
bool shouldWritten
120+
)
121+
=> WriteExportedTypes_RecordTypes_OmitCompilerGeneratedRecordEqualityMethods(
122+
sourceCode: @"#nullable enable
123+
using System;
124+
125+
public record R(int X) {
126+
public virtual bool Equals(R? other) => throw new NotImplementedException();
127+
public override int GetHashCode() => throw new NotImplementedException();
128+
}",
129+
expectedMemberDeclaration: expectedMemberDeclaration,
130+
enableRecordTypes: true,
131+
omitCompilerGeneratedRecordEqualityMethods: omitCompilerGeneratedRecordEqualityMethods,
132+
shouldWritten: shouldWritten
133+
);
134+
135+
[TestCase("public int Y { [CompilerGenerated] get; [CompilerGenerated] init; }", true, true)]
136+
[TestCase("public int Y { [CompilerGenerated] get; [CompilerGenerated] init; }", false, true)]
137+
[TestCase("public RX(int X, int Y) {}", true, true)]
138+
[TestCase("public RX(int X, int Y) {}", false, true)]
139+
[TestCase("[CompilerGenerated]\npublic sealed override bool Equals(R? other) {}", true, false)]
140+
[TestCase("[CompilerGenerated]\npublic sealed override bool Equals(R? other) {}", false, true)]
141+
[TestCase("[CompilerGenerated]\npublic virtual bool Equals(RX? other) {}", true, false)]
142+
[TestCase("[CompilerGenerated]\npublic virtual bool Equals(RX? other) {}", false, true)]
143+
[TestCase("[CompilerGenerated]\npublic static bool operator == (RX? left, RX? right) {}", false, true)]
144+
[TestCase("[CompilerGenerated]\npublic static bool operator == (RX? left, RX? right) {}", true, false)]
145+
[TestCase("[CompilerGenerated]\npublic static bool operator != (RX? left, RX? right) {}", false, true)]
146+
[TestCase("[CompilerGenerated]\npublic static bool operator != (RX? left, RX? right) {}", true, false)]
147+
public void WriteExportedTypes_RecordTypes_OmitCompilerGeneratedRecordEqualityMethods_Derived(
148+
string expectedMemberDeclaration,
149+
bool omitCompilerGeneratedRecordEqualityMethods,
150+
bool shouldWritten
151+
)
152+
=> WriteExportedTypes_RecordTypes_OmitCompilerGeneratedRecordEqualityMethods(
153+
sourceCode: @"
154+
public record R(int X) {}
155+
public record RX(int X, int Y) : R(X) {}",
156+
expectedMemberDeclaration: expectedMemberDeclaration,
157+
enableRecordTypes: true,
158+
omitCompilerGeneratedRecordEqualityMethods: omitCompilerGeneratedRecordEqualityMethods,
159+
shouldWritten: shouldWritten
160+
);
161+
}

tests/Smdn.Reflection.ReverseGenerating.ListApi.Core/Smdn.Reflection.ReverseGenerating.ListApi/ApiListWriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
namespace Smdn.Reflection.ReverseGenerating.ListApi;
2323

2424
[TestFixture]
25-
class ApiListWriterTests {
25+
public partial class ApiListWriterTests {
2626
private ILogger? logger = null;
2727

2828
[OneTimeSetUp]

0 commit comments

Comments
 (0)