• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SamboyCoding / Cpp2IL / 28716420818

04 Jul 2026 06:58PM UTC coverage: 38.737% (+0.2%) from 38.498%
28716420818

push

github

SamboyCoding
chore: Code cleanup

2880 of 8470 branches covered (34.0%)

Branch coverage included in aggregate %.

23 of 23 new or added lines in 2 files covered. (100.0%)

2 existing lines in 2 files now uncovered.

5404 of 12915 relevant lines covered (41.84%)

206113.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.61
/Cpp2IL.Core/OutputFormats/DiffableCsOutputFormat.cs
1
using System;
2
using System.Buffers.Binary;
3
using System.Collections.Generic;
4
using System.Diagnostics.CodeAnalysis;
5
using System.Globalization;
6
using System.IO;
7
using System.Linq;
8
using System.Reflection;
9
using System.Text;
10
using Cpp2IL.Core.Api;
11
using Cpp2IL.Core.Extensions;
12
using Cpp2IL.Core.Logging;
13
using Cpp2IL.Core.Model.Contexts;
14
using Cpp2IL.Core.Utils;
15
using LibCpp2IL;
16

17
namespace Cpp2IL.Core.OutputFormats;
18

19
public class DiffableCsOutputFormat : Cpp2IlOutputFormat
20
{
21
    public static bool IncludeMethodLength = false;
22

23
    public override string OutputFormatId => "diffable-cs";
1✔
24
    public override string OutputFormatName => "Diffable C#";
×
25

26
    public override void DoOutput(ApplicationAnalysisContext context, string outputRoot)
27
    {
28
        //General principle of diffable CS:
29
        //- Same-line method bodies ({ })
30
        //- Attributes in alphabetical order
31
        //- Members in alphabetical order and in nested type-field-event-prop-method member order
32
        //- No info on addresses or tokens as these change with every rebuild
33

34
        //The idea is to make it as easy as possible for software like WinMerge, github, etc, to diff the two versions of the code and show the user exactly what changed.
35

36
        outputRoot = Path.Combine(outputRoot, "DiffableCs");
3✔
37

38
        if (Directory.Exists(outputRoot))
3!
39
        {
40
            Logger.InfoNewline("Removing old DiffableCs output directory...", "DiffableCsOutputFormat");
×
41
            Directory.Delete(outputRoot, true);
×
42
        }
43

44
        Logger.InfoNewline("Building C# files and directory structure...", "DiffableCsOutputFormat");
3✔
45
        var files = BuildOutput(context, outputRoot);
3✔
46

47
        Logger.InfoNewline("Writing C# files...", "DiffableCsOutputFormat");
3✔
48
        foreach (var (filePath, fileContent) in files)
11,340✔
49
        {
50
            File.WriteAllText(filePath, fileContent.ToString());
5,667✔
51
        }
52
    }
3✔
53

54
    private static Dictionary<string, StringBuilder> BuildOutput(ApplicationAnalysisContext context, string outputRoot)
55
    {
56
        var ret = new Dictionary<string, StringBuilder>();
3✔
57

58
        foreach (var assembly in context.Assemblies)
222✔
59
        {
60
            var asmPath = Path.Combine(outputRoot, assembly.CleanAssemblyName);
108✔
61
            Directory.CreateDirectory(asmPath);
108✔
62

63
            foreach (var type in assembly.TopLevelTypes)
11,550✔
64
            {
65
                if (type is InjectedTypeAnalysisContext)
5,667✔
66
                    continue;
67

68
                var path = Path.Combine(asmPath, type.NamespaceAsSubdirs, MiscUtils.CleanPathElement(type.Name + ".cs"));
5,667✔
69
                Directory.CreateDirectory(Path.GetDirectoryName(path)!);
5,667✔
70

71
                var sb = new StringBuilder();
5,667✔
72

73
                //Namespace at top of file
74
                if (!string.IsNullOrEmpty(type.Namespace))
5,667✔
75
                    sb.AppendLine($"namespace {type.Namespace};").AppendLine();
5,541✔
76
                else
77
                    sb.AppendLine("//Type is in global namespace").AppendLine();
126✔
78

79
                AppendType(sb, type);
5,667✔
80

81
                ret[path] = sb;
5,667✔
82
            }
83
        }
84

85
        return ret;
3✔
86
    }
87

88
    private static void AppendType(StringBuilder sb, TypeAnalysisContext type, int indent = 0)
89
    {
90
        // if (type.IsCompilerGeneratedBasedOnCustomAttributes)
91
        //Do not output compiler-generated types
92
        // return;
93

94
        //Custom attributes for type. Includes a trailing newline
95
        AppendCustomAttributes(sb, type, indent);
7,404✔
96

97
        //Type declaration line
98
        sb.Append('\t', indent);
7,404✔
99

100
        sb.Append(CsFileUtils.GetKeyWordsForType(type));
7,404✔
101
        sb.Append(' ');
7,404✔
102
        sb.Append(CsFileUtils.GetTypeName(type));
7,404✔
103
        CsFileUtils.AppendInheritanceInfo(type, sb);
7,404✔
104
        sb.AppendLine();
7,404✔
105
        sb.Append('\t', indent);
7,404✔
106
        sb.Append('{');
7,404✔
107
        sb.AppendLine();
7,404✔
108

109
        //Type declaration done, increase indent
110
        indent++;
7,404✔
111

112
        if (type.IsEnumType)
7,404✔
113
        {
114
            var enumValues = type.Fields.Where(f => f.IsStatic).ToList();
11,655✔
115
            enumValues.SortByExtractedKey(e => e.Token); //Not as good as sorting by value but it'll do
58,059✔
116
            foreach (var enumValue in enumValues)
21,612✔
117
            {
118
                sb.Append('\t', indent);
9,957✔
119
                sb.Append(enumValue.Name);
9,957✔
120
                sb.Append(" = ");
9,957✔
121
                sb.Append(InvariantValue(enumValue.BackingData!.DefaultValue));
9,957✔
122
                sb.Append(',');
9,957✔
123
                sb.AppendLine();
9,957✔
124
            }
125
        }
126
        else
127
        {
128
            //Nested classes, alphabetical order
129
            var nestedTypes = type.NestedTypes.Clone();
6,555✔
130
            nestedTypes.SortByExtractedKey(t => t.Name);
13,683✔
131
            foreach (var nested in nestedTypes)
16,584✔
132
                AppendType(sb, nested, indent);
1,737✔
133

134
            //Fields, offset order, static first
135
            var fields = type.Fields.Clone();
6,555✔
136
            fields.SortByExtractedKey(f => f.IsStatic ? f.Offset : f.Offset + 0x1000);
54,105✔
137
            foreach (var field in fields)
44,460✔
138
                AppendField(sb, field, indent);
15,675✔
139

140
            sb.AppendLine();
6,555✔
141

142
            //Events, alphabetical order
143
            var events = type.Events.Clone();
6,555✔
144
            events.SortByExtractedKey(e => e.Name);
6,561✔
145
            foreach (var evt in events)
13,140✔
146
                AppendEvent(sb, evt, indent);
15✔
147

148
            //Properties, alphabetical order
149
            var properties = type.Properties.Clone();
6,555✔
150
            properties.SortByExtractedKey(p => p.Name);
28,365✔
151
            foreach (var prop in properties)
24,570✔
152
                AppendProperty(sb, prop, indent);
5,730✔
153

154
            //Methods, alphabetical order
155
            var methods = type.Methods.Clone();
6,555✔
156
            methods.SortByExtractedKey(m => m.Name);
264,573✔
157
            foreach (var method in methods)
87,564✔
158
                AppendMethod(sb, method, indent);
37,227✔
159
        }
160

161
        //Decrease indent, close brace
162
        indent--;
7,404✔
163
        sb.Append('\t', indent);
7,404✔
164
        sb.Append('}');
7,404✔
165
        sb.AppendLine().AppendLine();
7,404✔
166
    }
7,404✔
167

168
    private static void AppendField(StringBuilder sb, FieldAnalysisContext field, int indent)
169
    {
170
        if (field is InjectedFieldAnalysisContext)
15,675!
171
            return;
×
172

173
        //Custom attributes for field. Includes a trailing newline
174
        AppendCustomAttributes(sb, field, indent);
15,675✔
175

176
        //Field declaration line
177
        sb.Append('\t', indent);
15,675✔
178
        sb.Append(CsFileUtils.GetKeyWordsForField(field));
15,675✔
179
        sb.Append(' ');
15,675✔
180
        sb.Append(CsFileUtils.GetTypeName(field.FieldType));
15,675✔
181
        sb.Append(' ');
15,675✔
182
        sb.Append(field.Name);
15,675✔
183

184
        if ((field.Attributes & FieldAttributes.HasFieldRVA) != 0)
15,675✔
185
        {
186
            var fieldRva = field.StaticArrayInitialValue;
297✔
187
            if (fieldRva.Length > 0)
297✔
188
            {
189
                AppendFieldRvaInitializer(sb, field, fieldRva, indent);
297✔
190
                return;
297✔
191
            }
192
        }
193

194
        if (field.BackingData?.DefaultValue is { } defaultValue)
15,378!
195
        {
196
            sb.Append(" = ");
549✔
197

198
            if (defaultValue is string stringDefaultValue)
549✔
199
                sb.Append('"').Append(stringDefaultValue).Append('"');
186✔
200
            else if (defaultValue is char charDefaultValue)
363✔
201
                sb.Append("'\\u").Append(((int)charDefaultValue).ToString("X")).Append("'");
6✔
202
            else
203
                sb.Append(InvariantValue(defaultValue));
357✔
204
        }
205

206
        sb.Append("; //Field offset: 0x");
15,378✔
207
        sb.Append(field.Offset.ToString("X"));
15,378✔
208

209
        if ((field.Attributes & FieldAttributes.HasFieldRVA) != 0)
15,378!
UNCOV
210
            sb.Append(" || Has Field RVA (address hidden for diffability)");
×
211

212
        sb.AppendLine();
15,378✔
213
    }
15,378✔
214

215
    private static void AppendFieldRvaInitializer(StringBuilder sb, FieldAnalysisContext field, byte[] data, int indent)
216
    {
217
        var tail = $" //Field offset: 0x{field.Offset.ToString("X")} || Has Field RVA (address hidden for diffability)";
297✔
218

219
        if (TryAscendingInt32Array(data, out var ints))
297✔
220
        {
221
            sb.Append(" = new int[]").Append(tail).AppendLine();
15✔
222
            sb.Append('\t', indent).Append('{').AppendLine();
15✔
223
            for (var i = 0; i < ints.Length; i += 12)
78✔
224
            {
225
                var n = Math.Min(12, ints.Length - i);
24✔
226
                sb.Append('\t', indent + 1);
24✔
227
                for (var j = 0; j < n; j++)
402✔
228
                {
229
                    if (j > 0) sb.Append(", ");
330✔
230
                    sb.Append(ints[i + j]);
177✔
231
                }
232
                if (i + n < ints.Length) sb.Append(',');
33✔
233
                sb.AppendLine();
24✔
234
            }
235
            sb.Append('\t', indent).Append("};").AppendLine();
15✔
236
            return;
15✔
237
        }
238

239
        sb.Append(" = new byte[]").Append(tail).AppendLine();
282✔
240
        sb.Append('\t', indent).Append('{').AppendLine();
282✔
241
        for (var i = 0; i < data.Length; i += 16)
16,578✔
242
        {
243
            var n = Math.Min(16, data.Length - i);
8,007✔
244
            sb.Append('\t', indent + 1);
8,007✔
245
            for (var j = 0; j < n; j++)
268,896✔
246
            {
247
                if (j > 0) sb.Append(", ");
244,875✔
248
                sb.Append("0x").Append(data[i + j].ToString("X2"));
126,441✔
249
            }
250
            if (i + n < data.Length) sb.Append(',');
15,732✔
251
            sb.AppendLine();
8,007✔
252
        }
253
        sb.Append('\t', indent).Append("};").AppendLine();
282✔
254
    }
282✔
255

256
    //blobs that decode as 0 followed by strictly ascending little-endian int32s are (probably) offset tables,
257
    //so show them as int[] rather than a hex dump
258
    private static bool TryAscendingInt32Array(byte[] b, [NotNullWhen(true)] out int[]? ints)
259
    {
260
        ints = null;
297✔
261

262
        const int minElements = 8; //short blobs can pass the ascending check by pure coincidence
263
        if (b.Length < sizeof(int) * minElements || b.Length % sizeof(int) != 0)
297✔
264
            return false;
105✔
265

266
        var values = new int[b.Length / sizeof(int)];
192✔
267
        var prev = -1;
192✔
268

269
        for (var i = 0; i < values.Length; i++)
810✔
270
        {
271
            var v = BinaryPrimitives.ReadInt32LittleEndian(b.AsSpan(i * sizeof(int), sizeof(int)));
390✔
272

273
            if (i == 0 && v != 0)
390✔
274
                return false;
156✔
275

276
            if (v <= prev)
234✔
277
                return false;
21✔
278

279
            prev = v;
213✔
280
            values[i] = v;
213✔
281
        }
282

283
        ints = values;
15✔
284
        return true;
15✔
285
    }
286

287
    private static void AppendEvent(StringBuilder sb, EventAnalysisContext evt, int indent)
288
    {
289
        //Custom attributes for event. Includes a trailing newline
290
        AppendCustomAttributes(sb, evt, indent);
15✔
291

292
        //Event declaration line
293
        sb.Append('\t', indent);
15✔
294
        sb.Append(CsFileUtils.GetKeyWordsForEvent(evt));
15✔
295
        sb.Append(' ');
15✔
296
        sb.Append(CsFileUtils.GetTypeName(evt.EventType));
15✔
297
        sb.Append(' ');
15✔
298
        sb.Append(evt.Name).AppendLine();
15✔
299
        sb.Append('\t', indent);
15✔
300
        sb.Append('{');
15✔
301
        sb.AppendLine();
15✔
302

303
        //Add/Remove/Invoke
304
        indent++;
15✔
305
        if (evt.Adder != null)
15✔
306
            AppendAccessor(sb, evt.Adder, "add", indent);
15✔
307
        if (evt.Remover != null)
15✔
308
            AppendAccessor(sb, evt.Remover, "remove", indent);
15✔
309
        if (evt.Invoker != null)
15!
310
            AppendAccessor(sb, evt.Invoker, "fire", indent);
×
311
        indent--;
15✔
312

313
        sb.Append('\t', indent);
15✔
314
        sb.Append('}');
15✔
315
        sb.AppendLine().AppendLine();
15✔
316
    }
15✔
317

318
    private static void AppendProperty(StringBuilder sb, PropertyAnalysisContext prop, int indent)
319
    {
320
        //Custom attributes for property. Includes a trailing newline
321
        AppendCustomAttributes(sb, prop, indent);
5,730✔
322

323
        //Property declaration line
324
        sb.Append('\t', indent);
5,730✔
325
        sb.Append(CsFileUtils.GetKeyWordsForProperty(prop));
5,730✔
326
        sb.Append(' ');
5,730✔
327
        sb.Append(CsFileUtils.GetTypeName(prop.PropertyType));
5,730✔
328
        sb.Append(' ');
5,730✔
329
        sb.Append(prop.Name);
5,730✔
330
        sb.AppendLine();
5,730✔
331
        sb.Append('\t', indent);
5,730✔
332
        sb.Append('{');
5,730✔
333
        sb.AppendLine();
5,730✔
334

335
        //Get/Set
336
        indent++;
5,730✔
337
        if (prop.Getter != null)
5,730✔
338
            AppendAccessor(sb, prop.Getter, "get", indent);
5,433✔
339
        if (prop.Setter != null)
5,730✔
340
            AppendAccessor(sb, prop.Setter, "set", indent);
801✔
341
        indent--;
5,730✔
342

343
        sb.Append('\t', indent);
5,730✔
344
        sb.Append('}');
5,730✔
345
        sb.AppendLine().AppendLine();
5,730✔
346
    }
5,730✔
347

348
    private static void AppendMethod(StringBuilder sb, MethodAnalysisContext method, int indent)
349
    {
350
        if (method is InjectedMethodAnalysisContext)
37,227!
351
            return;
×
352

353
        //Custom attributes for method. Includes a trailing newline
354
        AppendCustomAttributes(sb, method, indent);
37,227✔
355

356
        //Method declaration line
357
        sb.Append('\t', indent);
37,227✔
358
        sb.Append(CsFileUtils.GetKeyWordsForMethod(method));
37,227✔
359
        sb.Append(' ');
37,227✔
360
        if (method.Name is not ".ctor" and not ".cctor")
37,227✔
361
        {
362
            sb.Append(CsFileUtils.GetTypeName(method.ReturnType));
31,050✔
363
            sb.Append(' ');
31,050✔
364
            sb.Append(method.Name);
31,050✔
365
        }
366
        else
367
        {
368
            //Constructor
369
            sb.Append(CsFileUtils.GetTypeName(method.DeclaringType!));
6,177✔
370
        }
371

372
        sb.Append('(');
37,227✔
373
        sb.Append(CsFileUtils.GetMethodParameterString(method));
37,227✔
374
        sb.Append(") { }");
37,227✔
375

376
        if (IncludeMethodLength)
37,227!
377
        {
378
            sb.Append(" //Length: ");
×
379
            sb.Append(method.RawBytes.Length);
×
380
        }
381

382
        sb.AppendLine().AppendLine();
37,227✔
383
    }
37,227✔
384

385
    //get/set/add/remove/raise
386
    private static void AppendAccessor(StringBuilder sb, MethodAnalysisContext accessor, string accessorType, int indent)
387
    {
388
        //Custom attributes for accessor. Includes a trailing newline
389
        AppendCustomAttributes(sb, accessor, indent);
6,264✔
390

391
        sb.Append('\t', indent);
6,264✔
392
        sb.Append(CsFileUtils.GetKeyWordsForMethod(accessor, true, true));
6,264✔
393
        sb.Append(' ');
6,264✔
394
        sb.Append(accessorType);
6,264✔
395
        sb.Append(" { } //Length: ");
6,264✔
396
        sb.Append(accessor.RawBytes.Length);
6,264✔
397
        sb.AppendLine();
6,264✔
398
    }
6,264✔
399

400
    private static void AppendCustomAttributes(StringBuilder sb, HasCustomAttributes owner, int indent)
401
        => sb.Append(CsFileUtils.GetCustomAttributeStrings(owner, indent, true, true));
72,315✔
402

403
    private static string InvariantValue(object? value)
404
        => value is null ? "" : value is IFormattable f ? f.ToString(null, CultureInfo.InvariantCulture) : value.ToString() ?? "";
10,314!
405
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc