Skip to content

Commit

Permalink
Add support for C# 12 primary constructors.
Browse files Browse the repository at this point in the history
  • Loading branch information
siegfriedpammer committed Aug 11, 2024
1 parent e9949df commit 2043e5d
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,24 +50,17 @@ void IDisposable.Dispose()
[Serializable]
[SpecialName]
[CompilationMapping(SourceConstructFlags.Closure)]
internal sealed class getSeq_00405 : GeneratedSequenceBase<int>
internal sealed class getSeq_00405(int pc, int current) : GeneratedSequenceBase<int>()
{
[DebuggerNonUserCode]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
[CompilerGenerated]
public int pc;
public int pc = pc;

[DebuggerNonUserCode]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
[CompilerGenerated]
public int current;

public getSeq_00405(int pc, int current)
{
this.pc = pc;
this.current = current;
base._002Ector();
}
public int current = current;

public override int GenerateNext(ref IEnumerable<int> next)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

// C:\Users\Siegfried\Documents\Visual Studio 2017\Projects\ConsoleApp13\ConsoleApplication1\bin\Release\ConsoleApplication1.exe
// ConsoleApplication1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// Global type: <Module>
Expand Down Expand Up @@ -51,24 +50,17 @@ void IDisposable.Dispose()
[Serializable]
[SpecialName]
[CompilationMapping(SourceConstructFlags.Closure)]
internal sealed class getSeq_00405 : GeneratedSequenceBase<int>
internal sealed class getSeq_00405(int pc, int current) : GeneratedSequenceBase<int>()
{
[DebuggerNonUserCode]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
[CompilerGenerated]
public int pc;
public int pc = pc;

[DebuggerNonUserCode]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
[CompilerGenerated]
public int current;

public getSeq_00405(int pc, int current)
{
this.pc = pc;
this.current = current;
base._002Ector();
}
public int current = current;

public override int GenerateNext(ref IEnumerable<int> next)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,58 @@ public class UnsafeFields
public unsafe static int StaticSizeOf = sizeof(SimpleStruct);
public unsafe int SizeOf = sizeof(SimpleStruct);
}


#if CS120
public class ClassWithPrimaryCtorUsingGlobalParameter(int a)
{
public void Print()
{
Console.WriteLine(a);
}
}

public class ClassWithPrimaryCtorUsingGlobalParameterAssignedToField(int a)
{
private readonly int a = a;

public void Print()
{
Console.WriteLine(a);
}
}

public class ClassWithPrimaryCtorUsingGlobalParameterAssignedToFieldAndUsedInMethod(int a)
{
#pragma warning disable CS9124 // Parameter is captured into the state of the enclosing type and its value is also used to initialize a field, property, or event.
private readonly int _a = a;
#pragma warning restore CS9124 // Parameter is captured into the state of the enclosing type and its value is also used to initialize a field, property, or event.

public void Print()
{
Console.WriteLine(a);
}
}

public class ClassWithPrimaryCtorUsingGlobalParameterAssignedToProperty(int a)
{
public int A { get; set; } = a;

public void Print()
{
Console.WriteLine(A);
}
}

public class ClassWithPrimaryCtorUsingGlobalParameterAssignedToEvent(EventHandler a)
{
public event EventHandler A = a;

public void Print()
{
Console.WriteLine(this.A);
}
}
#endif
}
}
63 changes: 41 additions & 22 deletions ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,8 @@ public static bool MemberIsHidden(MetadataFile module, EntityHandle member, Deco
{
if (settings.AnonymousMethods && IsAnonymousMethodCacheField(field, metadata))
return true;
if (settings.UsePrimaryConstructorSyntaxForNonRecordTypes && IsPrimaryConstructorParameterBackingField(field, metadata))
return true;
if (settings.AutomaticProperties && IsAutomaticPropertyBackingField(field, metadata, out var propertyName))
{
if (!settings.GetterOnlyAutomaticProperties && IsGetterOnlyProperty(propertyName))
Expand Down Expand Up @@ -390,6 +392,11 @@ bool IsGetterOnlyProperty(string propertyName)

return false;
}
static bool IsPrimaryConstructorParameterBackingField(SRM.FieldDefinition field, MetadataReader metadata)
{
var name = metadata.GetString(field.Name);
return name.StartsWith("<", StringComparison.Ordinal) && name.EndsWith(">P", StringComparison.Ordinal);
}

static bool IsSwitchOnStringCache(SRM.FieldDefinition field, MetadataReader metadata)
{
Expand Down Expand Up @@ -1303,12 +1310,12 @@ EntityDeclaration DoDecompile(ITypeDefinition typeDef, DecompileRun decompileRun
// e.g. DelegateDeclaration
return entityDecl;
}
bool isRecord = typeDef.Kind switch {
TypeKind.Class => settings.RecordClasses && typeDef.IsRecord,
TypeKind.Struct => settings.RecordStructs && typeDef.IsRecord,
bool isRecordLike = typeDef.Kind switch {
TypeKind.Class => (settings.RecordClasses && typeDef.IsRecord) || settings.UsePrimaryConstructorSyntaxForNonRecordTypes,
TypeKind.Struct => (settings.RecordStructs && typeDef.IsRecord) || settings.UsePrimaryConstructorSyntaxForNonRecordTypes,
_ => false,
};
RecordDecompiler recordDecompiler = isRecord ? new RecordDecompiler(typeSystem, typeDef, settings, CancellationToken) : null;
RecordDecompiler recordDecompiler = isRecordLike ? new RecordDecompiler(typeSystem, typeDef, settings, CancellationToken) : null;
if (recordDecompiler != null)
decompileRun.RecordDecompilers.Add(typeDef, recordDecompiler);

Expand All @@ -1318,33 +1325,41 @@ EntityDeclaration DoDecompile(ITypeDefinition typeDef, DecompileRun decompileRun
{
ParameterDeclaration pd = typeSystemAstBuilder.ConvertParameter(p);
(IProperty prop, IField field) = recordDecompiler.GetPropertyInfoByPrimaryConstructorParameter(p);
Syntax.Attribute[] attributes = prop.GetAttributes().Select(attr => typeSystemAstBuilder.ConvertAttribute(attr)).ToArray();
if (attributes.Length > 0)

if (prop != null)
{
var section = new AttributeSection {
AttributeTarget = "property"
};
section.Attributes.AddRange(attributes);
pd.Attributes.Add(section);
var attributes = prop?.GetAttributes().Select(attr => typeSystemAstBuilder.ConvertAttribute(attr)).ToArray();
if (attributes?.Length > 0)
{
var section = new AttributeSection {
AttributeTarget = "property"
};
section.Attributes.AddRange(attributes);
pd.Attributes.Add(section);
}
}
attributes = field.GetAttributes()
.Where(a => !PatternStatementTransform.attributeTypesToRemoveFromAutoProperties.Contains(a.AttributeType.FullName))
.Select(attr => typeSystemAstBuilder.ConvertAttribute(attr)).ToArray();
if (attributes.Length > 0)
if (field != null && (recordDecompiler.FieldIsGenerated(field) || typeDef.IsRecord))
{
var section = new AttributeSection {
AttributeTarget = "field"
};
section.Attributes.AddRange(attributes);
pd.Attributes.Add(section);
var attributes = field.GetAttributes()
.Where(a => !PatternStatementTransform.attributeTypesToRemoveFromAutoProperties.Contains(a.AttributeType.FullName))
.Select(attr => typeSystemAstBuilder.ConvertAttribute(attr)).ToArray();
if (attributes.Length > 0)
{
var section = new AttributeSection {
AttributeTarget = "field"
};
section.Attributes.AddRange(attributes);
pd.Attributes.Add(section);
}
}
typeDecl.PrimaryConstructorParameters.Add(pd);
}
}

// With C# 9 records, the relative order of fields and properties matters:
IEnumerable<IMember> fieldsAndProperties = recordDecompiler?.FieldsAndProperties
?? typeDef.Fields.Concat<IMember>(typeDef.Properties);
IEnumerable<IMember> fieldsAndProperties = isRecordLike && typeDef.IsRecord
? recordDecompiler.FieldsAndProperties
: typeDef.Fields.Concat<IMember>(typeDef.Properties);

// For COM interop scenarios, the relative order of virtual functions/properties matters:
IEnumerable<IMember> allOrderedMembers = RequiresNativeOrdering(typeDef) ? GetMembersWithNativeOrdering(typeDef) :
Expand Down Expand Up @@ -1481,6 +1496,10 @@ void DoDecompileMember(IEntity entity, RecordDecompiler recordDecompiler, Partia
{
return;
}
if (recordDecompiler?.FieldIsGenerated(field) == true)
{
return;
}
entityDecl = DoDecompile(field, decompileRun, decompilationContext.WithCurrentMember(field));
entityMap.Add(field, entityDecl);
break;
Expand Down
70 changes: 57 additions & 13 deletions ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ class RecordDecompiler
readonly IType baseClass;
readonly Dictionary<IField, IProperty> backingFieldToAutoProperty = new Dictionary<IField, IProperty>();
readonly Dictionary<IProperty, IField> autoPropertyToBackingField = new Dictionary<IProperty, IField>();
readonly Dictionary<IParameter, IProperty> primaryCtorParameterToAutoProperty = new Dictionary<IParameter, IProperty>();
readonly Dictionary<IProperty, IParameter> autoPropertyToPrimaryCtorParameter = new Dictionary<IProperty, IParameter>();
readonly Dictionary<IParameter, IMember> primaryCtorParameterToAutoPropertyOrBackingField = new Dictionary<IParameter, IMember>();
readonly Dictionary<IMember, IParameter> autoPropertyOrBackingFieldToPrimaryCtorParameter = new Dictionary<IMember, IParameter>();

public RecordDecompiler(IDecompilerTypeSystem dts, ITypeDefinition recordTypeDef, DecompilerSettings settings, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -78,6 +78,8 @@ void DetectAutomaticProperties()
bool IsAutoProperty(IProperty p, out IField field)
{
field = null;
if (p.IsStatic)
return false;
if (p.Parameters.Count != 0)
return false;
if (p.Getter != null)
Expand Down Expand Up @@ -158,8 +160,18 @@ bool IsAutoSetter(IMethod method, out IField field)

IMethod DetectPrimaryConstructor()
{
if (!settings.UsePrimaryConstructorSyntax)
return null;
if (recordTypeDef.IsRecord)
{
if (!settings.UsePrimaryConstructorSyntax)
return null;
}
else
{
if (!settings.UsePrimaryConstructorSyntaxForNonRecordTypes)
return null;
if (isStruct)
return null;
}

var subst = recordTypeDef.AsParameterizedType().GetSubstitution();
foreach (var method in recordTypeDef.Methods)
Expand All @@ -170,8 +182,8 @@ IMethod DetectPrimaryConstructor()
var m = method.Specialize(subst);
if (IsPrimaryConstructor(m, method))
return method;
primaryCtorParameterToAutoProperty.Clear();
autoPropertyToPrimaryCtorParameter.Clear();
primaryCtorParameterToAutoPropertyOrBackingField.Clear();
autoPropertyOrBackingFieldToPrimaryCtorParameter.Clear();
}

return null;
Expand Down Expand Up @@ -205,10 +217,18 @@ bool IsPrimaryConstructor(IMethod method, IMethod unspecializedMethod)
return false;
if (!(value.Kind == VariableKind.Parameter && value.Index == i))
return false;
if (!backingFieldToAutoProperty.TryGetValue(field, out var property))
return false;
primaryCtorParameterToAutoProperty.Add(unspecializedMethod.Parameters[i], property);
autoPropertyToPrimaryCtorParameter.Add(property, unspecializedMethod.Parameters[i]);
IMember backingMember;
if (backingFieldToAutoProperty.TryGetValue(field, out var property))
{
backingMember = property;
}
else
{
backingMember = field;
}

primaryCtorParameterToAutoPropertyOrBackingField.Add(unspecializedMethod.Parameters[i], backingMember);
autoPropertyOrBackingFieldToPrimaryCtorParameter.Add(backingMember, unspecializedMethod.Parameters[i]);
}

if (!isStruct)
Expand Down Expand Up @@ -261,6 +281,9 @@ bool IsRecordType(IType type)
/// </summary>
public bool MethodIsGenerated(IMethod method)
{
if (!recordTypeDef.IsRecord)
return false;

if (IsCopyConstructor(method))
{
return IsGeneratedCopyConstructor(method);
Expand Down Expand Up @@ -320,6 +343,9 @@ public bool MethodIsGenerated(IMethod method)

internal bool PropertyIsGenerated(IProperty property)
{
if (!recordTypeDef.IsRecord)
return false;

switch (property.Name)
{
case "EqualityContract" when !isStruct:
Expand All @@ -329,17 +355,35 @@ internal bool PropertyIsGenerated(IProperty property)
}
}

internal bool FieldIsGenerated(IField field)
{
if (!settings.UsePrimaryConstructorSyntaxForNonRecordTypes)
return false;

var name = field.Name;
return name.StartsWith("<", StringComparison.Ordinal)
&& name.EndsWith(">P", StringComparison.Ordinal)
&& field.IsCompilerGenerated();
}

public bool IsPropertyDeclaredByPrimaryConstructor(IProperty property)
{
var subst = recordTypeDef.AsParameterizedType().GetSubstitution();
return primaryCtor != null
&& autoPropertyToPrimaryCtorParameter.ContainsKey((IProperty)property.Specialize(subst));
&& autoPropertyOrBackingFieldToPrimaryCtorParameter.ContainsKey((IProperty)property.Specialize(subst));
}

internal (IProperty prop, IField field) GetPropertyInfoByPrimaryConstructorParameter(IParameter parameter)
{
var prop = primaryCtorParameterToAutoProperty[parameter];
return (prop, autoPropertyToBackingField[prop]);
var member = primaryCtorParameterToAutoPropertyOrBackingField[parameter];
if (member is IField field)
return (null, field);
return ((IProperty)member, autoPropertyToBackingField[(IProperty)member]);
}

internal IParameter GetPrimaryConstructorParameterFromBackingField(IField field)
{
return autoPropertyOrBackingFieldToPrimaryCtorParameter[field];
}

public bool IsCopyConstructor(IMethod method)
Expand Down
Loading

0 comments on commit 2043e5d

Please sign in to comment.