1
+ using System . Diagnostics ;
2
+ using System . Globalization ;
3
+ using System . Text . RegularExpressions ;
4
+ using Microsoft . CodeAnalysis ;
5
+ using Microsoft . CodeAnalysis . CSharp ;
6
+ using Microsoft . CodeAnalysis . CSharp . Syntax ;
7
+ using Microsoft . Extensions . Options ;
8
+ using static Microsoft . CodeAnalysis . CSharp . SyntaxFactory ;
9
+
10
+ namespace Silk . NET . SilkTouch . Mods ;
11
+
12
+ /// <summary>
13
+ /// Contains various transformations related to enums.
14
+ /// </summary>
15
+ [ ModConfiguration < Configuration > ]
16
+ public class TransformEnums ( IOptionsSnapshot < TransformEnums . Configuration > cfg ) : IMod
17
+ {
18
+ /// <summary>
19
+ /// TransformEnums mod configuration.
20
+ /// </summary>
21
+ public record Configuration
22
+ {
23
+ /// <summary>
24
+ /// Transforms [Flags] enums to have a "None = 0" member if they do not already have an equivalent.
25
+ /// </summary>
26
+ public bool AddNoneMemberToFlags { get ; init ; } = false ;
27
+
28
+ /// <summary>
29
+ /// Removes enum members that match the enum member filter.
30
+ /// </summary>
31
+ /// <remarks>
32
+ /// This was originally designed to remove max enum value members.
33
+ /// </remarks>
34
+ public EnumMemberFilterConfiguration [ ] RemoveMembers { get ; init ; } = [ ] ;
35
+ }
36
+
37
+ /// <summary>
38
+ /// Represents a filter used to match enum members.
39
+ /// </summary>
40
+ public record EnumMemberFilterConfiguration
41
+ {
42
+ /// <summary>
43
+ /// The enum type name must match this regex.
44
+ /// </summary>
45
+ public string TypeName { get ; init ; } = ".*" ;
46
+
47
+ /// <summary>
48
+ /// The enum member name must match this regex.
49
+ /// </summary>
50
+ public string MemberName { get ; init ; } = ".*" ;
51
+
52
+ /// <summary>
53
+ /// The enum member value must match this in value.
54
+ /// This value will be parsed as an integer. Hexadecimal is allowed.
55
+ /// Use null to disable this filter.
56
+ /// </summary>
57
+ public string ? MemberValue { get ; init ; }
58
+ }
59
+
60
+ private class EnumMemberFilter
61
+ {
62
+ public Regex TypeName { get ; }
63
+ public Regex MemberName { get ; }
64
+ public long ? MemberValue { get ; }
65
+
66
+ public EnumMemberFilter ( EnumMemberFilterConfiguration configuration )
67
+ {
68
+ TypeName = new Regex ( configuration . TypeName ) ;
69
+ MemberName = new Regex ( configuration . MemberName ) ;
70
+
71
+ if ( configuration . MemberValue != null )
72
+ {
73
+ if ( configuration . MemberValue . StartsWith ( "0x" ) )
74
+ {
75
+ MemberValue = long . Parse ( configuration . MemberValue [ "0x" . Length ..] , NumberStyles . AllowHexSpecifier ) ;
76
+ }
77
+ else
78
+ {
79
+ MemberValue = long . Parse ( configuration . MemberValue ) ;
80
+ }
81
+ }
82
+ }
83
+
84
+ public bool IsTypeMatch ( ITypeSymbol ? enumType )
85
+ {
86
+ return enumType != null && TypeName . IsMatch ( enumType . Name ) ;
87
+ }
88
+
89
+ public bool IsMemberMatch ( ISymbol ? enumMember )
90
+ {
91
+ if ( enumMember is not IFieldSymbol fieldSymbol )
92
+ {
93
+ return false ;
94
+ }
95
+
96
+ if ( ! MemberName . IsMatch ( enumMember . Name ) )
97
+ {
98
+ return false ;
99
+ }
100
+
101
+ if ( MemberValue == null )
102
+ {
103
+ // Filter is disabled
104
+ return true ;
105
+ }
106
+
107
+ if ( fieldSymbol . ConstantValue == null )
108
+ {
109
+ // We don't know the constant value for sure
110
+ // Return false as a default
111
+ return false ;
112
+ }
113
+
114
+ return Convert . ToInt64 ( fieldSymbol . ConstantValue ) == MemberValue ;
115
+ }
116
+ }
117
+
118
+ /// <inheritdoc />
119
+ public async Task ExecuteAsync ( IModContext ctx , CancellationToken ct = default )
120
+ {
121
+ var config = cfg . Get ( ctx . JobKey ) ;
122
+ var removeMemberFilters = config . RemoveMembers . Select ( c => new EnumMemberFilter ( c ) ) . ToList ( ) ;
123
+
124
+ var proj = ctx . SourceProject ;
125
+ if ( proj == null )
126
+ {
127
+ return ;
128
+ }
129
+
130
+ var compilation = await proj . GetCompilationAsync ( ct ) ;
131
+ if ( compilation == null )
132
+ {
133
+ return ;
134
+ }
135
+
136
+ var rewriter = new Rewriter ( config , removeMemberFilters , compilation ) ;
137
+ foreach ( var docId in proj ? . DocumentIds ?? [ ] )
138
+ {
139
+ var doc = proj ! . GetDocument ( docId ) ?? throw new InvalidOperationException ( "Document missing" ) ;
140
+ proj = doc . WithSyntaxRoot (
141
+ rewriter . Visit ( await doc . GetSyntaxRootAsync ( ct ) ) ? . NormalizeWhitespace ( )
142
+ ?? throw new InvalidOperationException ( "Visit returned null." )
143
+ ) . Project ;
144
+ }
145
+
146
+ ctx . SourceProject = proj ;
147
+ }
148
+
149
+ private class Rewriter ( Configuration config , List < EnumMemberFilter > removeMemberFilters , Compilation compilation ) : CSharpSyntaxRewriter
150
+ {
151
+ public override SyntaxNode ? VisitEnumDeclaration ( EnumDeclarationSyntax node )
152
+ {
153
+ var semanticModel = compilation . GetSemanticModel ( node . SyntaxTree ) ;
154
+ var symbol = semanticModel . GetDeclaredSymbol ( node ) ;
155
+ if ( symbol == null )
156
+ {
157
+ return base . VisitEnumDeclaration ( node ) ;
158
+ }
159
+
160
+ // This list is used to defer the modification of the enum declaration syntax node
161
+ // This is important because modifying the node will detach it from the semantic model
162
+ var members = node . Members . ToList ( ) ;
163
+
164
+ foreach ( var filter in removeMemberFilters )
165
+ {
166
+ if ( ! filter . IsTypeMatch ( symbol ) )
167
+ {
168
+ continue ;
169
+ }
170
+
171
+ members = members
172
+ . Where ( member => ! filter . IsMemberMatch ( semanticModel . GetDeclaredSymbol ( member ) ) )
173
+ . ToList ( ) ;
174
+ }
175
+
176
+ var isFlagsEnum = node . AttributeLists . SelectMany ( list => list . Attributes )
177
+ . Any ( attribute => attribute . IsAttribute ( "System.Flags" ) ) ;
178
+
179
+ if ( node . Identifier . ToString ( ) == "ClusterAccelerationStructureAddressResolutionFlagsNV" )
180
+ {
181
+ Debugger . Break ( ) ;
182
+ }
183
+
184
+ if ( isFlagsEnum && config . AddNoneMemberToFlags )
185
+ {
186
+ // Add None member if it doesn't exist yet
187
+ var hasNoneMember = symbol . Members ( ) . Any ( member =>
188
+ {
189
+ if ( member is not IFieldSymbol fieldSymbol )
190
+ {
191
+ return false ;
192
+ }
193
+
194
+ if ( member . Name == "None" )
195
+ {
196
+ return true ;
197
+ }
198
+
199
+ if ( fieldSymbol . ConstantValue == null )
200
+ {
201
+ // We don't know the constant value for sure
202
+ // Return false as a default
203
+ return false ;
204
+ }
205
+
206
+ return Convert . ToInt64 ( fieldSymbol . ConstantValue ) == 0 ;
207
+ } ) ;
208
+
209
+ if ( ! hasNoneMember )
210
+ {
211
+ var noneMember = EnumMemberDeclaration ( "None" )
212
+ . WithEqualsValue (
213
+ EqualsValueClause (
214
+ LiteralExpression ( SyntaxKind . NumericLiteralExpression , Literal ( 0 ) )
215
+ )
216
+ ) ;
217
+
218
+ members . Insert ( 0 , noneMember ) ;
219
+ }
220
+ }
221
+
222
+ node = node . WithMembers ( [ ..members ] ) ;
223
+
224
+ return base . VisitEnumDeclaration ( node ) ;
225
+ }
226
+ }
227
+ }
0 commit comments