Skip to content

Commit

Permalink
Merge pull request #3235 from icsharpcode/cs12/primary-ctors
Browse files Browse the repository at this point in the history
Add C# 12 primary constructors
  • Loading branch information
siegfriedpammer authored Aug 13, 2024
2 parents 6cee0cd + 2043e5d commit 215964a
Show file tree
Hide file tree
Showing 11 changed files with 274 additions and 72 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 215964a

Please sign in to comment.