mirror of https://github.com/icsharpcode/ILSpy.git
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
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; |
|
} |
|
} |
|
} |
|
}
|
|
|