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

SamboyCoding / Cpp2IL / 28681607915

03 Jul 2026 08:16PM UTC coverage: 38.407% (+4.1%) from 34.294%
28681607915

Pull #572

github

web-flow
Merge 5e35e13a9 into 45d5b3264
Pull Request #572: Diffable fieldrva literals

2854 of 8510 branches covered (33.54%)

Branch coverage included in aggregate %.

56 of 103 new or added lines in 2 files covered. (54.37%)

2 existing lines in 2 files now uncovered.

5384 of 12939 relevant lines covered (41.61%)

200359.08 hits per line

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

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

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
        // Field-RVA default bytes (the data IL2CPP hides in global-metadata.dat, e.g. obfuscation "vault" __Raw
185
        // blobs and Roslyn array initializers) — emit as a REAL C# initializer (`= new byte[]/int[] { .. }`) rather
186
        // than a trailing comment, so the value reads as code, matching the runtime-init arrays above. Only the
187
        // bytes are shown; the RVA/pointer address stays hidden, so the file remains diff-stable.
188
        if ((field.Attributes & FieldAttributes.HasFieldRVA) != 0)
15,675✔
189
        {
190
            var fieldRva = field.StaticArrayInitialValue;
297✔
191
            if (fieldRva.Length > 0 )
297✔
192
            {
193
                AppendFieldRvaInitializer(sb, field, fieldRva, indent);
297✔
194
                return;
297✔
195
            }
196
        }
197

198
        if (field.BackingData?.DefaultValue is { } defaultValue)
15,378!
199
        {
200
            sb.Append(" = ");
549✔
201

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

210
        sb.Append("; //Field offset: 0x");
15,378✔
211
        sb.Append(field.Offset.ToString("X"));
15,378✔
212

213
        if ((field.Attributes & FieldAttributes.HasFieldRVA) != 0)
15,378!
214
        {
215
            // Reached only when the field has field RVA but no decodable bytes (StaticArrayInitialValue empty) --
216
            // the with-bytes case is emitted as a real initializer above and returns before here. Mark it and stop.
UNCOV
217
            sb.Append(" || Has Field RVA (address hidden for diffability)");
×
NEW
218
            sb.AppendLine();
×
NEW
219
            return;
×
220
        }
221

222
        sb.AppendLine();
15,378✔
223
    }
15,378✔
224

225
    /// <summary>Emit a static array field's runtime-recovered value (from a .cctor RuntimeHelpers.InitializeArray)
226
    /// as a REAL C# array initializer appended after the field name: <c> = new T[] { .. }; //Field offset ..</c>.
227
    /// Decoded per the field's declared element type: byte[]/bool[] as hex (16/line); signed/unsigned integer
228
    /// arrays as decimals (12/line); char[]/float[]/double[]/unknown fall back to a raw byte[] hex dump. The
229
    /// diffable is a structural view (bodies are empty, not compilable), so an exact-typed literal isn't required
230
    /// — the point is that the value reads as code, not a comment.</summary>
231
    private static void AppendRuntimeInitInitializer(StringBuilder sb, FieldAnalysisContext field, byte[] data, int indent)
232
    {
NEW
233
        var elemName = field.FieldType is SzArrayTypeAnalysisContext sz ? sz.ElementType.FullName : null;
×
NEW
234
        var (elemSize, signed, keyword) = elemName switch
×
NEW
235
        {
×
NEW
236
            "System.SByte" => (1, true, "sbyte"),
×
NEW
237
            "System.Int16" => (2, true, "short"),
×
NEW
238
            "System.UInt16" => (2, false, "ushort"),
×
NEW
239
            "System.Int32" => (4, true, "int"),
×
NEW
240
            "System.UInt32" => (4, false, "uint"),
×
NEW
241
            "System.Int64" => (8, true, "long"),
×
NEW
242
            "System.UInt64" => (8, false, "ulong"),
×
NEW
243
            _ => (0, false, (string?)null),   // byte/bool/char/float/double/unknown -> raw byte[] hex
×
NEW
244
        };
×
245

NEW
246
        var offset = $" //Field offset: 0x{field.Offset.ToString("X")} (restored from .cctor RuntimeHelpers.InitializeArray)";
×
247

248
        // byte-sized / unknown element -> hex byte[] (16/line); multi-byte integer -> decimals (12/line).
NEW
249
        if (keyword is null || elemSize == 0 || data.Length % elemSize != 0)
×
250
        {
NEW
251
            sb.Append(" = new byte[]").Append(offset).AppendLine();
×
NEW
252
            sb.Append('\t', indent).Append('{').AppendLine();
×
NEW
253
            for (var i = 0; i < data.Length; i += 16)
×
254
            {
NEW
255
                var n = System.Math.Min(16, data.Length - i);
×
NEW
256
                sb.Append('\t', indent + 1);
×
NEW
257
                for (var j = 0; j < n; j++)
×
258
                {
NEW
259
                    if (j > 0) sb.Append(", ");
×
NEW
260
                    sb.Append("0x").Append(data[i + j].ToString("X2"));
×
261
                }
NEW
262
                if (i + n < data.Length) sb.Append(',');
×
NEW
263
                sb.AppendLine();
×
264
            }
NEW
265
            sb.Append('\t', indent).Append("};").AppendLine();
×
NEW
266
            return;
×
267
        }
268

NEW
269
        var values = new List<string>(data.Length / elemSize);
×
NEW
270
        for (var i = 0; i < data.Length; i += elemSize)
×
271
        {
NEW
272
            long v = 0;
×
NEW
273
            for (var k = 0; k < elemSize; k++) v |= (long)data[i + k] << (8 * k);      // little-endian
×
NEW
274
            if (signed)
×
275
            {
NEW
276
                var bits = elemSize * 8;
×
NEW
277
                if (bits < 64 && (v & (1L << (bits - 1))) != 0) v -= 1L << bits;         // sign-extend
×
NEW
278
                values.Add(v.ToString());
×
279
            }
280
            else
281
            {
NEW
282
                values.Add(((ulong)v & (elemSize == 8 ? ulong.MaxValue : (1UL << (elemSize * 8)) - 1)).ToString());
×
283
            }
284
        }
NEW
285
        sb.Append(" = new ").Append(keyword).Append("[]").Append(offset).AppendLine();
×
NEW
286
        sb.Append('\t', indent).Append('{').AppendLine();
×
NEW
287
        for (var i = 0; i < values.Count; i += 12)
×
288
        {
NEW
289
            var n = System.Math.Min(12, values.Count - i);
×
NEW
290
            sb.Append('\t', indent + 1)
×
NEW
291
              .Append(string.Join(", ", values.GetRange(i, n)))
×
NEW
292
              .Append(i + n < values.Count ? "," : "")
×
NEW
293
              .AppendLine();
×
294
        }
NEW
295
        sb.Append('\t', indent).Append("};").AppendLine();
×
NEW
296
    }
×
297

298
    /// <summary>
299
    /// Render the field-RVA default bytes IL2CPP hides in global-metadata.dat as a REAL C# initializer appended
300
    /// after the field name: <c> = new byte[] { .. }; //Field offset .. || Has Field RVA</c> — code, not a comment,
301
    /// matching <see cref="AppendRuntimeInitInitializer"/>. The RVA/pointer address is never emitted, so the file
302
    /// stays diff-stable. If the whole blob decodes as a little-endian int32 offset table (first == 0, strictly
303
    /// ascending, non-negative) it is emitted as an <c>int[]</c> literal — that IS the real element type of the
304
    /// array this data initializes via RuntimeHelpers.InitializeArray; otherwise a <c>byte[]</c> literal.
305
    /// </summary>
306
    private static void AppendFieldRvaInitializer(StringBuilder sb, FieldAnalysisContext field, byte[] data, int indent)
307
    {
308
        var tail = $" //Field offset: 0x{field.Offset.ToString("X")} || Has Field RVA (address hidden for diffability)";
297✔
309

310
        if (TryAscendingInt32Array(data, out var ints))
297✔
311
        {
312
            sb.Append(" = new int[]").Append(tail).AppendLine();
21✔
313
            sb.Append('\t', indent).Append('{').AppendLine();
21✔
314

315
            for (var i = 0; i < ints.Length; i += 12)
102✔
316
            {
317
                var n = System.Math.Min(12, ints.Length - i);
30✔
318
                sb.Append('\t', indent + 1)
30✔
319
                  .Append(string.Join(", ", ints.Skip(i).Take(n)))
30✔
320
                  .Append(i + n < ints.Length ? "," : "")
30✔
321
                  .AppendLine();
30✔
322
            }
323

324
            sb.Append('\t', indent).Append("};").AppendLine();
21✔
325
            return;
21✔
326
        }
327

328
        sb.Append(" = new byte[]").Append(tail).AppendLine();
276✔
329
        sb.Append('\t', indent).Append('{').AppendLine();
276✔
330
        for (var i = 0; i < data.Length; i += 16)
16,548✔
331
        {
332
            var n = System.Math.Min(16, data.Length - i);
7,998✔
333
            sb.Append('\t', indent + 1);
7,998✔
334
            for (var j = 0; j < n; j++)
268,638✔
335
            {
336
                if (j > 0) sb.Append(", ");
244,644✔
337
                sb.Append("0x").Append(data[i + j].ToString("X2"));
126,321✔
338
            }
339
            if (i + n < data.Length) sb.Append(',');
15,720✔
340
            sb.AppendLine();
7,998✔
341
        }
342
        sb.Append('\t', indent).Append("};").AppendLine();
276✔
343
    }
276✔
344

345
    /// <summary>True (with decoded values) if <paramref name="b"/> is a little-endian int32 offset table: length a
346
    /// multiple of 4 (&gt;= 2 elements), first element 0, strictly ascending, non-negative. Uniquely matches offset
347
    /// tables (a <c>static int[]</c>), never a key/blob/char array.</summary>
348
    private static bool TryAscendingInt32Array(byte[] b, [NotNullWhen(true)] out int[]? ints)
349
    {
350
        ints = null;
297✔
351

352
        if (b.Length < sizeof(int)*2 || b.Length % sizeof(int) != 0)
297✔
353
            return false;
51✔
354

355
        var values = new List<int>(b.Length / 4);
246✔
356
        long prev = long.MinValue;
246✔
357

358
        for (var i = 0; i < b.Length; i += 4)
990✔
359
        {
360
            var v = BinaryPrimitives.ReadInt32LittleEndian(b.AsSpan(i, sizeof(int)));
474✔
361

362
            if (i == 0 && v != 0)
474✔
363
                return false;
201✔
364

365
            if (v < 0 || v <= prev)
273✔
366
                return false;
24✔
367

368
            prev = v;
249✔
369
            values.Add(v);
249✔
370
        }
371

372
        ints = values.ToArray();
21✔
373
        return true;
21✔
374
    }
375

376
    private static void AppendEvent(StringBuilder sb, EventAnalysisContext evt, int indent)
377
    {
378
        //Custom attributes for event. Includes a trailing newline
379
        AppendCustomAttributes(sb, evt, indent);
15✔
380

381
        //Event declaration line
382
        sb.Append('\t', indent);
15✔
383
        sb.Append(CsFileUtils.GetKeyWordsForEvent(evt));
15✔
384
        sb.Append(' ');
15✔
385
        sb.Append(CsFileUtils.GetTypeName(evt.EventType));
15✔
386
        sb.Append(' ');
15✔
387
        sb.Append(evt.Name).AppendLine();
15✔
388
        sb.Append('\t', indent);
15✔
389
        sb.Append('{');
15✔
390
        sb.AppendLine();
15✔
391

392
        //Add/Remove/Invoke
393
        indent++;
15✔
394
        if (evt.Adder != null)
15✔
395
            AppendAccessor(sb, evt.Adder, "add", indent);
15✔
396
        if (evt.Remover != null)
15✔
397
            AppendAccessor(sb, evt.Remover, "remove", indent);
15✔
398
        if (evt.Invoker != null)
15!
399
            AppendAccessor(sb, evt.Invoker, "fire", indent);
×
400
        indent--;
15✔
401

402
        sb.Append('\t', indent);
15✔
403
        sb.Append('}');
15✔
404
        sb.AppendLine().AppendLine();
15✔
405
    }
15✔
406

407
    private static void AppendProperty(StringBuilder sb, PropertyAnalysisContext prop, int indent)
408
    {
409
        //Custom attributes for property. Includes a trailing newline
410
        AppendCustomAttributes(sb, prop, indent);
5,730✔
411

412
        //Property declaration line
413
        sb.Append('\t', indent);
5,730✔
414
        sb.Append(CsFileUtils.GetKeyWordsForProperty(prop));
5,730✔
415
        sb.Append(' ');
5,730✔
416
        sb.Append(CsFileUtils.GetTypeName(prop.PropertyType));
5,730✔
417
        sb.Append(' ');
5,730✔
418
        sb.Append(prop.Name);
5,730✔
419
        sb.AppendLine();
5,730✔
420
        sb.Append('\t', indent);
5,730✔
421
        sb.Append('{');
5,730✔
422
        sb.AppendLine();
5,730✔
423

424
        //Get/Set
425
        indent++;
5,730✔
426
        if (prop.Getter != null)
5,730✔
427
            AppendAccessor(sb, prop.Getter, "get", indent);
5,433✔
428
        if (prop.Setter != null)
5,730✔
429
            AppendAccessor(sb, prop.Setter, "set", indent);
801✔
430
        indent--;
5,730✔
431

432
        sb.Append('\t', indent);
5,730✔
433
        sb.Append('}');
5,730✔
434
        sb.AppendLine().AppendLine();
5,730✔
435
    }
5,730✔
436

437
    private static void AppendMethod(StringBuilder sb, MethodAnalysisContext method, int indent)
438
    {
439
        if (method is InjectedMethodAnalysisContext)
37,227!
440
            return;
×
441

442
        //Custom attributes for method. Includes a trailing newline
443
        AppendCustomAttributes(sb, method, indent);
37,227✔
444

445
        //Method declaration line
446
        sb.Append('\t', indent);
37,227✔
447
        sb.Append(CsFileUtils.GetKeyWordsForMethod(method));
37,227✔
448
        sb.Append(' ');
37,227✔
449
        if (method.Name is not ".ctor" and not ".cctor")
37,227✔
450
        {
451
            sb.Append(CsFileUtils.GetTypeName(method.ReturnType));
31,050✔
452
            sb.Append(' ');
31,050✔
453
            sb.Append(method.Name);
31,050✔
454
        }
455
        else
456
        {
457
            //Constructor: emit the simple type name (C# ctor syntax), NOT the TypeAnalysisContext whose
458
            //ToString() is "Type: <FullName>". GetTypeName strips generic-arity backticks.
459
            sb.Append(CsFileUtils.GetTypeName(method.DeclaringType!));
6,177✔
460
        }
461

462
        sb.Append('(');
37,227✔
463
        sb.Append(CsFileUtils.GetMethodParameterString(method));
37,227✔
464
        sb.Append(") { }");
37,227✔
465

466
        if (IncludeMethodLength)
37,227!
467
        {
468
            sb.Append(" //Length: ");
×
469
            sb.Append(method.RawBytes.Length);
×
470
        }
471

472
        sb.AppendLine().AppendLine();
37,227✔
473
    }
37,227✔
474

475
    //get/set/add/remove/raise
476
    private static void AppendAccessor(StringBuilder sb, MethodAnalysisContext accessor, string accessorType, int indent)
477
    {
478
        //Custom attributes for accessor. Includes a trailing newline
479
        AppendCustomAttributes(sb, accessor, indent);
6,264✔
480

481
        sb.Append('\t', indent);
6,264✔
482
        sb.Append(CsFileUtils.GetKeyWordsForMethod(accessor, true, true));
6,264✔
483
        sb.Append(' ');
6,264✔
484
        sb.Append(accessorType);
6,264✔
485
        sb.Append(" { } //Length: ");
6,264✔
486
        sb.Append(accessor.RawBytes.Length);
6,264✔
487
        sb.AppendLine();
6,264✔
488
    }
6,264✔
489

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

493
    /// <summary>Format a default/enum constant value culture-invariantly, so float/double literals render as
494
    /// `0.5` (not `0,5` on a comma-decimal locale, which is invalid C# and breaks diffability across machines).</summary>
495
    private static string InvariantValue(object? value)
496
        // null-safe: an enum member / const with no default-value blob (obfuscated metadata) passes null here;
497
        // the old `StringBuilder.Append(object)` tolerated it, so we must too (else `null.ToString()` -> NRE).
498
        => value is null ? "" : value is System.IFormattable f ? f.ToString(null, System.Globalization.CultureInfo.InvariantCulture) : value.ToString() ?? "";
10,314!
499
}
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