.NET Decompiler with support for PDB generation, ReadyToRun, Metadata (&more) - cross-platform!
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1131 lines
38 KiB

// 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;
using System.Threading;
using ICSharpCode.Decompiler.IL;
using ICSharpCode.Decompiler.IL.Transforms;
using ICSharpCode.Decompiler.TypeSystem;
namespace ICSharpCode.Decompiler.CSharp
{
class RecordDecompiler
{
readonly IDecompilerTypeSystem typeSystem;
readonly ITypeDefinition recordTypeDef;
readonly DecompilerSettings settings;
readonly CancellationToken cancellationToken;
readonly List<IMember> orderedMembers;
readonly bool isInheritedRecord;
readonly bool isStruct;
readonly bool isSealed;
readonly IMethod primaryCtor;
readonly IType baseClass;
readonly Dictionary<IField, IProperty> backingFieldToAutoProperty = new Dictionary<IField, IProperty>();
readonly Dictionary<IProperty, IField> autoPropertyToBackingField = new Dictionary<IProperty, IField>();
readonly Dictionary<IParameter, IProperty> primaryCtorParameterToAutoProperty = new Dictionary<IParameter, IProperty>();
readonly Dictionary<IProperty, IParameter> autoPropertyToPrimaryCtorParameter = new Dictionary<IProperty, IParameter>();
public RecordDecompiler(IDecompilerTypeSystem dts, ITypeDefinition recordTypeDef, DecompilerSettings settings, CancellationToken cancellationToken)
{
this.typeSystem = dts;
this.recordTypeDef = recordTypeDef;
this.settings = settings;
this.cancellationToken = cancellationToken;
this.baseClass = recordTypeDef.DirectBaseTypes.FirstOrDefault(b => b.Kind == TypeKind.Class);
this.isStruct = baseClass.IsKnownType(KnownTypeCode.ValueType);
this.isInheritedRecord = !isStruct && !baseClass.IsKnownType(KnownTypeCode.Object);
this.isSealed = recordTypeDef.IsSealed;
DetectAutomaticProperties();
this.orderedMembers = DetectMemberOrder(recordTypeDef, backingFieldToAutoProperty);
this.primaryCtor = DetectPrimaryConstructor();
}
void DetectAutomaticProperties()
{
var subst = recordTypeDef.AsParameterizedType().GetSubstitution();
foreach (var property in recordTypeDef.Properties)
{
cancellationToken.ThrowIfCancellationRequested();
var p = (IProperty)property.Specialize(subst);
if (IsAutoProperty(p, out var field))
{
backingFieldToAutoProperty.Add(field, p);
autoPropertyToBackingField.Add(p, field);
}
}
bool IsAutoProperty(IProperty p, out IField field)
{
field = null;
if (p.Parameters.Count != 0)
return false;
if (p.Getter != null)
{
if (!IsAutoGetter(p.Getter, out field))
return false;
}
if (p.Setter != null)
{
if (!IsAutoSetter(p.Setter, out var field2))
return false;
if (field != null)
{
if (!field.Equals(field2))
return false;
}
else
{
field = field2;
}
}
if (field == null)
return false;
if (!IsRecordType(field.DeclaringType))
return false;
return field.Name == $"<{p.Name}>k__BackingField";
}
bool IsAutoGetter(IMethod method, out IField field)
{
field = null;
var body = DecompileBody(method);
if (body == null)
return false;
// return this.field;
if (!body.Instructions[0].MatchReturn(out var retVal))
return false;
if (method.IsStatic)
{
return retVal.MatchLdsFld(out field);
}
else
{
if (!retVal.MatchLdFld(out var target, out field))
return false;
return target.MatchLdThis();
}
}
bool IsAutoSetter(IMethod method, out IField field)
{
field = null;
Debug.Assert(!method.IsStatic);
var body = DecompileBody(method);
if (body == null)
return false;
// this.field = value;
ILInstruction valueInst;
if (method.IsStatic)
{
if (!body.Instructions[0].MatchStsFld(out field, out valueInst))
return false;
}
else
{
if (!body.Instructions[0].MatchStFld(out var target, out field, out valueInst))
return false;
if (!target.MatchLdThis())
return false;
}
if (!valueInst.MatchLdLoc(out var value))
return false;
if (!(value.Kind == VariableKind.Parameter && value.Index == 0))
return false;
return body.Instructions[1].MatchReturn(out var retVal) && retVal.MatchNop();
}
}
IMethod DetectPrimaryConstructor()
{
if (!settings.UsePrimaryConstructorSyntax)
return null;
var subst = recordTypeDef.AsParameterizedType().GetSubstitution();
foreach (var method in recordTypeDef.Methods)
{
cancellationToken.ThrowIfCancellationRequested();
if (method.IsStatic || !method.IsConstructor)
continue;
var m = method.Specialize(subst);
if (IsPrimaryConstructor(m, method))
return method;
primaryCtorParameterToAutoProperty.Clear();
autoPropertyToPrimaryCtorParameter.Clear();
}
return null;
bool IsPrimaryConstructor(IMethod method, IMethod unspecializedMethod)
{
Debug.Assert(method.IsConstructor);
var body = DecompileBody(method);
if (body == null)
return false;
if (method.Parameters.Count == 0)
return false;
var addonInst = isStruct ? 1 : 2;
if (body.Instructions.Count < method.Parameters.Count + addonInst)
return false;
for (int i = 0; i < method.Parameters.Count; i++)
{
if (!body.Instructions[i].MatchStFld(out var target, out var field, out var valueInst))
return false;
if (!target.MatchLdThis())
return false;
if (method.Parameters[i].IsIn)
{
if (!valueInst.MatchLdObj(out valueInst, out _))
return false;
}
if (!valueInst.MatchLdLoc(out var value))
return false;
if (!(value.Kind == VariableKind.Parameter && value.Index == i))
return false;
if (!backingFieldToAutoProperty.TryGetValue(field, out var property))
return false;
primaryCtorParameterToAutoProperty.Add(unspecializedMethod.Parameters[i], property);
autoPropertyToPrimaryCtorParameter.Add(property, unspecializedMethod.Parameters[i]);
}
if (!isStruct)
{
var baseCtorCall = body.Instructions.SecondToLastOrDefault() as CallInstruction;
if (baseCtorCall == null)
return false;
}
var returnInst = body.Instructions.LastOrDefault();
return returnInst != null && returnInst.MatchReturn(out var retVal) && retVal.MatchNop();
}
}
static List<IMember> DetectMemberOrder(ITypeDefinition recordTypeDef, Dictionary<IField, IProperty> backingFieldToAutoProperty)
{
// 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.
var subst = recordTypeDef.AsParameterizedType().GetSubstitution();
return recordTypeDef.Properties.Select(p => p.Specialize(subst)).Concat(
recordTypeDef.Fields.Select(f => (IField)f.Specialize(subst)).Where(f => !backingFieldToAutoProperty.ContainsKey(f))
).ToList();
}
/// <summary>
/// Gets the fields and properties of the record type, interleaved as necessary to
/// maintain Equals/ToString/etc. semantics.
/// </summary>
public IEnumerable<IMember> FieldsAndProperties => orderedMembers;
/// <summary>
/// Gets the detected primary constructor. Returns null, if there was no primary constructor detected.
/// </summary>
public IMethod PrimaryConstructor => primaryCtor;
public bool IsInheritedRecord => isInheritedRecord;
bool IsRecordType(IType type)
{
return type.GetDefinition() == recordTypeDef
&& type.TypeArguments.SequenceEqual(recordTypeDef.TypeParameters);
}
/// <summary>
/// Gets whether the member of the record type will be automatically generated by the compiler.
/// </summary>
public bool MethodIsGenerated(IMethod method)
{
if (IsCopyConstructor(method))
{
return IsGeneratedCopyConstructor(method);
}
switch (method.Name)
{
// Some members in records are always compiler-generated and lead to a
// "duplicate definition" error if we emit the generated code.
case "op_Equality":
case "op_Inequality":
{
// Don't emit comparison operators into C# record definition
// Note: user can declare additional operator== as long as they have
// different parameter types.
return method.Parameters.Count == 2
&& method.Parameters.All(p => IsRecordType(p.Type));
}
case "Equals" when method.Parameters.Count == 1:
{
IType paramType = method.Parameters[0].Type;
if (paramType.IsKnownType(KnownTypeCode.Object) && method.IsOverride)
{
// override bool Equals(object? obj): always generated
return true;
}
else if (IsRecordType(paramType))
{
// virtual bool Equals(R? other): generated unless user-declared
return IsGeneratedEquals(method);
}
else if (isInheritedRecord && NormalizeTypeVisitor.TypeErasure.EquivalentTypes(paramType, baseClass) && method.IsOverride)
{
// override bool Equals(BaseClass? obj): always generated
return true;
}
else
{
return false;
}
}
case "GetHashCode":
return IsGeneratedGetHashCode(method);
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);
case "Deconstruct" when primaryCtor != null && method.Parameters.Count == primaryCtor.Parameters.Count:
return IsGeneratedDeconstruct(method);
default:
return false;
}
}
internal bool PropertyIsGenerated(IProperty property)
{
switch (property.Name)
{
case "EqualityContract" when !isStruct:
return IsGeneratedEqualityContract(property);
default:
return IsPropertyDeclaredByPrimaryConstructor(property);
}
}
public bool IsPropertyDeclaredByPrimaryConstructor(IProperty property)
{
var subst = recordTypeDef.AsParameterizedType().GetSubstitution();
return primaryCtor != null
&& autoPropertyToPrimaryCtorParameter.ContainsKey((IProperty)property.Specialize(subst));
}
internal (IProperty prop, IField field) GetPropertyInfoByPrimaryConstructorParameter(IParameter parameter)
{
var prop = primaryCtorParameterToAutoProperty[parameter];
return (prop, autoPropertyToBackingField[prop]);
}
public bool IsCopyConstructor(IMethod method)
{
if (method == null)
return false;
Debug.Assert(method.DeclaringTypeDefinition == recordTypeDef);
return method.IsConstructor
&& method.Parameters.Count == 1
&& IsRecordType(method.Parameters[0].Type);
}
private bool IsAllowedAttribute(IAttribute attribute)
{
switch (attribute.AttributeType.ReflectionName)
{
case "System.Runtime.CompilerServices.CompilerGeneratedAttribute":
return true;
default:
return false;
}
}
private bool IsGeneratedCopyConstructor(IMethod method)
{
/*
call BaseClass..ctor(ldloc this, ldloc original)
stfld <X>k__BackingField(ldloc this, ldfld <X>k__BackingField(ldloc original))
leave IL_0000 (nop)
*/
Debug.Assert(method.IsConstructor && method.Parameters.Count == 1);
if (method.GetAttributes().Any(attr => !IsAllowedAttribute(attr)) || method.GetReturnTypeAttributes().Any())
return false;
if (method.Accessibility != Accessibility.Protected && (!isSealed || method.Accessibility != Accessibility.Private))
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 other = variables.Single(v => v.Kind == VariableKind.Parameter && v.Index == 0);
Debug.Assert(IsRecordType(other.Type));
int pos = 0;
// First instruction is the base constructor call
if (!(body.Instructions[pos] is Call { Method: { IsConstructor: true } } baseCtorCall))
return false;
if (!object.Equals(baseCtorCall.Method.DeclaringType, baseClass))
return false;
if (baseCtorCall.Arguments.Count != (isInheritedRecord ? 2 : 1))
return false;
if (!baseCtorCall.Arguments[0].MatchLdThis())
return false;
if (isInheritedRecord)
{
if (!baseCtorCall.Arguments[1].MatchLdLoc(other))
return false;
}
pos++;
// Then all the fields are copied over
foreach (var member in orderedMembers)
{
if (!(member is IField field))
{
if (!autoPropertyToBackingField.TryGetValue((IProperty)member, out field))
continue;
}
if (pos >= body.Instructions.Count)
return false;
if (!body.Instructions[pos].MatchStFld(out var lhsTarget, out var lhsField, out var valueInst))
return false;
if (!lhsTarget.MatchLdThis())
return false;
if (!lhsField.Equals(field))
return false;
if (!valueInst.MatchLdFld(out var rhsTarget, out var rhsField))
return false;
if (!rhsTarget.MatchLdLoc(other))
return false;
if (!rhsField.Equals(field))
return false;
pos++;
}
return body.Instructions[pos] is Leave;
}
private bool IsGeneratedEqualityContract(IProperty property)
{
// Generated member:
// protected virtual Type EqualityContract {
// [CompilerGenerated] get => typeof(R);
// }
Debug.Assert(!isStruct && property.Name == "EqualityContract");
if (property.Accessibility != Accessibility.Protected && (!isSealed || property.Accessibility != Accessibility.Private))
return false;
if (!(isSealed || property.IsVirtual || property.IsOverride))
return false;
if (property.IsSealed)
return false;
var getter = property.Getter;
if (!(getter != null && !property.CanSet))
return false;
var attrs = property.GetAttributes().ToList();
switch (attrs.Count)
{
case 0:
// Roslyn 3.x does not emit a CompilerGeneratedAttribute on the property itself.
break;
case 1:
// Roslyn 4.4 started doing so.
if (!attrs[0].AttributeType.IsKnownType(KnownAttribute.CompilerGenerated))
return false;
break;
default:
return false;
}
if (getter.GetReturnTypeAttributes().Any())
return false;
attrs = getter.GetAttributes().ToList();
if (attrs.Count != 1)
return false;
if (!attrs[0].AttributeType.IsKnownType(KnownAttribute.CompilerGenerated))
return false;
var body = DecompileBody(getter);
if (body == null || body.Instructions.Count != 1)
return false;
if (!(body.Instructions.Single() is Leave leave))
return false;
// leave IL_0000 (call GetTypeFromHandle(ldtypetoken R))
if (!TransformExpressionTrees.MatchGetTypeFromHandle(leave.Value, out IType ty))
return false;
return IsRecordType(ty);
}
private bool IsGeneratedPrintMembers(IMethod method)
{
Debug.Assert(method.Name == "PrintMembers");
if (method.Parameters.Count != 1)
return false;
if (!isSealed && !method.IsOverridable)
return false;
if (method.GetAttributes().Any(attr => !IsAllowedAttribute(attr)) || method.GetReturnTypeAttributes().Any())
return false;
if (method.Accessibility != Accessibility.Protected && (!isSealed || method.Accessibility != Accessibility.Private))
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;
//Roslyn 4.0.0-3.final start to insert an call to RuntimeHelpers.EnsureSufficientExecutionStack()
if (!isStruct && !isInheritedRecord && body.Instructions[pos] is Call {
Arguments: { Count: 0 },
Method: { Name: "EnsureSufficientExecutionStack", DeclaringType: { Namespace: "System.Runtime.CompilerServices", Name: "RuntimeHelpers" } }
})
{
pos++;
}
if (isInheritedRecord)
{
// Special case: inherited record adding no new members
if (body.Instructions[pos].MatchReturn(out var returnValue)
&& IsBaseCall(returnValue) && !orderedMembers.Any(IsPrintedMember))
{
return true;
}
// if (call PrintMembers(ldloc this, ldloc builder)) Block IL_000f {
// callvirt Append(ldloc builder, ldstr ", ")
// }
if (!body.Instructions[pos].MatchIfInstruction(out var condition, out var trueInst))
return false;
if (!IsBaseCall(condition))
return false;
// trueInst = callvirt Append(ldloc builder, ldstr ", ")
trueInst = Block.Unwrap(trueInst);
if (!MatchStringBuilderAppend(trueInst, builder, out var val))
return false;
if (!(val.MatchLdStr(out string text) && text == ", "))
return false;
pos++;
bool IsBaseCall(ILInstruction inst)
{
if (!(inst is CallInstruction { Method: { Name: "PrintMembers" } } call))
return false;
if (call.Arguments.Count != 2)
return false;
if (!call.Arguments[0].MatchLdThis())
return false;
if (!call.Arguments[1].MatchLdLoc(builder))
return false;
return true;
}
}
bool needsComma = false;
foreach (var member in orderedMembers)
{
if (!IsPrintedMember(member))
continue;
cancellationToken.ThrowIfCancellationRequested();
/*
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;
}
}
else if (val is Box box)
{
if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(box.Type, member.ReturnType))
return false;
val = box.Argument;
}
if (val is CallInstruction getterCall && member is IProperty property)
{
if (!getterCall.Method.Equals(property.Getter))
return false;
if (getterCall.Arguments.Count != 1)
return false;
if (!getterCall.Arguments[0].MatchLdThis())
return false;
}
else if (val.MatchLdFld(out var target, out var field) || val.MatchLdFlda(out target, out field))
{
if (!target.MatchLdThis())
return false;
if (!field.Equals(member))
return false;
}
else
{
return false;
}
pos++;
needsComma = true;
}
// leave IL_0000 (ldc.i4 1)
return body.Instructions[pos].MatchReturn(out var retVal)
&& retVal.MatchLdcI4(needsComma ? 1 : 0);
bool IsPrintedMember(IMember member)
{
if (member.IsStatic)
{
return false; // static fields/properties are not printed
}
if (!isStruct && member.Name == "EqualityContract")
{
return false; // EqualityContract is never printed
}
if (member.IsExplicitInterfaceImplementation)
{
return false; // explicit interface impls are not printed
}
if (member.IsOverride)
{
return false; // override is not printed (again), the virtual base property was already printed
}
return true;
}
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" && method.Parameters.Count == 0);
if (!method.IsOverride)
return false;
if (method.IsSealed)
return false;
if (method.GetAttributes().Any(attr => !IsAllowedAttribute(attr)) || method.GetReturnTypeAttributes().Any())
return false;
var body = DecompileBody(method);
if (body == null)
return false;
// stloc stringBuilder(newobj StringBuilder..ctor())
if (!body.Instructions[0].MatchStLoc(out var stringBuilder, out var stringBuilderInit))
return false;
if (!(stringBuilderInit is NewObj { Arguments: { Count: 0 }, Method: { DeclaringTypeDefinition: { Name: "StringBuilder", Namespace: "System.Text" } } }))
return false;
// callvirt Append(ldloc stringBuilder, ldstr "R")
if (!MatchAppendCallWithValue(body.Instructions[1], recordTypeDef.Name))
return false;
// callvirt Append(ldloc stringBuilder, ldstr " { ")
if (!MatchAppendCallWithValue(body.Instructions[2], " { "))
return false;
// if (callvirt PrintMembers(ldloc this, ldloc stringBuilder)) { trueInst }
if (!body.Instructions[3].MatchIfInstruction(out var condition, out var trueInst))
return true;
if (!((condition is CallInstruction { Method: { Name: "PrintMembers" } } printMembersCall) &&
(condition is CallVirt || (isSealed && condition is Call))))
return false;
if (printMembersCall.Arguments.Count != 2)
return false;
if (!printMembersCall.Arguments[0].MatchLdThis())
return false;
if (!printMembersCall.Arguments[1].MatchLdLoc(stringBuilder))
return false;
// trueInst: callvirt Append(ldloc stringBuilder, ldstr " ")
if (!MatchAppendCallWithValue(Block.Unwrap(trueInst), " "))
return false;
// callvirt Append(ldloc stringBuilder, ldstr "}")
if (!MatchAppendCallWithValue(body.Instructions[4], "}"))
return false;
// leave IL_0000 (callvirt ToString(ldloc stringBuilder))
if (!(body.Instructions[5] is Leave leave))
return false;
if (!(leave.Value is CallVirt { Method: { Name: "ToString" } } toStringCall))
return false;
if (toStringCall.Arguments.Count != 1)
return false;
return toStringCall.Arguments[0].MatchLdLoc(stringBuilder);
bool MatchAppendCallWithValue(ILInstruction inst, string val)
{
if (!(inst is CallVirt { Method: { Name: "Append" } } call))
return false;
if (call.Arguments.Count != 2)
return false;
if (!call.Arguments[0].MatchLdLoc(stringBuilder))
return false;
//Roslyn 4.0.0-3.final start to use char for 1 length string
if (call.Method.Parameters[0].Type.IsKnownType(KnownTypeCode.Char))
{
return val != null && val.Length == 1 && call.Arguments[1].MatchLdcI4(val[0]);
}
return call.Arguments[1].MatchLdStr(out string val1) && val1 == val;
}
}
private bool IsGeneratedEquals(IMethod method)
{
// virtual bool Equals(R? other) {
// return other != null && EqualityContract == other.EqualityContract && EqualityComparer<int>.Default.Equals(A, other.A) && ...;
// }
// Starting with Roslyn 3.10, it's:
// virtual bool Equals(R? other) {
// return this == other || other != null && EqualityContract == other.EqualityContract && EqualityComparer<int>.Default.Equals(A, other.A) && ...;
// }
Debug.Assert(method.Name == "Equals" && method.Parameters.Count == 1);
if (method.Parameters.Count != 1)
return false;
if (!isSealed && !method.IsOverridable)
return false;
if (method.GetAttributes().Any(attr => !IsAllowedAttribute(attr)) || method.GetReturnTypeAttributes().Any())
return false;
if (orderedMembers == null)
return false;
var body = DecompileBody(method);
if (body == null)
return false;
if (!body.Instructions[0].MatchReturn(out var returnValue))
return false;
// special case for empty record struct; always returns true;
if (returnValue.MatchLdcI4(1))
return true;
var variables = body.Ancestors.OfType<ILFunction>().Single().Variables;
var other = variables.Single(v => v.Kind == VariableKind.Parameter && v.Index == 0);
Debug.Assert(IsRecordType(other.Type));
if (returnValue.MatchLogicOr(out var lhs, out var rhs))
{
// this == other || ...
if (!lhs.MatchCompEquals(out var compLeft, out var compRight))
return false;
if (!compLeft.MatchLdThis())
return false;
if (!compRight.MatchLdLoc(other))
return false;
returnValue = rhs;
}
var conditions = UnpackLogicAndChain(returnValue);
Debug.Assert(conditions.Count >= 1);
int pos = 0;
if (!isStruct)
{
if (isInheritedRecord)
{
// call BaseClass::Equals(ldloc this, ldloc other)
if (pos >= conditions.Count)
return false;
if (!(conditions[pos] is Call { Method: { Name: "Equals" } } call))
return false;
if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(call.Method.DeclaringType, baseClass))
return false;
if (call.Arguments.Count != 2)
return false;
if (!call.Arguments[0].MatchLdThis())
return false;
if (!call.Arguments[1].MatchLdLoc(other))
return false;
pos++;
}
else
{
// comp.o(ldloc other != ldnull)
if (pos >= conditions.Count)
return false;
if (!conditions[pos].MatchCompNotEqualsNull(out var arg))
return false;
if (!arg.MatchLdLoc(other))
return false;
pos++;
// call op_Equality(callvirt get_EqualityContract(ldloc this), callvirt get_EqualityContract(ldloc other))
// Special-cased because Roslyn isn't using EqualityComparer<T> here.
if (pos >= conditions.Count)
return false;
if (!(conditions[pos] is Call { Method: { IsOperator: true, Name: "op_Equality" } } opEqualityCall))
return false;
if (!opEqualityCall.Method.DeclaringType.IsKnownType(KnownTypeCode.Type))
return false;
if (opEqualityCall.Arguments.Count != 2)
return false;
if (!MatchGetEqualityContract(opEqualityCall.Arguments[0], out var target1))
return false;
if (!MatchGetEqualityContract(opEqualityCall.Arguments[1], out var target2))
return false;
if (!target1.MatchLdThis())
return false;
if (!target2.MatchLdLoc(other))
return false;
pos++;
}
}
foreach (var member in orderedMembers)
{
if (!MemberConsideredForEquality(member))
continue;
if (!isStruct && member.Name == "EqualityContract")
{
continue; // already special-cased
}
// EqualityComparer<int>.Default.Equals(A, other.A)
// callvirt Equals(call get_Default(), ldfld <A>k__BackingField(ldloc this), ldfld <A>k__BackingField(ldloc other))
if (pos >= conditions.Count)
return false;
if (!(conditions[pos] is CallVirt { Method: { Name: "Equals" } } equalsCall))
return false;
if (equalsCall.Arguments.Count != 3)
return false;
if (!IsEqualityComparerGetDefaultCall(equalsCall.Arguments[0], member.ReturnType))
return false;
if (!MatchMemberAccess(equalsCall.Arguments[1], out var target1, out var member1))
return false;
if (!MatchMemberAccess(equalsCall.Arguments[2], out var target2, out var member2))
return false;
if (!target1.MatchLdThis())
return false;
if (!member1.Equals(member))
return false;
if (!(isStruct ? target2.MatchLdLoca(other) : target2.MatchLdLoc(other)))
return false;
if (!member2.Equals(member))
return false;
pos++;
}
return pos == conditions.Count;
}
static List<ILInstruction> UnpackLogicAndChain(ILInstruction rootOfChain)
{
var result = new List<ILInstruction>();
Visit(rootOfChain);
return result;
void Visit(ILInstruction inst)
{
if (inst.MatchLogicAnd(out var lhs, out var rhs))
{
Visit(lhs);
Visit(rhs);
}
else
{
result.Add(inst);
}
}
}
private bool MatchGetEqualityContract(ILInstruction inst, out ILInstruction target)
{
target = null;
if (!(inst is CallInstruction { Method: { Name: "get_EqualityContract" } } call))
return false;
if (!(inst is CallVirt || (isSealed && inst is Call)))
return false;
if (call.Arguments.Count != 1)
return false;
target = call.Arguments[0];
return true;
}
private static bool IsEqualityComparerGetDefaultCall(ILInstruction inst, IType type)
{
if (!(inst is Call { Method: { Name: "get_Default", IsStatic: true } } call))
return false;
if (!(call.Method.DeclaringType is { Name: "EqualityComparer", Namespace: "System.Collections.Generic" }))
return false;
if (call.Method.DeclaringType.TypeArguments.Count != 1)
return false;
if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(call.Method.DeclaringType.TypeArguments[0], type))
return false;
return call.Arguments.Count == 0;
}
bool MemberConsideredForEquality(IMember member)
{
if (member.IsStatic)
return false;
if (member is IProperty property)
{
if (!isStruct && property.Name == "EqualityContract")
return !isInheritedRecord;
return autoPropertyToBackingField.ContainsKey(property);
}
else
{
return member is IField;
}
}
bool IsGeneratedGetHashCode(IMethod method)
{
/*
return (
(
EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(A)
) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(B)
) * -1521134295 + EqualityComparer<object>.Default.GetHashCode(C);
*/
Debug.Assert(method.Name == "GetHashCode");
if (method.Parameters.Count != 0)
return false;
if (!method.IsOverride || method.IsSealed)
return false;
if (method.GetAttributes().Any(attr => !IsAllowedAttribute(attr)) || method.GetReturnTypeAttributes().Any())
return false;
if (orderedMembers == null)
return false;
var body = DecompileBody(method);
if (body == null)
return false;
if (!body.Instructions[0].MatchReturn(out var returnValue))
return false;
// special case for empty record struct; always returns false;
if (returnValue.MatchLdcI4(0))
return true;
var hashedMembers = new List<IMember>();
bool foundBaseClassHash = false;
if (!Visit(returnValue))
return false;
if (foundBaseClassHash != isInheritedRecord)
return false;
return orderedMembers.Where(MemberConsideredForEquality).SequenceEqual(hashedMembers);
bool Visit(ILInstruction inst)
{
if (inst is BinaryNumericInstruction {
Operator: BinaryNumericOperator.Add,
CheckForOverflow: false,
Left: BinaryNumericInstruction {
Operator: BinaryNumericOperator.Mul,
CheckForOverflow: false,
Left: var left,
Right: LdcI4 { Value: -1521134295 }
},
Right: var right
})
{
if (!Visit(left))
return false;
return ProcessIndividualHashCode(right);
}
else
{
return ProcessIndividualHashCode(inst);
}
}
bool ProcessIndividualHashCode(ILInstruction inst)
{
// base.GetHashCode(): call GetHashCode(ldloc this)
if (inst is Call { Method: { Name: "GetHashCode" } } baseHashCodeCall)
{
if (baseHashCodeCall.Arguments.Count != 1)
return false;
if (!baseHashCodeCall.Arguments[0].MatchLdThis())
return false;
if (foundBaseClassHash || hashedMembers.Count > 0)
return false; // must be first
foundBaseClassHash = true;
return baseHashCodeCall.Method.DeclaringType.Equals(baseClass);
}
// callvirt GetHashCode(call get_Default(), callvirt get_EqualityContract(ldloc this))
// callvirt GetHashCode(call get_Default(), ldfld <A>k__BackingField(ldloc this)))
if (!(inst is CallVirt { Method: { Name: "GetHashCode" } } getHashCodeCall))
return false;
if (getHashCodeCall.Arguments.Count != 2)
return false;
// getHashCodeCall.Arguments[0] checked later
if (!MatchMemberAccess(getHashCodeCall.Arguments[1], out var target, out var member))
return false;
if (!target.MatchLdThis())
return false;
if (!IsEqualityComparerGetDefaultCall(getHashCodeCall.Arguments[0], member.ReturnType))
return false;
hashedMembers.Add(member);
return true;
}
}
bool IsGeneratedDeconstruct(IMethod method)
{
Debug.Assert(method.Name == "Deconstruct" && method.Parameters.Count == primaryCtor.Parameters.Count);
if (!method.ReturnType.IsKnownType(KnownTypeCode.Void))
return false;
for (int i = 0; i < method.Parameters.Count; i++)
{
var deconstruct = method.Parameters[i];
var ctor = primaryCtor.Parameters[i];
if (!deconstruct.IsOut)
return false;
IType ctorType = ctor.Type;
if (ctor.IsIn)
ctorType = ((ByReferenceType)ctorType).ElementType;
if (!ctorType.Equals(((ByReferenceType)deconstruct.Type).ElementType))
return false;
if (ctor.Name != deconstruct.Name)
return false;
}
var body = DecompileBody(method);
if (body == null || body.Instructions.Count != method.Parameters.Count + 1)
return false;
for (int i = 0; i < body.Instructions.Count - 1; i++)
{
// stobj T(ldloc parameter, call getter(ldloc this))
if (!body.Instructions[i].MatchStObj(out var targetInst, out var getter, out _))
return false;
if (!targetInst.MatchLdLoc(out var target))
return false;
if (!(target.Kind == VariableKind.Parameter && target.Index == i))
return false;
if (getter is not Call call || call.Arguments.Count != 1)
return false;
if (!call.Arguments[0].MatchLdThis())
return false;
if (!call.Method.IsAccessor)
return false;
var autoProperty = (IProperty)call.Method.AccessorOwner;
if (!autoPropertyToBackingField.ContainsKey(autoProperty))
return false;
}
var returnInst = body.Instructions.LastOrDefault();
return returnInst != null && returnInst.MatchReturn(out var retVal) && retVal.MatchNop();
}
bool MatchMemberAccess(ILInstruction inst, out ILInstruction target, out IMember member)
{
target = null;
member = null;
if (inst is CallInstruction {
Method: {
AccessorKind: System.Reflection.MethodSemanticsAttributes.Getter,
AccessorOwner: IProperty property
}
} call && (call is CallVirt || (isSealed && call is Call)))
{
if (call.Arguments.Count != 1)
return false;
target = call.Arguments[0];
member = property;
return true;
}
else if (inst.MatchLdFld(out target, out IField field))
{
if (backingFieldToAutoProperty.TryGetValue(field, out property))
member = property;
else
member = field;
return true;
}
else
{
return false;
}
}
Block DecompileBody(IMethod method)
{
if (method == null || method.MetadataToken.IsNil)
return null;
var metadata = typeSystem.MainModule.metadata;
var methodDefHandle = (MethodDefinitionHandle)method.MetadataToken;
var methodDef = metadata.GetMethodDefinition(methodDefHandle);
if (!methodDef.HasBody())
return null;
var genericContext = new GenericContext(
classTypeParameters: recordTypeDef.TypeParameters,
methodTypeParameters: null);
var body = typeSystem.MainModule.PEFile.Reader.GetMethodBody(methodDef.RelativeVirtualAddress);
var ilReader = new ILReader(typeSystem.MainModule);
var il = ilReader.ReadIL(methodDefHandle, body, genericContext, ILFunctionKind.TopLevelFunction, cancellationToken);
var settings = new DecompilerSettings(LanguageVersion.CSharp1);
var transforms = CSharpDecompiler.GetILTransforms();
// Remove the last couple transforms -- we don't need variable names etc. here
int lastBlockTransform = transforms.FindLastIndex(t => t is BlockILTransform);
transforms.RemoveRange(lastBlockTransform + 1, transforms.Count - (lastBlockTransform + 1));
// Use CombineExitsTransform so that "return other != null && ...;" is a single statement even in release builds
transforms.Add(new CombineExitsTransform());
il.RunTransforms(transforms,
new ILTransformContext(il, typeSystem, debugInfo: null, settings) {
CancellationToken = cancellationToken
});
if (il.Body is BlockContainer container)
{
return container.EntryPoint;
}
else if (il.Body is Block block)
{
return block;
}
else
{
return null;
}
}
}
}