// 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 orderedMembers; readonly bool isInheritedRecord; readonly bool isStruct; readonly bool isSealed; readonly IMethod primaryCtor; readonly IType baseClass; readonly Dictionary backingFieldToAutoProperty = new Dictionary(); readonly Dictionary autoPropertyToBackingField = new Dictionary(); readonly Dictionary primaryCtorParameterToAutoPropertyOrBackingField = new Dictionary(); readonly Dictionary autoPropertyOrBackingFieldToPrimaryCtorParameter = new Dictionary(); 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) ?? false; this.isInheritedRecord = !isStruct && !(baseClass?.IsKnownType(KnownTypeCode.Object) ?? false); 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.IsStatic) return false; 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 (recordTypeDef.IsRecord) { if (!settings.UsePrimaryConstructorSyntax) return null; } else { if (!settings.UsePrimaryConstructorSyntaxForNonRecordTypes) return null; if (isStruct) 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; primaryCtorParameterToAutoPropertyOrBackingField.Clear(); autoPropertyOrBackingFieldToPrimaryCtorParameter.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].ReferenceKind is ReferenceKind.In or ReferenceKind.RefReadOnly) { 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; IMember backingMember; if (backingFieldToAutoProperty.TryGetValue(field, out var property)) { backingMember = property; } else if (!recordTypeDef.IsRecord) { backingMember = field; } else { return false; } primaryCtorParameterToAutoPropertyOrBackingField.Add(unspecializedMethod.Parameters[i], backingMember); autoPropertyOrBackingFieldToPrimaryCtorParameter.Add(backingMember, 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 DetectMemberOrder(ITypeDefinition recordTypeDef, Dictionary 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(); } /// /// Gets the fields and properties of the record type, interleaved as necessary to /// maintain Equals/ToString/etc. semantics. /// public IEnumerable FieldsAndProperties => orderedMembers; /// /// Gets the detected primary constructor. Returns null, if there was no primary constructor detected. /// public IMethod PrimaryConstructor => primaryCtor; public bool IsInheritedRecord => isInheritedRecord; bool IsRecordType(IType type) { return type.GetDefinition() == recordTypeDef && type.TypeArguments.SequenceEqual(recordTypeDef.TypeParameters); } /// /// Gets whether the member of the record type will be automatically generated by the compiler. /// public bool MethodIsGenerated(IMethod method) { if (!recordTypeDef.IsRecord) return false; 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 && baseClass != null && 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 "$" 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) { if (!recordTypeDef.IsRecord) return false; switch (property.Name) { case "EqualityContract" when !isStruct: return IsGeneratedEqualityContract(property); default: return IsPropertyDeclaredByPrimaryConstructor(property); } } internal bool FieldIsGenerated(IField field) { if (!settings.UsePrimaryConstructorSyntaxForNonRecordTypes) return false; var name = field.Name; return name.StartsWith("<", StringComparison.Ordinal) && name.EndsWith(">P", StringComparison.Ordinal) && field.IsCompilerGenerated(); } public bool IsPropertyDeclaredByPrimaryConstructor(IProperty property) { var subst = recordTypeDef.AsParameterizedType().GetSubstitution(); return primaryCtor != null && autoPropertyOrBackingFieldToPrimaryCtorParameter.ContainsKey((IProperty)property.Specialize(subst)); } internal (IProperty prop, IField field) GetPropertyInfoByPrimaryConstructorParameter(IParameter parameter) { var member = primaryCtorParameterToAutoPropertyOrBackingField[parameter]; if (member is IField field) return (null, field); return ((IProperty)member, autoPropertyToBackingField[(IProperty)member]); } internal IParameter GetPrimaryConstructorParameterFromBackingField(IField field) { return autoPropertyOrBackingFieldToPrimaryCtorParameter[field]; } 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 k__BackingField(ldloc this, ldfld 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().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().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.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.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().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 (baseClass != null && !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 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.Default.Equals(A, other.A) // callvirt Equals(call get_Default(), ldfld k__BackingField(ldloc this), ldfld 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 UnpackLogicAndChain(ILInstruction rootOfChain) { var result = new List(); 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.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer.Default.GetHashCode(A) ) * -1521134295 + EqualityComparer.Default.GetHashCode(B) ) * -1521134295 + EqualityComparer.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(); 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 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.ReferenceKind != ReferenceKind.Out) return false; IType ctorType = ctor.Type; if (ctor.ReferenceKind is ReferenceKind.In or ReferenceKind.RefReadOnly) 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.MetadataFile.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; } } } }