Skip to content

Commit 3b53feb

Browse files
Beautify enum declarations by referencing previously declared members
Enum members whose value duplicates an earlier member now reference it (Item2B = Item2A), [Flags] members combine earlier single-bit members (All = Item1 | Item2 | Item3) or their complement (NotItem1 = ~Item1) instead of showing a bare number. Several guardrails keep the output faithful to how such enums are written by hand: only previously declared members are referenced (field row order); a multi-bit value lying entirely within a larger, earlier member is a field encoding inside that mask, not a flag union, and stays numeric, as do zero-valued members of [Flags] enums, which routinely have several unrelated zero members. The ~X form is suppressed in byte/ushort enum declarations, where the initializer constant folds in int and would not compile. Enums with unusual underlying types (bool, native int) keep the plain constant conversion. With these rules, decompiling System.Private.CoreLib reproduces the hand-written declarations of TypeAttributes, MethodAttributes, AttributeTargets and FileAttributes almost verbatim. Assisted-by: Claude:claude-fable-5:Claude Code
1 parent 9898add commit 3b53feb

4 files changed

Lines changed: 213 additions & 24 deletions

File tree

ICSharpCode.Decompiler.Tests/TestCases/Pretty/EnumTests.cs

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public enum EnumDuplicateItemTest
5151
Item0 = 0,
5252
Item1 = 1,
5353
Item2A = 2,
54-
Item2B = 2
54+
Item2B = Item2A
5555
}
5656

5757
public enum LongBasedEnum : long
@@ -88,7 +88,16 @@ public enum SimpleFlagsEnum
8888
Item1 = 1,
8989
Item2 = 2,
9090
Item3 = 4,
91-
All = 7
91+
All = Item1 | Item2 | Item3
92+
}
93+
94+
[Flags]
95+
public enum SelfReferentialEnum
96+
{
97+
None = 0,
98+
Item1 = 1,
99+
Item2 = Item1,
100+
Item3 = 3
92101
}
93102

94103
[Flags]
@@ -102,6 +111,78 @@ public enum NegativeValueWithoutFlags
102111
Value = -2147483647
103112
}
104113

114+
public enum NonFlagsDuplicateItems
115+
{
116+
OK = 200,
117+
Default = OK,
118+
NoContent = 204
119+
}
120+
121+
public enum ZeroDuplicateItems
122+
{
123+
Unknown = 0,
124+
Default = Unknown
125+
}
126+
127+
[Flags]
128+
public enum ZeroDuplicateFlags
129+
{
130+
None = 0,
131+
Default = 0,
132+
Item1 = 1
133+
}
134+
135+
[Flags]
136+
public enum MaskFamilyFlags
137+
{
138+
VisibilityMask = 7,
139+
NotPublic = 0,
140+
Public = 1,
141+
NestedPublic = 2,
142+
NestedPrivate = 3,
143+
LayoutMask = 0x18,
144+
AutoLayout = 0,
145+
SequentialLayout = 8,
146+
ExplicitLayout = 0x10
147+
}
148+
149+
[Flags]
150+
public enum UnsignedFlags : uint
151+
{
152+
None = 0u,
153+
Item1 = 1u,
154+
Item2 = 2u,
155+
Item3 = 4u,
156+
All = uint.MaxValue,
157+
NotItem1 = ~Item1
158+
}
159+
160+
[Flags]
161+
public enum ByteFlags : byte
162+
{
163+
None = 0,
164+
Item1 = 1,
165+
Item2 = 2,
166+
NotItem2 = 0xFD
167+
}
168+
169+
[Flags]
170+
public enum ShortFlags : short
171+
{
172+
None = 0,
173+
Item1 = 1,
174+
NotItem1 = ~Item1
175+
}
176+
177+
[Flags]
178+
public enum FlagsWithNegation
179+
{
180+
None = 0,
181+
Item1 = 1,
182+
Item2 = 2,
183+
NotItem1 = ~Item1
184+
}
185+
105186
public AttributeTargets SingleEnumValue()
106187
{
107188
return AttributeTargets.Class;
@@ -142,6 +223,21 @@ public object PreservingTypeWhenBoxedTwoEnum()
142223
return AttributeTargets.Class | AttributeTargets.Delegate;
143224
}
144225

226+
public SimpleFlagsEnum SignedEnumComplement()
227+
{
228+
return ~SimpleFlagsEnum.Item1;
229+
}
230+
231+
public UnsignedFlags UnsignedEnumComplement()
232+
{
233+
return ~UnsignedFlags.Item2;
234+
}
235+
236+
public ByteFlags ByteEnumComplement()
237+
{
238+
return ~ByteFlags.Item1;
239+
}
240+
145241
public void EnumInNotZeroCheck(SimpleEnum value, NoZero value2)
146242
{
147243
if (value != SimpleEnum.Item1)

ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2369,7 +2369,18 @@ EntityDeclaration DoDecompile(IField field, DecompileRun decompileRun, ITypeReso
23692369
object? constantValue = field.GetConstantValue();
23702370
if (constantValue != null)
23712371
{
2372-
enumDec.Initializer = typeSystemAstBuilder.ConvertConstantValue(decompilationContext.CurrentTypeDefinition.EnumUnderlyingType!, constantValue);
2372+
TypeCode underlyingTypeCode = ReflectionHelper.GetTypeCode(decompilationContext.CurrentTypeDefinition.EnumUnderlyingType);
2373+
if (underlyingTypeCode is >= TypeCode.SByte and <= TypeCode.UInt64)
2374+
{
2375+
long initValue = (long)CSharpPrimitiveCast.Cast(TypeCode.Int64, constantValue, false);
2376+
enumDec.Initializer = typeSystemAstBuilder.ConvertEnumValue(decompilationContext.CurrentTypeDefinition, initValue, field);
2377+
}
2378+
else
2379+
{
2380+
// Unusual underlying types (bool, native int, ...) cannot be losslessly
2381+
// squeezed through the long-based member-reference beautification.
2382+
enumDec.Initializer = typeSystemAstBuilder.ConvertConstantValue(decompilationContext.CurrentTypeDefinition.EnumUnderlyingType!, constantValue);
2383+
}
23732384
}
23742385
enumDec.Attributes.AddRange(field.GetAttributes().Select(a => new AttributeSection(typeSystemAstBuilder.ConvertAttribute(a))));
23752386
enumDec.AddAnnotation(new MemberResolveResult(null, field));

ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs

Lines changed: 79 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
using System.Diagnostics.CodeAnalysis;
2424
using System.Linq;
2525
using System.Reflection;
26+
using System.Reflection.Metadata.Ecma335;
2627
using System.Runtime.CompilerServices;
2728

2829
using ICSharpCode.Decompiler.CSharp.Resolver;
@@ -1293,26 +1294,47 @@ bool IsFlagsEnum(ITypeDefinition type)
12931294
return type.HasAttribute(KnownAttribute.Flags);
12941295
}
12951296

1296-
Expression ConvertEnumValue(IType type, long val)
1297+
/// <summary>
1298+
/// Converts a numeric enum value into its enum member representation, if possible.
1299+
/// Uses a series of enum members concatenated by <c>|</c>, if necessary.
1300+
/// Returns <c>(EnumType)value</c> (or the plain numeric value, if <paramref name="declaringEnumMember"/> is set), if it fails.
1301+
/// <para>
1302+
/// If <paramref name="declaringEnumMember"/> is set to a non-<see langword="null"/> value,
1303+
/// unqualified references are used and self references are avoided. Also, casts to EnumType are dropped.
1304+
/// </para>
1305+
/// </summary>
1306+
internal Expression ConvertEnumValue(IType type, long val, IField? declaringEnumMember = null)
12971307
{
12981308
ITypeDefinition enumDefinition = type.GetDefinition()!;
12991309
TypeCode enumBaseTypeCode = ReflectionHelper.GetTypeCode(enumDefinition.EnumUnderlyingType);
1310+
bool isFlags = IsFlagsEnum(enumDefinition);
13001311
var fields = enumDefinition.Fields
13011312
.Select(PrepareConstant)
1302-
.Where(f => f.field != null)
1313+
.WhereNotNull()
13031314
.ToArray();
1304-
foreach (var (value, field) in fields)
1315+
int declaringTokenRowNumber = declaringEnumMember == null ? int.MaxValue : MetadataTokens.GetRowNumber(declaringEnumMember.MetadataToken);
1316+
foreach (var (value, field, weight) in fields)
13051317
{
1306-
if (value == val)
1318+
// In a [Flags] enum declaration, only reference single-bit members directly:
1319+
// combined values are built from their flag components below (so that
1320+
// e.g. All = Item1 | Item2 | Item3), and zero members stay numeric, because
1321+
// mask-style enums routinely contain several unrelated zero members
1322+
// (e.g. MethodAttributes.PrivateScope/ReuseSlot).
1323+
if (value == val && (declaringEnumMember == null || !isFlags || weight == 1))
13071324
{
1308-
var mre = new MemberReferenceExpression(new TypeReferenceExpression(ConvertType(type)), field.Name);
1309-
if (AddResolveResultAnnotations)
1310-
mre.AddAnnotation(new MemberResolveResult(mre.Target.GetResolveResult(), field));
1311-
return mre;
1325+
if (field == declaringEnumMember || (declaringTokenRowNumber < MetadataTokens.GetRowNumber(field.MetadataToken)))
1326+
{
1327+
return ConvertConstantValue(enumDefinition.EnumUnderlyingType!, CSharpPrimitiveCast.Cast(enumBaseTypeCode, val, false));
1328+
}
1329+
return MakeEnumMemberReference(field);
13121330
}
13131331
}
1314-
if (IsFlagsEnum(enumDefinition))
1332+
if (isFlags)
13151333
{
1334+
// The complement of a byte- or ushort-based enum member is computed in int and
1335+
// therefore negative, which an enum member initializer cannot implicitly convert
1336+
// back to the underlying type -- the ~X form would not compile there.
1337+
bool complementCompiles = declaringEnumMember == null || enumBaseTypeCode is not (TypeCode.Byte or TypeCode.UInt16);
13161338
long enumValue = val;
13171339
Expression? expr = null;
13181340
long negatedEnumValue = ~val;
@@ -1333,24 +1355,27 @@ Expression ConvertEnumValue(IType type, long val)
13331355
break;
13341356
}
13351357
Expression? negatedExpr = null;
1336-
foreach (var (fieldValue, field) in fields.OrderByDescending(f => CalculateHammingWeight(unchecked((ulong)f.value))))
1358+
foreach (var (fieldValue, field, weight) in fields.OrderByDescending(f => f.weight))
13371359
{
1338-
if (fieldValue == 0)
1360+
if (fieldValue == 0 || field == declaringEnumMember)
13391361
continue; // skip None enum value
13401362

1363+
if (declaringTokenRowNumber < MetadataTokens.GetRowNumber(field.MetadataToken))
1364+
continue;
1365+
13411366
if ((fieldValue & enumValue) == fieldValue)
13421367
{
1343-
var fieldExpression = new MemberReferenceExpression(new TypeReferenceExpression(ConvertType(type)), field.Name);
1368+
var fieldExpression = MakeEnumMemberReference(field);
13441369
if (expr == null)
13451370
expr = fieldExpression;
13461371
else
13471372
expr = new BinaryOperatorExpression(expr, BinaryOperatorType.BitwiseOr, fieldExpression);
13481373

13491374
enumValue &= ~fieldValue;
13501375
}
1351-
if ((fieldValue & negatedEnumValue) == fieldValue)
1376+
if (complementCompiles && (fieldValue & negatedEnumValue) == fieldValue)
13521377
{
1353-
var fieldExpression = new MemberReferenceExpression(new TypeReferenceExpression(ConvertType(type)), field.Name);
1378+
var fieldExpression = MakeEnumMemberReference(field);
13541379
if (negatedExpr == null)
13551380
negatedExpr = fieldExpression;
13561381
else
@@ -1359,28 +1384,43 @@ Expression ConvertEnumValue(IType type, long val)
13591384
negatedEnumValue &= ~fieldValue;
13601385
}
13611386
}
1362-
if (enumValue == 0 && expr != null)
1387+
// A multi-bit value that lies entirely within a larger, previously declared member
1388+
// is usually a field encoding inside that mask (e.g. TypeAttributes.NestedPrivate
1389+
// within VisibilityMask), not a union of independent flags; keep it numeric.
1390+
bool isEncodedInEarlierMask = declaringEnumMember != null && fields.Any(
1391+
f => f.field != declaringEnumMember
1392+
&& MetadataTokens.GetRowNumber(f.field.MetadataToken) < declaringTokenRowNumber
1393+
&& (f.value & val) == val && f.value != val);
1394+
if (enumValue == 0 && expr != null && !isEncodedInEarlierMask)
13631395
{
13641396
if (!(negatedEnumValue == 0 && negatedExpr != null && negatedExpr.Descendants.Count() < expr.Descendants.Count()))
13651397
{
13661398
return expr;
13671399
}
13681400
}
1369-
if (negatedEnumValue == 0 && negatedExpr != null)
1401+
if (complementCompiles && negatedEnumValue == 0 && negatedExpr != null)
13701402
{
13711403
return new UnaryOperatorExpression(UnaryOperatorType.BitNot, negatedExpr);
13721404
}
13731405
}
1374-
return new CastExpression(ConvertType(type), new PrimitiveExpression(CSharpPrimitiveCast.Cast(enumBaseTypeCode, val, false)));
13751406

1376-
(long value, IField field) PrepareConstant(IField field)
1407+
var numericExpression = ConvertConstantValue(enumDefinition.EnumUnderlyingType!, CSharpPrimitiveCast.Cast(enumBaseTypeCode, val, false));
1408+
if (declaringEnumMember != null)
1409+
{
1410+
return numericExpression;
1411+
}
1412+
return new CastExpression(ConvertType(type), numericExpression);
1413+
1414+
(long value, IField field, int weight)? PrepareConstant(IField field)
13771415
{
13781416
if (!field.IsConst)
1379-
return (-1, null!);
1417+
return null;
13801418
object? constantValue = field.GetConstantValue();
13811419
if (constantValue == null)
1382-
return (-1, null!);
1383-
return ((long)CSharpPrimitiveCast.Cast(TypeCode.Int64, constantValue, checkForOverflow: false), field);
1420+
return null;
1421+
var value = (long)CSharpPrimitiveCast.Cast(TypeCode.Int64, constantValue, checkForOverflow: false);
1422+
var weight = CalculateHammingWeight(unchecked((ulong)value));
1423+
return (value, field, weight);
13841424
}
13851425

13861426
// see https://en.wikipedia.org/wiki/Hamming_weight
@@ -1395,6 +1435,24 @@ int CalculateHammingWeight(ulong value)
13951435
x = (x + (x >> 4)) & m4; //put count of each 8 bits into those 8 bits
13961436
return unchecked((int)((x * h01) >> 56)); //returns left 8 bits of x + (x<<8) + (x<<16) + (x<<24) + ...
13971437
}
1438+
1439+
Expression MakeEnumMemberReference(IField field)
1440+
{
1441+
if (declaringEnumMember == null)
1442+
{
1443+
var mre = new MemberReferenceExpression(new TypeReferenceExpression(ConvertType(type)), field.Name);
1444+
if (AddResolveResultAnnotations)
1445+
mre.AddAnnotation(new MemberResolveResult(mre.Target.GetResolveResult(), field));
1446+
return mre;
1447+
}
1448+
else
1449+
{
1450+
var ie = new IdentifierExpression(field.Name);
1451+
if (AddResolveResultAnnotations)
1452+
ie.AddAnnotation(new MemberResolveResult(null, field));
1453+
return ie;
1454+
}
1455+
}
13981456
}
13991457

14001458
static bool IsValidFraction(long num, long den)

ICSharpCode.Decompiler/Util/CollectionExtensions.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,30 @@ public static IEnumerable<U> SelectWithIndex<T, U>(this IEnumerable<T> source, F
253253
}
254254
}
255255

256+
/// <summary>
257+
/// Filters out null elements, narrowing the element type to non-nullable.
258+
/// </summary>
259+
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source) where T : class
260+
{
261+
foreach (var item in source)
262+
{
263+
if (item != null)
264+
yield return item;
265+
}
266+
}
267+
268+
/// <summary>
269+
/// Filters out null elements and unwraps the remaining values.
270+
/// </summary>
271+
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source) where T : struct
272+
{
273+
foreach (var item in source)
274+
{
275+
if (item.HasValue)
276+
yield return item.GetValueOrDefault();
277+
}
278+
}
279+
256280
/// <summary>
257281
/// The merge step of merge sort.
258282
/// </summary>

0 commit comments

Comments
 (0)