Browse Source

Records: detect when PrintMembers() is compiler-generated

Only for the simple case: record having only (auto-)properties but no fields and no inheritance.
pull/2251/head
Daniel Grunwald 4 years ago
parent
commit
500317a9e8
  1. 138
      ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs
  2. 2
      ILSpy/ILSpy.csproj
  3. 2
      ILSpy/Languages/CSharpLanguage.cs

138
ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs

@ -1,5 +1,23 @@ @@ -1,5 +1,23 @@

// Copyright (c) 2020 Daniel Grunwald
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection.Metadata;
@ -16,12 +34,29 @@ namespace ICSharpCode.Decompiler.CSharp @@ -16,12 +34,29 @@ namespace ICSharpCode.Decompiler.CSharp
readonly IDecompilerTypeSystem typeSystem;
readonly ITypeDefinition recordTypeDef;
readonly CancellationToken cancellationToken;
readonly List<IMember> orderedMembers;
public RecordDecompiler(IDecompilerTypeSystem dts, ITypeDefinition recordTypeDef, CancellationToken cancellationToken)
{
this.typeSystem = dts;
this.recordTypeDef = recordTypeDef;
this.cancellationToken = cancellationToken;
this.orderedMembers = DetectMemberOrder(recordTypeDef);
}
static List<IMember> DetectMemberOrder(ITypeDefinition recordTypeDef)
{
// For records, the order of members is important:
// Equals/GetHashCode/PrintMembers must agree on an order of fields+properties.
// The IL metadata has the order of fields and the order of properties, but we
// need to detect the correct interleaving.
// We could try to detect this from the PrintMembers body, but let's initially
// restrict ourselves to the common case where the record only uses properties.
if (recordTypeDef.Fields.All(f => f.Name.StartsWith("<", StringComparison.Ordinal) && f.Name.EndsWith("BackingField", StringComparison.Ordinal)))
{
return recordTypeDef.Properties.ToList<IMember>();
}
return null;
}
bool IsRecordType(IType type)
@ -69,6 +104,8 @@ namespace ICSharpCode.Decompiler.CSharp @@ -69,6 +104,8 @@ namespace ICSharpCode.Decompiler.CSharp
case "<Clone>$" when method.Parameters.Count == 0:
// Always generated; Method name cannot be expressed in C#
return true;
case "PrintMembers":
return IsGeneratedPrintMembers(method);
case "ToString" when method.Parameters.Count == 0:
return IsGeneratedToString(method);
default:
@ -122,10 +159,107 @@ namespace ICSharpCode.Decompiler.CSharp @@ -122,10 +159,107 @@ namespace ICSharpCode.Decompiler.CSharp
return false;
return IsRecordType(ty);
}
private bool IsGeneratedPrintMembers(IMethod method)
{
Debug.Assert(method.Name == "PrintMembers");
if (method.Parameters.Count != 1)
return false;
if (!method.IsOverridable)
return false;
if (method.GetAttributes().Any() || method.GetReturnTypeAttributes().Any())
return false;
if (orderedMembers == null)
return false;
var body = DecompileBody(method);
if (body == null)
return false;
var variables = body.Ancestors.OfType<ILFunction>().Single().Variables;
var builder = variables.Single(v => v.Kind == VariableKind.Parameter && v.Index == 0);
if (builder.Type.ReflectionName != "System.Text.StringBuilder")
return false;
int pos = 0;
bool needsComma = false;
foreach (var member in orderedMembers)
{
if (member.Name == "EqualityContract")
{
continue; // EqualityContract is never printed
}
/*
callvirt Append(ldloc builder, ldstr "A")
callvirt Append(ldloc builder, ldstr " = ")
callvirt Append(ldloc builder, constrained[System.Int32].callvirt ToString(addressof System.Int32(call get_A(ldloc this))))
callvirt Append(ldloc builder, ldstr ", ")
callvirt Append(ldloc builder, ldstr "B")
callvirt Append(ldloc builder, ldstr " = ")
callvirt Append(ldloc builder, constrained[System.Int32].callvirt ToString(ldflda B(ldloc this)))
leave IL_0000 (ldc.i4 1) */
if (!MatchStringBuilderAppendConstant(out string text))
return false;
string expectedText = (needsComma ? ", " : "") + member.Name + " = ";
if (text != expectedText)
return false;
if (!MatchStringBuilderAppend(body.Instructions[pos], builder, out var val))
return false;
if (val is CallInstruction { Method: { Name: "ToString", IsStatic: false } } toStringCall)
{
if (toStringCall.Arguments.Count != 1)
return false;
val = toStringCall.Arguments[0];
if (val is AddressOf addressOf)
{
val = addressOf.Value;
}
}
if (val is CallInstruction getterCall && member is IProperty property)
{
if (!getterCall.Method.MemberDefinition.Equals(property.Getter.MemberDefinition))
return false;
if (getterCall.Arguments.Count != 1)
return false;
if (!getterCall.Arguments[0].MatchLdThis())
return false;
}
else
{
return false;
}
pos++;
needsComma = true;
}
// leave IL_0000 (ldc.i4 1)
return body.Instructions[pos].MatchReturn(out var retVal)
&& retVal.MatchLdcI4(orderedMembers.Count > 0 ? 1 : 0);
bool MatchStringBuilderAppendConstant(out string text)
{
text = null;
while (MatchStringBuilderAppend(body.Instructions[pos], builder, out var val) && val.MatchLdStr(out string valText))
{
text += valText;
pos++;
}
return text != null;
}
}
private bool MatchStringBuilderAppend(ILInstruction inst, ILVariable sb, out ILInstruction val)
{
val = null;
if (!(inst is CallVirt { Method: { Name: "Append", DeclaringType: { Namespace: "System.Text", Name: "StringBuilder" } } } call))
return false;
if (call.Arguments.Count != 2)
return false;
if (!call.Arguments[0].MatchLdLoc(sb))
return false;
val = call.Arguments[1];
return true;
}
private bool IsGeneratedToString(IMethod method)
{
Debug.Assert(method.Name == "ToString");
Debug.Assert(method.Name == "ToString" && method.Parameters.Count == 0);
if (!method.IsOverride)
return false;
if (method.IsSealed)

2
ILSpy/ILSpy.csproj

@ -465,7 +465,7 @@ @@ -465,7 +465,7 @@
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Include="Properties\Resources.zh-Hans.resx" />
<EmbeddedResource Condition=" '$(COMPUTERNAME)' != 'DANIEL-E590' " Include="Properties\Resources.zh-Hans.resx" />
<Resource Include="Images\ILSpy.ico" />
<EmbeddedResource Include="TextView\CSharp-Mode.xshd" />
<EmbeddedResource Include="TextView\ILAsm-Mode.xshd" />

2
ILSpy/Languages/CSharpLanguage.cs

@ -110,7 +110,7 @@ namespace ICSharpCode.ILSpy @@ -110,7 +110,7 @@ namespace ICSharpCode.ILSpy
new LanguageVersion(Decompiler.CSharp.LanguageVersion.CSharp7_2.ToString(), "C# 7.2 / VS 2017.4"),
new LanguageVersion(Decompiler.CSharp.LanguageVersion.CSharp7_3.ToString(), "C# 7.3 / VS 2017.7"),
new LanguageVersion(Decompiler.CSharp.LanguageVersion.CSharp8_0.ToString(), "C# 8.0 / VS 2019"),
new LanguageVersion(Decompiler.CSharp.LanguageVersion.Preview.ToString(), "C# 9.0 (experimental)"),
new LanguageVersion(Decompiler.CSharp.LanguageVersion.Preview.ToString(), "C# 9.0 / VS 2019.8"),
};
}
return versions;

Loading…
Cancel
Save