diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index cdcab56dc..02b24a0d1 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -105,6 +105,7 @@ + diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index 3c674d65a..d0ab94223 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -404,6 +404,12 @@ namespace ICSharpCode.Decompiler.Tests RunForLibrary(cscOptions: cscOptions | CompilerOptions.Preview); } + [Test] + public void Records([ValueSource(nameof(roslynLatestOnlyOptions))] CompilerOptions cscOptions) + { + RunForLibrary(cscOptions: cscOptions | CompilerOptions.Preview); + } + [Test] public void NullPropagation([ValueSource(nameof(roslynOnlyOptions))] CompilerOptions cscOptions) { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs new file mode 100644 index 000000000..4afa52c18 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs @@ -0,0 +1,89 @@ +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty +{ + public record Empty + { + } + + public record Fields + { + public int A; + public double B = 1.0; + public object C; + public dynamic D; + public string S = "abc"; + } + + public record Pair + { + public A First { + get; + init; + } + public B Second { + get; + init; + } + } + + public record Properties + { + public int A { + get; + set; + } + public int B { + get; + } + public int C => 43; + public object O { + get; + set; + } + public string S { + get; + set; + } + public dynamic D { + get; + set; + } + + public Properties() + { + B = 42; + } + } + + public abstract record WithNestedRecords + { + public record A : WithNestedRecords + { + public override string AbstractProp => "A"; + } + + public record B : WithNestedRecords + { + public override string AbstractProp => "B"; + + public int? Value { + get; + set; + } + } + + public record DerivedGeneric : Pair where T : struct + { + public bool Flag; + } + + public abstract string AbstractProp { + get; + } + } +} +namespace System.Runtime.CompilerServices +{ + internal class IsExternalInit + { + } +} diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index b3bc01da1..02b15fdda 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -18,14 +18,11 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection.Metadata; -using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; -using System.Runtime.InteropServices; using System.Threading; using ICSharpCode.Decompiler.CSharp.OutputVisitor; @@ -415,6 +412,7 @@ namespace ICSharpCode.Decompiler.CSharp typeSystemAstBuilder.AddResolveResultAnnotations = true; typeSystemAstBuilder.UseNullableSpecifierForValueTypes = settings.LiftNullables; typeSystemAstBuilder.SupportInitAccessors = settings.InitAccessors; + typeSystemAstBuilder.SupportRecordClasses = settings.RecordClasses; return typeSystemAstBuilder; } @@ -1170,6 +1168,8 @@ namespace ICSharpCode.Decompiler.CSharp // e.g. DelegateDeclaration return entityDecl; } + bool isRecord = settings.RecordClasses && typeDef.IsRecord; + RecordDecompiler recordDecompiler = isRecord ? new RecordDecompiler(typeSystem, typeDef, CancellationToken) : null; foreach (var type in typeDef.NestedTypes) { if (!type.MetadataToken.IsNil && !MemberIsHidden(module.PEFile, type.MetadataToken, settings)) @@ -1179,20 +1179,28 @@ namespace ICSharpCode.Decompiler.CSharp typeDecl.Members.Add(nestedType); } } - foreach (var field in typeDef.Fields) + // With C# 9 records, the relative order of fields and properties matters: + IEnumerable fieldsAndProperties = recordDecompiler?.FieldsAndProperties + ?? typeDef.Fields.Concat(typeDef.Properties); + foreach (var fieldOrProperty in fieldsAndProperties) { - if (!field.MetadataToken.IsNil && !MemberIsHidden(module.PEFile, field.MetadataToken, settings)) + if (fieldOrProperty.MetadataToken.IsNil || MemberIsHidden(module.PEFile, fieldOrProperty.MetadataToken, settings)) + { + continue; + } + if (fieldOrProperty is IField field) { if (typeDef.Kind == TypeKind.Enum && !field.IsConst) continue; var memberDecl = DoDecompile(field, decompileRun, decompilationContext.WithCurrentMember(field)); typeDecl.Members.Add(memberDecl); } - } - foreach (var property in typeDef.Properties) - { - if (!property.MetadataToken.IsNil && !MemberIsHidden(module.PEFile, property.MetadataToken, settings)) + else if (fieldOrProperty is IProperty property) { + if (recordDecompiler?.PropertyIsGenerated(property) == true) + { + continue; + } var propDecl = DoDecompile(property, decompileRun, decompilationContext.WithCurrentMember(property)); typeDecl.Members.Add(propDecl); } @@ -1207,6 +1215,10 @@ namespace ICSharpCode.Decompiler.CSharp } foreach (var method in typeDef.Methods) { + if (recordDecompiler?.MethodIsGenerated(method) == true) + { + continue; + } if (!method.MetadataToken.IsNil && !MemberIsHidden(module.PEFile, method.MetadataToken, settings)) { var memberDecl = DoDecompile(method, decompileRun, decompilationContext.WithCurrentMember(method)); diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs index c966c2c87..f24f57489 100644 --- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs +++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs @@ -80,6 +80,9 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor case ClassType.Enum: writer.WriteKeyword(Roles.EnumKeyword, "enum"); break; + case ClassType.RecordClass: + writer.WriteKeyword(Roles.RecordKeyword, "record"); + break; default: throw new Exception("Invalid value for ClassType"); } diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs index c8bb5da81..7c3591f52 100644 --- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs @@ -1480,6 +1480,10 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor WriteKeyword(Roles.StructKeyword); braceStyle = policy.StructBraceStyle; break; + case ClassType.RecordClass: + WriteKeyword(Roles.RecordKeyword); + braceStyle = policy.ClassBraceStyle; + break; default: WriteKeyword(Roles.ClassKeyword); braceStyle = policy.ClassBraceStyle; diff --git a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs new file mode 100644 index 000000000..026268ab7 --- /dev/null +++ b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs @@ -0,0 +1,912 @@ +// 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 CancellationToken cancellationToken; + readonly List orderedMembers; + readonly bool isInheritedRecord; + readonly IType baseClass; + readonly Dictionary backingFieldToAutoProperty = new Dictionary(); + readonly Dictionary autoPropertyToBackingField = new Dictionary(); + + public RecordDecompiler(IDecompilerTypeSystem dts, ITypeDefinition recordTypeDef, CancellationToken cancellationToken) + { + this.typeSystem = dts; + this.recordTypeDef = recordTypeDef; + this.cancellationToken = cancellationToken; + this.baseClass = recordTypeDef.DirectBaseTypes.FirstOrDefault(b => b.Kind == TypeKind.Class); + this.isInheritedRecord = !baseClass.IsKnownType(KnownTypeCode.Object); + DetectAutomaticProperties(); + this.orderedMembers = DetectMemberOrder(recordTypeDef, backingFieldToAutoProperty); + } + + 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(); + } + } + + 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; + + 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 (method.IsConstructor && method.Parameters.Count == 1 + && IsRecordType(method.Parameters[0].Type)) + { + 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 "$" when method.Parameters.Count == 0: + // Always generated; Method name cannot be expressed in C# + return true; + case "PrintMembers": + return IsGeneratedPrintMembers(method); + case "ToString" when method.Parameters.Count == 0: + return IsGeneratedToString(method); + default: + return false; + } + } + + internal bool PropertyIsGenerated(IProperty property) + { + switch (property.Name) + { + case "EqualityContract": + return IsGeneratedEqualityContract(property); + 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() || method.GetReturnTypeAttributes().Any()) + return false; + if (method.Accessibility != Accessibility.Protected) + 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(property.Name == "EqualityContract"); + if (property.Accessibility != Accessibility.Protected) + return false; + if (!(property.IsVirtual || property.IsOverride)) + return false; + if (property.IsSealed) + return false; + var getter = property.Getter; + if (!(getter != null && !property.CanSet)) + return false; + if (property.GetAttributes().Any()) + return false; + if (getter.GetReturnTypeAttributes().Any()) + return false; + var 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 (!method.IsOverridable) + return false; + if (method.GetAttributes().Any() || method.GetReturnTypeAttributes().Any()) + return false; + if (method.Accessibility != Accessibility.Protected) + 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; + 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 (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() || 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 CallVirt { Method: { Name: "PrintMembers" } } printMembersCall)) + 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 MatchAppendCall(ILInstruction inst, out string val) + { + val = null; + 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; + return call.Arguments[1].MatchLdStr(out val); + } + + bool MatchAppendCallWithValue(ILInstruction inst, string val) + { + return MatchAppendCall(inst, out string tmp) && tmp == val; + } + } + + private bool IsGeneratedEquals(IMethod method) + { + // virtual bool Equals(R? other) { + // return 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 (!method.IsOverridable) + return false; + if (method.GetAttributes().Any() || method.GetReturnTypeAttributes().Any()) + return false; + if (orderedMembers == null) + return false; + var body = DecompileBody(method); + if (body == null) + return false; + if (!body.Instructions[0].MatchReturn(out var returnValue)) + 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)); + var conditions = UnpackLogicAndChain(returnValue); + Debug.Assert(conditions.Count >= 1); + int pos = 0; + 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 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 (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 (!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 static bool MatchGetEqualityContract(ILInstruction inst, out ILInstruction target) + { + target = null; + if (!(inst is CallVirt { Method: { Name: "get_EqualityContract" } } 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 (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() || 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; + 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 MatchMemberAccess(ILInstruction inst, out ILInstruction target, out IMember member) + { + target = null; + member = null; + if (inst is CallVirt + { + Method: + { + AccessorKind: System.Reflection.MethodSemanticsAttributes.Getter, + AccessorOwner: IProperty property + } + } 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; + } + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs b/ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs index cdc68010f..6a5bdfc75 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs @@ -33,7 +33,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax Class, Struct, Interface, - Enum + Enum, + /// + /// C# 9 'record' + /// + RecordClass, } /// @@ -63,6 +67,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax return GetChildByRole(Roles.InterfaceKeyword); case ClassType.Enum: return GetChildByRole(Roles.EnumKeyword); + case ClassType.RecordClass: + return GetChildByRole(Roles.RecordKeyword); default: return CSharpTokenNode.Null; } diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs index a3f9e69ab..10c9ab72c 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs @@ -87,7 +87,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax public static readonly TokenRole InterfaceKeyword = new TokenRole("interface"); public static readonly TokenRole StructKeyword = new TokenRole("struct"); public static readonly TokenRole ClassKeyword = new TokenRole("class"); + public static readonly TokenRole RecordKeyword = new TokenRole("record"); } } - diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs index ab385329f..1e04e5402 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs @@ -213,6 +213,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax /// If disabled, emits "set /*init*/;" instead. /// public bool SupportInitAccessors { get; set; } + + /// + /// Controls whether C# 9 "record" class types are supported. + /// + public bool SupportRecordClasses { get; set; } #endregion #region Convert Type @@ -1744,6 +1749,10 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax } default: classType = ClassType.Class; + if (SupportRecordClasses && typeDefinition.IsRecord) + { + classType = ClassType.RecordClass; + } break; } @@ -1774,22 +1783,30 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax { foreach (IType baseType in typeDefinition.DirectBaseTypes) { - // if the declared type is an enum, replace all references to System.Enum with the enum-underlying type if (typeDefinition.Kind == TypeKind.Enum && baseType.IsKnownType(KnownTypeCode.Enum)) { + // if the declared type is an enum, replace all references to System.Enum with the enum-underlying type if (!typeDefinition.EnumUnderlyingType.IsKnownType(KnownTypeCode.Int32)) { decl.BaseTypes.Add(ConvertType(typeDefinition.EnumUnderlyingType)); } - // if the declared type is a struct, ignore System.ValueType } else if ((typeDefinition.Kind == TypeKind.Struct || typeDefinition.Kind == TypeKind.Void) && baseType.IsKnownType(KnownTypeCode.ValueType)) { + // if the declared type is a struct, ignore System.ValueType continue; - // always ignore System.Object } else if (baseType.IsKnownType(KnownTypeCode.Object)) { + // always ignore System.Object + continue; + } + else if (SupportRecordClasses && typeDefinition.IsRecord + && baseType.Name == "IEquatable" && baseType.Namespace == "System" + && baseType.TypeArguments.Count == 1 + && baseType.TypeArguments[0].Equals(typeDefinition.AsParameterizedType())) + { + // omit "IEquatable" in records continue; } else diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index b7e714035..99e600a66 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -135,12 +135,13 @@ namespace ICSharpCode.Decompiler initAccessors = false; functionPointers = false; forEachWithGetEnumeratorExtension = false; + recordClasses = false; } } public CSharp.LanguageVersion GetMinimumRequiredVersion() { - if (nativeIntegers || initAccessors || functionPointers || forEachWithGetEnumeratorExtension) + if (nativeIntegers || initAccessors || functionPointers || forEachWithGetEnumeratorExtension || recordClasses) return CSharp.LanguageVersion.Preview; if (nullableReferenceTypes || readOnlyMethods || asyncEnumerator || asyncUsingAndForEachStatement || staticLocalFunctions || ranges || switchExpressions) @@ -175,7 +176,7 @@ namespace ICSharpCode.Decompiler /// /// Use C# 9 nint/nuint types. /// - [Category("C# 9.0 (experimental)")] + [Category("C# 9.0 / VS 2019.8")] [Description("DecompilerSettings.NativeIntegers")] public bool NativeIntegers { get { return nativeIntegers; } @@ -193,7 +194,7 @@ namespace ICSharpCode.Decompiler /// /// Use C# 9 init; property accessors. /// - [Category("C# 9.0 (experimental)")] + [Category("C# 9.0 / VS 2019.8")] [Description("DecompilerSettings.InitAccessors")] public bool InitAccessors { get { return initAccessors; } @@ -206,13 +207,31 @@ namespace ICSharpCode.Decompiler } } + bool recordClasses = true; + + /// + /// Use C# 9 record classes. + /// + [Category("C# 9.0 / VS 2019.8")] + [Description("DecompilerSettings.RecordClasses")] + public bool RecordClasses { + get { return recordClasses; } + set { + if (recordClasses != value) + { + recordClasses = value; + OnPropertyChanged(); + } + } + } + bool functionPointers = true; /// /// Use C# 9 delegate* unmanaged types. /// If this option is disabled, function pointers will instead be decompiled with type `IntPtr`. /// - [Category("C# 9.0 (experimental)")] + [Category("C# 9.0 / VS 2019.8")] [Description("DecompilerSettings.FunctionPointers")] public bool FunctionPointers { get { return functionPointers; } @@ -610,7 +629,7 @@ namespace ICSharpCode.Decompiler /// /// Support GetEnumerator extension methods in foreach. /// - [Category("C# 9.0 (experimental)")] + [Category("C# 9.0 / VS 2019.8")] [Description("DecompilerSettings.DecompileForEachWithGetEnumeratorExtension")] public bool ForEachWithGetEnumeratorExtension { get { return forEachWithGetEnumeratorExtension; } diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 6531f0678..429807535 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -66,6 +66,7 @@ + diff --git a/ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs b/ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs index e0e796cc9..4eb6b922c 100644 --- a/ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs +++ b/ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs @@ -73,5 +73,10 @@ namespace ICSharpCode.Decompiler.TypeSystem /// This serves as default nullability for members of the type that do not have a [Nullable] attribute. /// Nullability NullableContext { get; } + + /// + /// Gets whether the type has the necessary members to be considered a C# 9 record type. + /// + bool IsRecord { get; } } } diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs index 6de21c146..ca25a93c1 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs @@ -244,7 +244,8 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation case "NullableAttribute": return (options & TypeSystemOptions.NullabilityAnnotations) != 0; case "NullableContextAttribute": - return (options & TypeSystemOptions.NullabilityAnnotations) != 0 && (target == SymbolKind.TypeDefinition || target == SymbolKind.Method); + return (options & TypeSystemOptions.NullabilityAnnotations) != 0 + && (target == SymbolKind.TypeDefinition || IsMethodLike(target)); default: return false; } @@ -254,6 +255,19 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation return false; } } + + static bool IsMethodLike(SymbolKind kind) + { + return kind switch + { + SymbolKind.Method => true, + SymbolKind.Operator => true, + SymbolKind.Constructor => true, + SymbolKind.Destructor => true, + SymbolKind.Accessor => true, + _ => false + }; + } #endregion #region Security Attributes diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs index 4abfd390b..a6c41404e 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs @@ -24,11 +24,8 @@ using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Runtime.InteropServices; -using System.Text; -using System.Threading; using ICSharpCode.Decompiler.Metadata; -using ICSharpCode.Decompiler.Semantics; using ICSharpCode.Decompiler.Util; namespace ICSharpCode.Decompiler.TypeSystem.Implementation @@ -738,5 +735,38 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation return false; } #endregion + + #region IsRecord + byte isRecord = ThreeState.Unknown; + + public bool IsRecord { + get { + if (isRecord == ThreeState.Unknown) + { + isRecord = ThreeState.From(ComputeIsRecord()); + } + return isRecord == ThreeState.True; + } + } + + private bool ComputeIsRecord() + { + if (Kind != TypeKind.Class) + return false; + var metadata = module.metadata; + var typeDef = metadata.GetTypeDefinition(handle); + bool opEquality = false; + bool opInequality = false; + bool clone = false; + foreach (var methodHandle in typeDef.GetMethods()) + { + var method = metadata.GetMethodDefinition(methodHandle); + opEquality |= metadata.StringComparer.Equals(method.Name, "op_Equality"); + opInequality |= metadata.StringComparer.Equals(method.Name, "op_Inequality"); + clone |= metadata.StringComparer.Equals(method.Name, "$"); + } + return opEquality & opInequality & clone; + } + #endregion } } diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/MinimalCorlib.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MinimalCorlib.cs index 376f5c5f5..6db846f4d 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MinimalCorlib.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MinimalCorlib.cs @@ -302,6 +302,8 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation return EmptyList.Instance; } + bool ITypeDefinition.IsRecord => false; + ITypeDefinition IType.GetDefinition() => this; TypeParameterSubstitution IType.GetSubstitution() => TypeParameterSubstitution.Identity; diff --git a/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs b/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs index bf25900b6..993804d95 100644 --- a/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs +++ b/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs @@ -616,5 +616,22 @@ namespace ICSharpCode.Decompiler.TypeSystem } return null; } + + /// + /// When given a generic type definition, returns the self-parameterized type + /// (i.e. the type of "this" within the type definition). + /// When given a non-generic type definition, returns that definition unchanged. + /// + public static IType AsParameterizedType(this ITypeDefinition td) + { + if (td.TypeParameterCount == 0) + { + return td; + } + else + { + return new ParameterizedType(td, td.TypeArguments); + } + } } } diff --git a/ILSpy/ILSpy.csproj b/ILSpy/ILSpy.csproj index 1a96e92f4..9276682bf 100644 --- a/ILSpy/ILSpy.csproj +++ b/ILSpy/ILSpy.csproj @@ -465,7 +465,7 @@ PublicResXFileCodeGenerator Resources.Designer.cs - + diff --git a/ILSpy/Languages/CSharpHighlightingTokenWriter.cs b/ILSpy/Languages/CSharpHighlightingTokenWriter.cs index 20a480da8..bc1951943 100644 --- a/ILSpy/Languages/CSharpHighlightingTokenWriter.cs +++ b/ILSpy/Languages/CSharpHighlightingTokenWriter.cs @@ -16,7 +16,6 @@ // 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.Linq; @@ -228,6 +227,7 @@ namespace ICSharpCode.ILSpy case "class": case "interface": case "delegate": + case "record": color = referenceTypeKeywordsColor; break; case "select": diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 4d257d0d6..38e2ab6a4 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -110,7 +110,7 @@ namespace ICSharpCode.ILSpy new LanguageVersion(Decompiler.CSharp.LanguageVersion.CSharp7_2.ToString(), "C# 7.2 / VS 2017.4"), new LanguageVersion(Decompiler.CSharp.LanguageVersion.CSharp7_3.ToString(), "C# 7.3 / VS 2017.7"), new LanguageVersion(Decompiler.CSharp.LanguageVersion.CSharp8_0.ToString(), "C# 8.0 / VS 2019"), - new LanguageVersion(Decompiler.CSharp.LanguageVersion.Preview.ToString(), "C# 9.0 (experimental)"), + new LanguageVersion(Decompiler.CSharp.LanguageVersion.Preview.ToString(), "C# 9.0 / VS 2019.8"), }; } return versions; diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs index 3dc019586..0d9fbe478 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -1064,6 +1064,15 @@ namespace ICSharpCode.ILSpy.Properties { } } + /// + /// Looks up a localized string similar to Records. + /// + public static string DecompilerSettings_RecordClasses { + get { + return ResourceManager.GetString("DecompilerSettings.RecordClasses", resourceCulture); + } + } + /// /// Looks up a localized string similar to Remove dead and side effect free code (use with caution!). /// diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index c42d773c4..d814ac270 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -384,6 +384,9 @@ Are you sure you want to continue? Read-only methods + + Records + Remove dead and side effect free code (use with caution!)