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

SamboyCoding / Cpp2IL / 28842709595

07 Jul 2026 04:59AM UTC coverage: 36.304% (+0.2%) from 36.132%
28842709595

Pull #577

github

web-flow
Merge 999a690c7 into 5ef1a1957
Pull Request #577: Diffable CS improvements

2734 of 8570 branches covered (31.9%)

Branch coverage included in aggregate %.

249 of 267 new or added lines in 6 files covered. (93.26%)

5078 of 12948 relevant lines covered (39.22%)

170756.73 hits per line

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

92.95
/Cpp2IL.Core/OutputFormats/DiffableCsOutputFormat.cs
1
using System;
2
using System.Buffers.Binary;
3
using System.CodeDom.Compiler;
4
using System.Collections.Generic;
5
using System.Diagnostics.CodeAnalysis;
6
using System.Globalization;
7
using System.IO;
8
using System.Linq;
9
using System.Reflection;
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, StringWriter> BuildOutput(ApplicationAnalysisContext context, string outputRoot)
55
    {
56
        var ret = new Dictionary<string, StringWriter>();
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 stringWriter = new StringWriter();
5,667✔
72
                var writer = new IndentedTextWriter(stringWriter, "\t");
5,667✔
73

74
                //Namespace at top of file
75
                if (!string.IsNullOrEmpty(type.Namespace))
5,667✔
76
                {
77
                    writer.WriteLine($"namespace {type.Namespace};");
5,541✔
78
                    writer.WriteLineNoTabs(string.Empty);
5,541✔
79
                }
80
                else
81
                {
82
                    writer.WriteLine("//Type is in global namespace");
126✔
83
                    writer.WriteLineNoTabs(string.Empty);
126✔
84
                }
85

86
                WriteType(writer, type);
5,667✔
87

88
                ret[path] = stringWriter;
5,667✔
89
            }
90
        }
91

92
        return ret;
3✔
93
    }
94

95
    private static void WriteType(IndentedTextWriter writer, TypeAnalysisContext type)
96
    {
97
        // if (type.IsCompilerGeneratedBasedOnCustomAttributes)
98
        //Do not output compiler-generated types
99
        // return;
100

101
        //Custom attributes for type. Includes a trailing newline
102
        WriteCustomAttributes(writer, type);
7,404✔
103

104
        //Type declaration line
105
        writer.Write(CsFileUtils.GetKeyWordsForType(type));
7,404✔
106
        writer.Write(' ');
7,404✔
107
        writer.Write(CsFileUtils.GetTypeName(type));
7,404✔
108
        CsFileUtils.WriteInheritanceInfo(type, writer);
7,404✔
109
        writer.WriteLine();
7,404✔
110
        writer.WriteLine('{');
7,404✔
111

112
        //Type declaration done, increase indent
113
        writer.Indent++;
7,404✔
114

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

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

141
            writer.WriteLineNoTabs(string.Empty);
6,555✔
142

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

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

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

162
        //Decrease indent, close brace
163
        writer.Indent--;
7,404✔
164
        writer.WriteLine('}');
7,404✔
165
        writer.WriteLineNoTabs(string.Empty);
7,404✔
166
    }
7,404✔
167

168
    private static void WriteField(IndentedTextWriter writer, FieldAnalysisContext field)
169
    {
170
        if (field is InjectedFieldAnalysisContext)
15,675!
171
            return;
×
172

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

176
        //Field declaration line
177
        writer.Write(CsFileUtils.GetKeyWordsForField(field));
15,675✔
178
        writer.Write(' ');
15,675✔
179
        writer.Write(CsFileUtils.GetTypeName(field.FieldType));
15,675✔
180
        writer.Write(' ');
15,675✔
181
        writer.Write(field.Name);
15,675✔
182

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

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

197
            if (defaultValue is string stringDefaultValue)
549✔
198
            {
199
                writer.Write('"');
186✔
200
                writer.Write(stringDefaultValue);
186✔
201
                writer.Write('"');
186✔
202
            }
203
            else if (defaultValue is char charDefaultValue)
363✔
204
            {
205
                writer.Write("'\\u");
6✔
206
                writer.Write(((int)charDefaultValue).ToString("X"));
6✔
207
                writer.Write("'");
6✔
208
            }
209
            else
210
                writer.Write(InvariantValue(defaultValue));
357✔
211
        }
212

213
        writer.Write("; //Field offset: 0x");
15,378✔
214
        writer.Write(field.Offset.ToString("X"));
15,378✔
215

216
        if ((field.Attributes & FieldAttributes.HasFieldRVA) != 0)
15,378!
NEW
217
            writer.Write(" || Has Field RVA (address hidden for diffability)");
×
218

219
        writer.WriteLine();
15,378✔
220
    }
15,378✔
221

222
    private static void WriteFieldRvaInitializer(IndentedTextWriter writer, FieldAnalysisContext field, byte[] data)
223
    {
224
        var tail = $" //Field offset: 0x{field.Offset.ToString("X")} || Has Field RVA (address hidden for diffability)";
297✔
225

226
        if (TryAscendingInt32Array(data, out var ints))
297✔
227
        {
228
            writer.Write(" = new int[]");
15✔
229
            writer.Write(tail);
15✔
230
            writer.WriteLine();
15✔
231
            writer.WriteLine('{');
15✔
232
            writer.Indent++;
15✔
233
            for (var i = 0; i < ints.Length; i += 12)
78✔
234
            {
235
                var n = Math.Min(12, ints.Length - i);
24✔
236
                for (var j = 0; j < n; j++)
402✔
237
                {
238
                    if (j > 0) writer.Write(", ");
330✔
239
                    writer.Write(ints[i + j]);
177✔
240
                }
241
                if (i + n < ints.Length) writer.Write(',');
33✔
242
                writer.WriteLine();
24✔
243
            }
244
            writer.Indent--;
15✔
245
            writer.WriteLine("};");
15✔
246
            return;
15✔
247
        }
248

249
        writer.Write(" = new byte[]");
282✔
250
        writer.Write(tail);
282✔
251
        writer.WriteLine();
282✔
252
        writer.WriteLine('{');
282✔
253
        writer.Indent++;
282✔
254
        for (var i = 0; i < data.Length; i += 16)
16,578✔
255
        {
256
            var n = Math.Min(16, data.Length - i);
8,007✔
257
            for (var j = 0; j < n; j++)
268,896✔
258
            {
259
                if (j > 0) writer.Write(", ");
244,875✔
260
                writer.Write("0x");
126,441✔
261
                writer.Write(data[i + j].ToString("X2"));
126,441✔
262
            }
263
            if (i + n < data.Length) writer.Write(',');
15,732✔
264
            writer.WriteLine();
8,007✔
265
        }
266
        writer.Indent--;
282✔
267
        writer.WriteLine("};");
282✔
268
    }
282✔
269

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

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

280
        var values = new int[b.Length / sizeof(int)];
192✔
281
        var prev = -1;
192✔
282

283
        for (var i = 0; i < values.Length; i++)
810✔
284
        {
285
            var v = BinaryPrimitives.ReadInt32LittleEndian(b.AsSpan(i * sizeof(int), sizeof(int)));
390✔
286

287
            if (i == 0 && v != 0)
390✔
288
                return false;
156✔
289

290
            if (v <= prev)
234✔
291
                return false;
21✔
292

293
            prev = v;
213✔
294
            values[i] = v;
213✔
295
        }
296

297
        ints = values;
15✔
298
        return true;
15✔
299
    }
300

301
    private static void WriteEvent(IndentedTextWriter writer, EventAnalysisContext evt)
302
    {
303
        //Custom attributes for event. Includes a trailing newline
304
        WriteCustomAttributes(writer, evt);
15✔
305

306
        //Event declaration line
307
        writer.Write(CsFileUtils.GetKeyWordsForEvent(evt));
15✔
308
        writer.Write(' ');
15✔
309
        writer.Write(CsFileUtils.GetTypeName(evt.EventType));
15✔
310
        writer.Write(' ');
15✔
311
        writer.Write(evt.Name);
15✔
312
        writer.WriteLine();
15✔
313
        writer.WriteLine('{');
15✔
314

315
        //Add/Remove/Invoke
316
        writer.Indent++;
15✔
317
        if (evt.Adder != null)
15!
318
            WriteAccessor(writer, evt.Adder, "add", evt.Visibility);
15✔
319
        if (evt.Remover != null)
15!
320
            WriteAccessor(writer, evt.Remover, "remove", evt.Visibility);
15✔
321
        if (evt.Invoker != null)
15!
NEW
322
            WriteAccessor(writer, evt.Invoker, "fire", evt.Visibility);
×
323
        writer.Indent--;
15✔
324

325
        writer.WriteLine('}');
15✔
326
        writer.WriteLineNoTabs(string.Empty);
15✔
327
    }
15✔
328

329
    private static void WriteProperty(IndentedTextWriter writer, PropertyAnalysisContext prop)
330
    {
331
        //Custom attributes for property. Includes a trailing newline
332
        WriteCustomAttributes(writer, prop);
5,730✔
333

334
        //Property declaration line
335
        writer.Write(CsFileUtils.GetKeyWordsForProperty(prop));
5,730✔
336
        writer.Write(' ');
5,730✔
337
        writer.Write(CsFileUtils.GetTypeName(prop.PropertyType));
5,730✔
338
        writer.Write(' ');
5,730✔
339
        writer.Write(prop.Name);
5,730✔
340
        writer.WriteLine();
5,730✔
341
        writer.WriteLine('{');
5,730✔
342

343
        //Get/Set
344
        writer.Indent++;
5,730✔
345
        if (prop.Getter != null)
5,730✔
346
            WriteAccessor(writer, prop.Getter, "get", prop.Visibility);
5,433✔
347
        if (prop.Setter != null)
5,730✔
348
            WriteAccessor(writer, prop.Setter, "set", prop.Visibility);
801✔
349
        writer.Indent--;
5,730✔
350

351
        writer.WriteLine('}');
5,730✔
352
        writer.WriteLineNoTabs(string.Empty);
5,730✔
353
    }
5,730✔
354

355
    private static void WriteMethod(IndentedTextWriter writer, MethodAnalysisContext method)
356
    {
357
        if (method is InjectedMethodAnalysisContext)
37,227!
358
            return;
×
359

360
        //Custom attributes for method. Includes a trailing newline
361
        WriteCustomAttributes(writer, method);
37,227✔
362

363
        //Method declaration line
364
        writer.Write(CsFileUtils.GetKeyWordsForMethod(method));
37,227✔
365
        writer.Write(' ');
37,227✔
366
        if (method.Name is not ".ctor" and not ".cctor")
37,227✔
367
        {
368
            writer.Write(CsFileUtils.GetTypeName(method.ReturnType));
31,050✔
369
            writer.Write(' ');
31,050✔
370
            writer.Write(method.Name);
31,050✔
371
        }
372
        else
373
        {
374
            //Constructor
375
            writer.Write(CsFileUtils.GetTypeName(method.DeclaringType!));
6,177✔
376
        }
377

378
        writer.Write('(');
37,227✔
379
        writer.Write(CsFileUtils.GetMethodParameterString(method));
37,227✔
380
        writer.Write(") { }");
37,227✔
381

382
        if (IncludeMethodLength)
37,227!
383
        {
NEW
384
            writer.Write(" //Length: ");
×
NEW
385
            writer.Write(method.RawBytes.Length);
×
386
        }
387

388
        writer.WriteLine();
37,227✔
389
        writer.WriteLineNoTabs(string.Empty);
37,227✔
390
    }
37,227✔
391

392
    //get/set/add/remove/raise
393
    private static void WriteAccessor(IndentedTextWriter writer, MethodAnalysisContext accessor, string accessorType, MethodAttributes parentVisibility)
394
    {
395
        //Custom attributes for accessor. Includes a trailing newline
396
        WriteCustomAttributes(writer, accessor);
6,264✔
397

398
        writer.Write(CsFileUtils.GetKeyWordsForMethod(accessor, parentVisibility));
6,264✔
399
        writer.Write(' ');
6,264✔
400
        writer.Write(accessorType);
6,264✔
401
        writer.Write(" { } //Length: ");
6,264✔
402
        writer.Write(accessor.RawBytes.Length);
6,264✔
403
        writer.WriteLine();
6,264✔
404
    }
6,264✔
405

406
    private static void WriteCustomAttributes(IndentedTextWriter writer, HasCustomAttributes owner)
407
        => CsFileUtils.WriteCustomAttributeStrings(owner, writer, true, true);
72,315✔
408

409
    private static string InvariantValue(object? value)
410
        => value is null ? "" : value is IFormattable f ? f.ToString(null, CultureInfo.InvariantCulture) : value.ToString() ?? "";
10,314!
411
}
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