Browse Source

Merge pull request #2251 from icsharpcode/records

C# 9 Records
pull/2276/head
Daniel Grunwald 5 years ago committed by GitHub
parent
commit
174c14a7bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
  2. 6
      ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
  3. 89
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs
  4. 30
      ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
  5. 3
      ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs
  6. 4
      ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs
  7. 912
      ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs
  8. 8
      ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs
  9. 2
      ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs
  10. 23
      ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs
  11. 29
      ICSharpCode.Decompiler/DecompilerSettings.cs
  12. 1
      ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
  13. 5
      ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs
  14. 16
      ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs
  15. 36
      ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs
  16. 2
      ICSharpCode.Decompiler/TypeSystem/Implementation/MinimalCorlib.cs
  17. 17
      ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs
  18. 2
      ILSpy/ILSpy.csproj
  19. 2
      ILSpy/Languages/CSharpHighlightingTokenWriter.cs
  20. 2
      ILSpy/Languages/CSharpLanguage.cs
  21. 9
      ILSpy/Properties/Resources.Designer.cs
  22. 3
      ILSpy/Properties/Resources.resx

1
ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj

@ -105,6 +105,7 @@ @@ -105,6 +105,7 @@
<Compile Include="TestAssemblyResolver.cs" />
<Compile Include="TestCases\Correctness\DeconstructionTests.cs" />
<Compile Include="TestCases\Correctness\StringConcat.cs" />
<None Include="TestCases\Pretty\Records.cs" />
<Compile Include="TestCases\VBPretty\Issue2192.cs" />
<Compile Include="Util\FileUtilityTests.cs" />
<None Include="TestCases\Pretty\FunctionPointers.cs" />

6
ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs

@ -404,6 +404,12 @@ namespace ICSharpCode.Decompiler.Tests @@ -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)
{

89
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Records.cs

@ -0,0 +1,89 @@ @@ -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<A, B>
{
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<T> : Pair<T, T?> where T : struct
{
public bool Flag;
}
public abstract string AbstractProp {
get;
}
}
}
namespace System.Runtime.CompilerServices
{
internal class IsExternalInit
{
}
}

30
ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs

@ -18,14 +18,11 @@ @@ -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 @@ -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 @@ -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 @@ -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<IMember> fieldsAndProperties = recordDecompiler?.FieldsAndProperties
?? typeDef.Fields.Concat<IMember>(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 @@ -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));

3
ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs

@ -80,6 +80,9 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor @@ -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");
}

4
ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs

@ -1480,6 +1480,10 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor @@ -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;

912
ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs

@ -0,0 +1,912 @@ @@ -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<IMember> orderedMembers;
readonly bool isInheritedRecord;
readonly IType baseClass;
readonly Dictionary<IField, IProperty> backingFieldToAutoProperty = new Dictionary<IField, IProperty>();
readonly Dictionary<IProperty, IField> autoPropertyToBackingField = new Dictionary<IProperty, IField>();
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<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;
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 (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 "<Clone>$" when method.Parameters.Count == 0:
// Always generated; Method name cannot be expressed in C#
return true;
case "PrintMembers":
return IsGeneratedPrintMembers(method);
case "ToString" when method.Parameters.Count == 0:
return IsGeneratedToString(method);
default:
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 <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() || 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<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(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<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;
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<int>.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<ILFunction>().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<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 (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 (!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 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<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() || 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<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 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;
}
}
}
}

8
ICSharpCode.Decompiler/CSharp/Syntax/GeneralScope/TypeDeclaration.cs

@ -33,7 +33,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax @@ -33,7 +33,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
Class,
Struct,
Interface,
Enum
Enum,
/// <summary>
/// C# 9 'record'
/// </summary>
RecordClass,
}
/// <summary>
@ -63,6 +67,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax @@ -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;
}

2
ICSharpCode.Decompiler/CSharp/Syntax/Roles.cs

@ -87,7 +87,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax @@ -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");
}
}

23
ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs

@ -213,6 +213,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax @@ -213,6 +213,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
/// If disabled, emits "set /*init*/;" instead.
/// </summary>
public bool SupportInitAccessors { get; set; }
/// <summary>
/// Controls whether C# 9 "record" class types are supported.
/// </summary>
public bool SupportRecordClasses { get; set; }
#endregion
#region Convert Type
@ -1744,6 +1749,10 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax @@ -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 @@ -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<R>" in records
continue;
}
else

29
ICSharpCode.Decompiler/DecompilerSettings.cs

@ -135,12 +135,13 @@ namespace ICSharpCode.Decompiler @@ -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 @@ -175,7 +176,7 @@ namespace ICSharpCode.Decompiler
/// <summary>
/// Use C# 9 <c>nint</c>/<c>nuint</c> types.
/// </summary>
[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 @@ -193,7 +194,7 @@ namespace ICSharpCode.Decompiler
/// <summary>
/// Use C# 9 <c>init;</c> property accessors.
/// </summary>
[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 @@ -206,13 +207,31 @@ namespace ICSharpCode.Decompiler
}
}
bool recordClasses = true;
/// <summary>
/// Use C# 9 <c>record</c> classes.
/// </summary>
[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;
/// <summary>
/// Use C# 9 <c>delegate* unmanaged</c> types.
/// If this option is disabled, function pointers will instead be decompiled with type `IntPtr`.
/// </summary>
[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 @@ -610,7 +629,7 @@ namespace ICSharpCode.Decompiler
/// <summary>
/// Support GetEnumerator extension methods in foreach.
/// </summary>
[Category("C# 9.0 (experimental)")]
[Category("C# 9.0 / VS 2019.8")]
[Description("DecompilerSettings.DecompileForEachWithGetEnumeratorExtension")]
public bool ForEachWithGetEnumeratorExtension {
get { return forEachWithGetEnumeratorExtension; }

1
ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj

@ -66,6 +66,7 @@ @@ -66,6 +66,7 @@
<Compile Include="CSharp\ProjectDecompiler\IProjectInfoProvider.cs" />
<Compile Include="CSharp\ProjectDecompiler\ProjectFileWriterSdkStyle.cs" />
<Compile Include="CSharp\ProjectDecompiler\ProjectFileWriterDefault.cs" />
<Compile Include="CSharp\RecordDecompiler.cs" />
<Compile Include="CSharp\RequiredNamespaceCollector.cs" />
<Compile Include="CSharp\SequencePointBuilder.cs" />
<Compile Include="CSharp\ProjectDecompiler\TargetFramework.cs" />

5
ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs

@ -73,5 +73,10 @@ namespace ICSharpCode.Decompiler.TypeSystem @@ -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.
/// </summary>
Nullability NullableContext { get; }
/// <summary>
/// Gets whether the type has the necessary members to be considered a C# 9 record type.
/// </summary>
bool IsRecord { get; }
}
}

16
ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs

@ -244,7 +244,8 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation @@ -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 @@ -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

36
ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs

@ -24,11 +24,8 @@ using System.Reflection; @@ -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 @@ -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, "<Clone>$");
}
return opEquality & opInequality & clone;
}
#endregion
}
}

2
ICSharpCode.Decompiler/TypeSystem/Implementation/MinimalCorlib.cs

@ -302,6 +302,8 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation @@ -302,6 +302,8 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation
return EmptyList<IProperty>.Instance;
}
bool ITypeDefinition.IsRecord => false;
ITypeDefinition IType.GetDefinition() => this;
TypeParameterSubstitution IType.GetSubstitution() => TypeParameterSubstitution.Identity;

17
ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs

@ -616,5 +616,22 @@ namespace ICSharpCode.Decompiler.TypeSystem @@ -616,5 +616,22 @@ namespace ICSharpCode.Decompiler.TypeSystem
}
return null;
}
/// <summary>
/// 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.
/// </summary>
public static IType AsParameterizedType(this ITypeDefinition td)
{
if (td.TypeParameterCount == 0)
{
return td;
}
else
{
return new ParameterizedType(td, td.TypeArguments);
}
}
}
}

2
ILSpy/ILSpy.csproj

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

2
ILSpy/Languages/CSharpHighlightingTokenWriter.cs

@ -16,7 +16,6 @@ @@ -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 @@ -228,6 +227,7 @@ namespace ICSharpCode.ILSpy
case "class":
case "interface":
case "delegate":
case "record":
color = referenceTypeKeywordsColor;
break;
case "select":

2
ILSpy/Languages/CSharpLanguage.cs

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

9
ILSpy/Properties/Resources.Designer.cs generated

@ -1064,6 +1064,15 @@ namespace ICSharpCode.ILSpy.Properties { @@ -1064,6 +1064,15 @@ namespace ICSharpCode.ILSpy.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Records.
/// </summary>
public static string DecompilerSettings_RecordClasses {
get {
return ResourceManager.GetString("DecompilerSettings.RecordClasses", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Remove dead and side effect free code (use with caution!).
/// </summary>

3
ILSpy/Properties/Resources.resx

@ -384,6 +384,9 @@ Are you sure you want to continue?</value> @@ -384,6 +384,9 @@ Are you sure you want to continue?</value>
<data name="DecompilerSettings.ReadOnlyMethods" xml:space="preserve">
<value>Read-only methods</value>
</data>
<data name="DecompilerSettings.RecordClasses" xml:space="preserve">
<value>Records</value>
</data>
<data name="DecompilerSettings.RemoveDeadAndSideEffectFreeCodeUseWithCaution" xml:space="preserve">
<value>Remove dead and side effect free code (use with caution!)</value>
</data>

Loading…
Cancel
Save