• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

zorbathut / dec / 11676005847

05 Nov 2024 01:46AM UTC coverage: 90.487% (+0.4%) from 90.088%
11676005847

push

github

zorbathut
Unit tests for Path.

4642 of 5130 relevant lines covered (90.49%)

190952.06 hits per line

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

88.21
/src/Serialization.cs
1
using System;
2
using System.Collections;
3
using System.Collections.Generic;
4
using System.ComponentModel;
5
using System.Linq;
6
using System.Reflection;
7

8
namespace Dec
9
{
10
    /// <summary>
11
    /// Internal serialization utilities.
12
    /// </summary>
13
    internal static class Serialization
14
    {
15
        // Initialize it to empty in order to support Recorder operations without Dec initialization.
16
        // At some point we'll figure out how to support Converters at that point as well.
17
        internal static bool ConverterInitialized = false;
10✔
18
        internal static System.Collections.Concurrent.ConcurrentDictionary<Type, Converter> ConverterObjects = new System.Collections.Concurrent.ConcurrentDictionary<Type, Converter>();
10✔
19
        internal static System.Collections.Concurrent.ConcurrentDictionary<Type, Type> ConverterGenericPrototypes = new System.Collections.Concurrent.ConcurrentDictionary<Type, Type>();
10✔
20

21
        internal class ConverterNullableString<T> : ConverterString<T?> where T : struct
22
        {
23
            private ConverterString<T> child;
24

25
            public ConverterNullableString(ConverterString<T> child)
25✔
26
            {
25✔
27
                this.child = child;
25✔
28
            }
25✔
29

30
            public override string Write(T? input)
31
            {
×
32
                if (!input.HasValue)
×
33
                {
×
34
                    Dbg.Err("Internal error: ConverterNullableString.Write called with null value; this should never happen");
×
35
                    return "";
×
36
                }
37

38
                return child.Write(input.Value);
×
39
            }
×
40

41
            public override T? Read(string input, Context context)
42
            {
15✔
43
                // if we're null, we must have already handled this elsewhere
44
                return child.Read(input, context);
15✔
45
            }
15✔
46
        }
47

48
        internal class ConverterNullableRecord<T> : ConverterRecord<T?> where T : struct
49
        {
50
            private ConverterRecord<T> child;
51

52
            public ConverterNullableRecord(ConverterRecord<T> child)
25✔
53
            {
25✔
54
                this.child = child;
25✔
55
            }
25✔
56

57
            public override void Record(ref T? input, Recorder recorder)
58
            {
15✔
59
                if (recorder.Mode == Recorder.Direction.Write)
15✔
60
                {
×
61
                    if (!input.HasValue)
×
62
                    {
×
63
                        Dbg.Err("Internal error: ConverterNullableRecord called with null value in write mode; this should never happen");
×
64
                        return;
×
65
                    }
66

67
                    var value = input.Value;
×
68
                    child.Record(ref value, recorder);
×
69
                }
×
70
                else if (recorder.Mode == Recorder.Direction.Read)
15✔
71
                {
15✔
72
                    T value = default;
15✔
73
                    child.Record(ref value, recorder);
15✔
74
                    input = value;
15✔
75
                }
15✔
76
                else
77
                {
×
78
                    Dbg.Err("Internal error: ConverterNullableRecord called with unknown mode; this should never happen");
×
79
                }
×
80
            }
15✔
81
        }
82

83
        internal class ConverterNullableFactory<T> : ConverterFactory<T?> where T : struct
84
        {
85
            private ConverterFactory<T> child;
86

87
            public ConverterNullableFactory(ConverterFactory<T> child)
25✔
88
            {
25✔
89
                this.child = child;
25✔
90
            }
25✔
91

92
            public override T? Create(Recorder recorder)
93
            {
15✔
94
                return child.Create(recorder);
15✔
95
            }
15✔
96

97
            public override void Read(ref T? input, Recorder recorder)
98
            {
15✔
99
                if (!input.HasValue)
15✔
100
                {
×
101
                    Dbg.Err("Internal error: ConverterNullableFactory.Read called with null value; this should never happen");
×
102
                    return;
×
103
                }
104

105
                var value = input.Value;
15✔
106
                child.Read(ref value, recorder);
15✔
107
                input = value;
15✔
108
            }
15✔
109

110
            public override void Write(T? input, Recorder recorder)
111
            {
×
112
                if (!input.HasValue)
×
113
                {
×
114
                    Dbg.Err("Internal error: ConverterNullableFactory.Write called with null value; this should never happen");
×
115
                    return;
×
116
                }
117

118
                child.Write(input.Value, recorder);
×
119
            }
×
120
        }
121

122
        internal static Converter ConverterFor(Type inputType)
123
        {
5,335,691✔
124
            if (ConverterObjects.TryGetValue(inputType, out var converter))
5,335,691✔
125
            {
5,290,309✔
126
                return converter;
5,290,309✔
127
            }
128

129
            // check for Nullable
130
            if (inputType.IsConstructedGenericType && inputType.GetGenericTypeDefinition() == typeof(Nullable<>))
45,382✔
131
            {
200✔
132
                var nullableType = inputType.GenericTypeArguments[0];
200✔
133

134
                // handle all types of converters
135
                var originalConverter = ConverterFor(nullableType);
200✔
136
                if (originalConverter is ConverterString nullableStringConverter)
200✔
137
                {
25✔
138
                    var nullableConverterType = typeof(ConverterNullableString<>).MakeGenericType(nullableType);
25✔
139
                    var nullableConverter = (ConverterString)Activator.CreateInstance(nullableConverterType, new object[] { originalConverter });
25✔
140
                    ConverterObjects[inputType] = nullableConverter;
25✔
141
                    return nullableConverter;
25✔
142
                }
143
                else if (originalConverter is ConverterRecord nullableRecordConverter)
175✔
144
                {
25✔
145
                    var nullableConverterType = typeof(ConverterNullableRecord<>).MakeGenericType(nullableType);
25✔
146
                    var nullableConverter = (ConverterRecord)Activator.CreateInstance(nullableConverterType, new object[] { originalConverter });
25✔
147
                    ConverterObjects[inputType] = nullableConverter;
25✔
148
                    return nullableConverter;
25✔
149
                }
150
                else if (originalConverter is ConverterFactory nullableFactoryConverter)
150✔
151
                {
25✔
152
                    var nullableConverterType = typeof(ConverterNullableFactory<>).MakeGenericType(nullableType);
25✔
153
                    var nullableConverter = (ConverterFactory)Activator.CreateInstance(nullableConverterType, new object[] { originalConverter });
25✔
154
                    ConverterObjects[inputType] = nullableConverter;
25✔
155
                    return nullableConverter;
25✔
156
                }
157
                else if (originalConverter != null)
125✔
158
                {
×
159
                    Dbg.Err($"Found converter {originalConverter} which is not a string, record, or factory converter. This is not allowed.");
×
160
                }
×
161
            }
125✔
162

163
            if (inputType.IsConstructedGenericType)
45,307✔
164
            {
8,267✔
165
                var genericType = inputType.GetGenericTypeDefinition();
8,267✔
166
                if (ConverterGenericPrototypes.TryGetValue(genericType, out var converterType))
8,267✔
167
                {
50✔
168
                    // construct `prototype` with the same generic arguments that `type` has
169
                    var concreteConverterType = converterType.MakeGenericType(inputType.GenericTypeArguments);
50✔
170
                    converter = (Converter)concreteConverterType.CreateInstanceSafe("converter", null);
50✔
171

172
                    // yes, do this even if it's null
173
                    ConverterObjects[inputType] = converter;
50✔
174

175
                    return converter;
50✔
176
                }
177
                else
178
                {
8,217✔
179
                    // stub it out so we can do the fast path next time
180
                    ConverterObjects[inputType] = null;
8,217✔
181
                }
8,217✔
182
            }
8,217✔
183

184
            var factoriedConverter = Config.ConverterFactory?.Invoke(inputType);
45,257✔
185
            ConverterObjects[inputType] = factoriedConverter;   // cache this so we don't generate a million of them
45,257✔
186
            return factoriedConverter;
45,257✔
187
        }
5,335,691✔
188

189

190
        internal static void Initialize()
191
        {
18,940✔
192
            if (ConverterInitialized)
18,940✔
193
            {
4,165✔
194
                return;
4,165✔
195
            }
196

197
            // this is here just so we don't keep thrashing if something breaks
198
            ConverterInitialized = true;
14,775✔
199

200
            ConverterObjects = new System.Collections.Concurrent.ConcurrentDictionary<Type, Converter>();
14,775✔
201

202
            IEnumerable<Type> conversionTypes;
203
            if (Config.TestParameters == null)
14,775✔
204
            {
5✔
205
                conversionTypes = UtilReflection.GetAllUserTypes().Where(t => t.IsSubclassOf(typeof(Converter)));
7,110✔
206
            }
5✔
207
            else if (Config.TestParameters.explicitConverters != null)
14,770✔
208
            {
2,710✔
209
                conversionTypes = Config.TestParameters.explicitConverters;
2,710✔
210
            }
2,710✔
211
            else
212
            {
12,060✔
213
                conversionTypes = Enumerable.Empty<Type>();
12,060✔
214
            }
12,060✔
215

216
            foreach (var type in conversionTypes)
45,820✔
217
            {
755✔
218
                if (type.IsAbstract)
755✔
219
                {
5✔
220
                    Dbg.Err($"Found converter {type} which is abstract. This is not allowed.");
5✔
221
                    continue;
5✔
222
                }
223

224
                if (type.IsGenericType)
750✔
225
                {
30✔
226
                    var baseConverterType = type;
30✔
227
                    while (baseConverterType.BaseType != typeof(ConverterString) && baseConverterType.BaseType != typeof(ConverterRecord) && baseConverterType.BaseType != typeof(ConverterFactory))
60✔
228
                    {
30✔
229
                        baseConverterType = baseConverterType.BaseType;
30✔
230
                    }
30✔
231

232
                    // we are now, presumably, at ConverterString<T> or ConverterRecord<T> or ConverterFactory<T>
233
                    // this *really* needs more error checking
234
                    Type converterTarget = baseConverterType.GenericTypeArguments[0];
30✔
235

236
                    if (!converterTarget.IsGenericType)
30✔
237
                    {
5✔
238
                        Dbg.Err($"Found generic converter {type} which is not referring to a generic constructed type.");
5✔
239
                        continue;
5✔
240
                    }
241

242
                    converterTarget = converterTarget.GetGenericTypeDefinition();
25✔
243
                    if (ConverterGenericPrototypes.ContainsKey(converterTarget))
25✔
244
                    {
×
245
                        Dbg.Err($"Found multiple converters for {converterTarget}: {ConverterGenericPrototypes[converterTarget]} and {type}");
×
246
                    }
×
247

248
                    ConverterGenericPrototypes[converterTarget] = type;
25✔
249
                    continue;
25✔
250
                }
251

252
                var converter = (Converter)type.CreateInstanceSafe("converter", null);
720✔
253
                if (converter != null && (converter is ConverterString || converter is ConverterRecord || converter is ConverterFactory))
720✔
254
                {
715✔
255
                    Type convertedType = converter.GetConvertedTypeHint();
715✔
256
                    if (ConverterObjects.ContainsKey(convertedType))
715✔
257
                    {
5✔
258
                        Dbg.Err($"Found multiple converters for {convertedType}: {ConverterObjects[convertedType]} and {type}");
5✔
259
                    }
5✔
260

261
                    ConverterObjects[convertedType] = converter;
715✔
262
                    continue;
715✔
263
                }
264
            }
5✔
265
        }
18,940✔
266

267
        internal static object GenerateResultFallback(object model, Type type)
268
        {
270✔
269
            if (model != null)
270✔
270
            {
5✔
271
                return model;
5✔
272
            }
273
            else if (type.IsValueType)
265✔
274
            {
165✔
275
                // We don't need Safe here because all value types are required to have a default constructor.
276
                return Activator.CreateInstance(type);
165✔
277
            }
278
            else
279
            {
100✔
280
                return null;
100✔
281
            }
282
        }
270✔
283

284
        internal enum ParseMode
285
        {
286
            Default,
287
            Replace,
288
            Patch,
289
            Append,
290

291
            // Dec-only
292
            Create,
293
            CreateOrReplace,
294
            CreateOrPatch,
295
            CreateOrIgnore,
296
            Delete,
297
            ReplaceIfExists,
298
            PatchIfExists,
299
            DeleteIfExists,
300
        }
301
        internal static ParseMode ParseModeFromString(Context context, string str)
302
        {
3,115,223✔
303
            if (str == null)
3,115,223✔
304
            {
3,108,713✔
305
                return ParseMode.Default;
3,108,713✔
306
            }
307
            else if (str == "replace")
6,510✔
308
            {
1,020✔
309
                return ParseMode.Replace;
1,020✔
310
            }
311
            else if (str == "patch")
5,490✔
312
            {
2,180✔
313
                return ParseMode.Patch;
2,180✔
314
            }
315
            else if (str == "append")
3,310✔
316
            {
950✔
317
                return ParseMode.Append;
950✔
318
            }
319
            else if (str == "create")
2,360✔
320
            {
320✔
321
                return ParseMode.Create;
320✔
322
            }
323
            else if (str == "createOrReplace")
2,040✔
324
            {
180✔
325
                return ParseMode.CreateOrReplace;
180✔
326
            }
327
            else if (str == "createOrPatch")
1,860✔
328
            {
200✔
329
                return ParseMode.CreateOrPatch;
200✔
330
            }
331
            else if (str == "createOrIgnore")
1,660✔
332
            {
100✔
333
                return ParseMode.CreateOrIgnore;
100✔
334
            }
335
            else if (str == "delete")
1,560✔
336
            {
240✔
337
                return ParseMode.Delete;
240✔
338
            }
339
            else if (str == "replaceIfExists")
1,320✔
340
            {
160✔
341
                return ParseMode.ReplaceIfExists;
160✔
342
            }
343
            else if (str == "patchIfExists")
1,160✔
344
            {
180✔
345
                return ParseMode.PatchIfExists;
180✔
346
            }
347
            else if (str == "deleteIfExists")
980✔
348
            {
140✔
349
                return ParseMode.DeleteIfExists;
140✔
350
            }
351
            else
352
            {
840✔
353
                Dbg.Err($"{context}: Invalid `{str}` mode!");
840✔
354

355
                return ParseMode.Default;
840✔
356
            }
357
        }
3,115,223✔
358

359
        internal enum ParseCommand
360
        {
361
            Replace,
362
            Patch,
363
            Append,
364
        }
365
        internal static List<(ParseCommand command, ReaderNodeParseable node)> CompileOrders(UtilType.ParseModeCategory modeCategory, List<ReaderNodeParseable> nodes)
366
        {
1,511,714✔
367
            var orders = new List<(ParseCommand command, ReaderNodeParseable payload)>();
1,511,714✔
368

369
            if (modeCategory == UtilType.ParseModeCategory.Dec)
1,511,714✔
370
            {
×
371
                Dbg.Err($"Internal error: CompileOrders called with Dec mode category, this should never happen! Please report it.");
×
372
                return orders;
×
373
            }
374

375
            foreach (var node in nodes)
7,558,570✔
376
            {
1,511,714✔
377
                var context = node.GetContext();
1,511,714✔
378
                var s_parseMode = ParseModeFromString(context, node.GetMetadata(ReaderNodeParseable.Metadata.Mode));
1,511,714✔
379

380
                ParseCommand s_parseCommand;
381

382
                switch (modeCategory)
1,511,714✔
383
                {
384
                    case UtilType.ParseModeCategory.Object:
385
                        switch (s_parseMode)
943,503✔
386
                        {
387
                            default:
388
                                Dbg.Err($"{context}: Invalid mode {s_parseMode} provided for an Object-type parse, defaulting to Patch");
240✔
389
                                goto case ParseMode.Default;
240✔
390

391
                            case ParseMode.Default:
392
                            case ParseMode.Patch:
393
                                s_parseCommand = ParseCommand.Patch;
943,503✔
394
                                break;
943,503✔
395
                        }
396
                        break;
943,503✔
397
                    case UtilType.ParseModeCategory.OrderedContainer:
398
                        switch (s_parseMode)
131,045✔
399
                        {
400
                            default:
401
                                Dbg.Err($"{context}: Invalid mode {s_parseMode} provided for an ordered-container-type parse, defaulting to Replace");
80✔
402
                                goto case ParseMode.Default;
80✔
403

404
                            case ParseMode.Default:
405
                            case ParseMode.Replace:
406
                                s_parseCommand = ParseCommand.Replace;
130,910✔
407
                                break;
130,910✔
408

409
                            case ParseMode.Append:
410
                                s_parseCommand = ParseCommand.Append;
135✔
411
                                break;
135✔
412
                        }
413
                        break;
131,045✔
414
                    case UtilType.ParseModeCategory.UnorderedContainer:
415
                        switch (s_parseMode)
35,680✔
416
                        {
417
                            default:
418
                                Dbg.Err($"{context}: Invalid mode {s_parseMode} provided for an unordered-container-type parse, defaulting to Replace");
×
419
                                goto case ParseMode.Default;
×
420

421
                            case ParseMode.Default:
422
                            case ParseMode.Replace:
423
                                s_parseCommand = ParseCommand.Replace;
35,280✔
424
                                break;
35,280✔
425

426
                            case ParseMode.Patch:
427
                                s_parseCommand = ParseCommand.Patch;
240✔
428
                                break;
240✔
429

430
                            case ParseMode.Append:
431
                                s_parseCommand = ParseCommand.Append;
160✔
432
                                break;
160✔
433
                        }
434

435
                        break;
35,680✔
436
                    case UtilType.ParseModeCategory.Value:
437
                        switch (s_parseMode)
401,486✔
438
                        {
439
                            default:
440
                                Dbg.Err($"{context}: Invalid mode {s_parseMode} provided for a value-type parse, defaulting to Replace");
80✔
441
                                goto case ParseMode.Default;
80✔
442

443
                            case ParseMode.Default:
444
                            case ParseMode.Replace:
445
                                s_parseCommand = ParseCommand.Replace;
401,486✔
446
                                break;
401,486✔
447
                        }
448
                        break;
401,486✔
449
                    default:
450
                        Dbg.Err($"{context}: Internal error, unknown mode category {modeCategory}, please report");
×
451
                        s_parseCommand = ParseCommand.Patch;  // . . . I guess?
×
452
                        break;
×
453
                }
454

455
                if (s_parseCommand == ParseCommand.Replace)
1,511,714✔
456
                {
567,676✔
457
                    orders.Clear();
567,676✔
458
                }
567,676✔
459

460
                orders.Add((s_parseCommand, node));
1,511,714✔
461
            }
1,511,714✔
462

463
            return orders;
1,511,714✔
464
        }
1,511,714✔
465

466
        internal static List<ReaderFileDec.ReaderDec> CompileDecOrders(List<ReaderFileDec.ReaderDec> decs)
467
        {
45,185✔
468
            var orders = new List<ReaderFileDec.ReaderDec>();
45,185✔
469
            bool everExisted = false;
45,185✔
470
            foreach (var item in decs)
228,165✔
471
            {
46,305✔
472
                var s_parseMode = ParseModeFromString(item.context, item.node.GetMetadata(ReaderNodeParseable.Metadata.Mode));
46,305✔
473

474
                switch (s_parseMode)
46,305✔
475
                {
476
                    default:
477
                        Dbg.Err($"{item.context}: Invalid mode {s_parseMode} provided for a Dec-type parse, defaulting to Create");
20✔
478
                        goto case ParseMode.Default;
20✔
479

480
                    case ParseMode.Default:
481
                    case ParseMode.Create:
482
                        if (orders.Count != 0)
45,185✔
483
                        {
80✔
484
                            Dbg.Err($"{item.context}: Create mode used when a Dec already exists, falling back to Patch");
80✔
485
                            goto case ParseMode.Patch;
80✔
486
                        }
487
                        orders.Add(item);
45,105✔
488
                        everExisted = true;
45,105✔
489
                        break;
45,105✔
490

491
                    case ParseMode.Replace:
492
                        if (orders.Count == 0)
100✔
493
                        {
40✔
494
                            Dbg.Err($"{item.context}: Replace mode used when a Dec doesn't exist, falling back to Create");
40✔
495
                            goto case ParseMode.Create;
40✔
496
                        }
497
                        orders.Clear();
60✔
498
                        orders.Add(item);
60✔
499
                        break;
60✔
500

501
                    case ParseMode.Patch:
502
                        if (orders.Count == 0)
420✔
503
                        {
60✔
504
                            Dbg.Err($"{item.context}: Patch mode used when a Dec doesn't exist, falling back to Create");
60✔
505
                            goto case ParseMode.Create;
60✔
506
                        }
507
                        orders.Add(item);
360✔
508
                        break;
360✔
509

510
                    case ParseMode.CreateOrReplace:
511
                        // doesn't matter if we have a thing or not
512
                        orders.Clear();
80✔
513
                        orders.Add(item);
80✔
514
                        everExisted = true;
80✔
515
                        break;
80✔
516

517
                    case ParseMode.CreateOrPatch:
518
                        // doesn't matter if we have a thing or not
519
                        orders.Add(item);
80✔
520
                        everExisted = true;
80✔
521
                        break;
80✔
522

523
                    case ParseMode.CreateOrIgnore:
524
                        if (orders.Count == 0)
80✔
525
                        {
20✔
526
                            orders.Add(item);
20✔
527
                            everExisted = true;
20✔
528
                        }
20✔
529
                        break;
80✔
530

531
                    case ParseMode.Delete:
532
                        if (!everExisted)
240✔
533
                        {
20✔
534
                            Dbg.Err($"{item.context}: Delete mode used when a Dec doesn't exist; did you want deleteIfExists?");
20✔
535
                        }
20✔
536
                        orders.Clear();
240✔
537
                        break;
240✔
538

539
                    case ParseMode.ReplaceIfExists:
540
                        if (orders.Count != 0)
80✔
541
                        {
60✔
542
                            orders.Clear();
60✔
543
                            orders.Add(item);
60✔
544
                        }
60✔
545
                        break;
80✔
546

547
                    case ParseMode.PatchIfExists:
548
                        if (orders.Count != 0)
80✔
549
                        {
60✔
550
                            orders.Add(item);
60✔
551
                        }
60✔
552
                        break;
80✔
553

554
                    case ParseMode.DeleteIfExists:
555
                        orders.Clear();
140✔
556
                        break;
140✔
557
                }
558
            }
46,305✔
559

560
            return orders;
45,185✔
561
        }
45,185✔
562

563
        internal static object ParseElement(List<ReaderNodeParseable> nodes, Type type, object original, ReaderGlobals globals, Recorder.Settings recSettings, FieldInfo fieldInfo = null, bool isRootDec = false, bool hasReferenceId = false, bool asThis = false, List<(ParseCommand command, ReaderNodeParseable node)> ordersOverride = null)
564
        {
1,555,779✔
565
            if (nodes == null || nodes.Count == 0)
1,555,779✔
566
            {
×
567
                Dbg.Err("Internal error, Dec failed to provide nodes to ParseElement. Please report this!");
×
568
                return original;
×
569
            }
570

571
            if (!globals.allowReflection && nodes.Count > 1)
1,555,779✔
572
            {
×
573
                Dbg.Err("Internal error, multiple nodes provided for recorder-mode behavior. Please report this!");
×
574
            }
×
575

576
            // We keep the original around in case of error, but do all our manipulation on a result object.
577
            object result = original;
1,555,779✔
578

579
            // Verify our Shared flags as the *very* first step to ensure nothing gets past us.
580
            // In theory this should be fine with Flexible; Flexible only happens on an outer wrapper that was shared, and therefore was null, and therefore this is default also
581
            if (recSettings.shared == Recorder.Settings.Shared.Allow)
1,555,779✔
582
            {
808,100✔
583
                if (!type.CanBeShared())
808,100✔
584
                {
100✔
585
                    // If shared, make sure our input is null and our type is appropriate for sharing
586
                    Dbg.Wrn($"{nodes[0].GetContext()}: Value type `{type}` tagged as Shared in recorder, this is meaningless but harmless");
100✔
587
                }
100✔
588
                else if (original != null && !hasReferenceId)
808,000✔
589
                {
25✔
590
                    // We need to create objects without context if it's shared, so we kind of panic in this case
591
                    Dbg.Err($"{nodes[0].GetContext()}: Shared `{type}` provided with non-null default object, this may result in unexpected behavior");
25✔
592
                }
25✔
593
            }
808,100✔
594

595
            // The next thing we do is parse all our attributes. This is because we want to verify that there are no attributes being ignored.
596

597
            // Validate all combinations here
598
            // This could definitely be more efficient and skip at least one traversal pass
599
            foreach (var s_node in nodes)
7,782,255✔
600
            {
1,557,459✔
601
                string nullAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Null);
1,557,459✔
602
                string refAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Ref);
1,557,459✔
603
                string classAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Class);
1,557,459✔
604
                string modeAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Mode);
1,557,459✔
605

606
                // Some of these are redundant and that's OK
607
                if (nullAttribute != null && (refAttribute != null || classAttribute != null || modeAttribute != null))
1,557,459✔
608
                {
180✔
609
                    Dbg.Err($"{s_node.GetContext()}: Null element may not have ref, class, or mode specified; guessing wildly at intentions");
180✔
610
                }
180✔
611
                else if (refAttribute != null && (nullAttribute != null || classAttribute != null || modeAttribute != null))
1,557,279✔
612
                {
75✔
613
                    Dbg.Err($"{s_node.GetContext()}: Ref element may not have null, class, or mode specified; guessing wildly at intentions");
75✔
614
                }
75✔
615
                else if (classAttribute != null && (nullAttribute != null || refAttribute != null))
1,557,204✔
616
                {
×
617
                    Dbg.Err($"{s_node.GetContext()}: Class-specified element may not have null or ref specified; guessing wildly at intentions");
×
618
                }
×
619
                else if (modeAttribute != null && (nullAttribute != null || refAttribute != null))
1,557,204✔
620
                {
×
621
                    Dbg.Err($"{s_node.GetContext()}: Mode-specified element may not have null or ref specified; guessing wildly at intentions");
×
622
                }
×
623

624
                var unrecognized = s_node.GetMetadataUnrecognized();
1,557,459✔
625
                if (unrecognized != null)
1,557,459✔
626
                {
65✔
627
                    Dbg.Err($"{s_node.GetContext()}: Has unknown attributes {unrecognized}");
65✔
628
                }
65✔
629
            }
1,557,459✔
630

631
            // Doesn't mean anything outside recorderMode, so we check it for validity just in case
632
            string refKey;
633
            ReaderNode refKeyNode = null; // stored entirely for error reporting
1,555,779✔
634
            if (!globals.allowRefs)
1,555,779✔
635
            {
608,124✔
636
                refKey = null;
608,124✔
637
                foreach (var s_node in nodes)
3,043,980✔
638
                {
609,804✔
639
                    string nodeRefAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Ref);
609,804✔
640
                    if (nodeRefAttribute != null)
609,804✔
641
                    {
200✔
642
                        Dbg.Err($"{s_node.GetContext()}: Found a reference tag while not evaluating Recorder mode, ignoring it");
200✔
643
                    }
200✔
644
                }
609,804✔
645
            }
608,124✔
646
            else
647
            {
947,655✔
648
                (refKey, refKeyNode) = nodes.Select(node => (node.GetMetadata(ReaderNodeParseable.Metadata.Ref), node)).Where(anp => anp.Item1 != null).LastOrDefault();
2,842,965✔
649
            }
947,655✔
650

651
            // First figure out type. We actually need type to be set before we can properly analyze and validate the mode flags.
652
            // If we're in an asThis block, it refers to the outer item, not the inner item; just skip this entirely
653
            bool isNull = false;
1,555,779✔
654
            if (!asThis)
1,555,779✔
655
            {
1,555,524✔
656
                string classAttribute = null;
1,555,524✔
657
                ReaderNode classAttributeNode = null; // stored entirely for error reporting
1,555,524✔
658
                bool replaced = false;
1,555,524✔
659
                foreach (var s_node in nodes)
7,780,980✔
660
                {
1,557,204✔
661
                    // However, we do need to watch for Replace, because that means we should nuke the class attribute and start over.
662
                    string modeAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Mode);
1,557,204✔
663
                    ParseMode s_parseMode = ParseModeFromString(s_node.GetContext(), modeAttribute);
1,557,204✔
664
                    if (s_parseMode == ParseMode.Replace)
1,557,204✔
665
                    {
520✔
666
                        // we also should maybe be doing this if we're a list, map, or set?
667
                        classAttribute = null;
520✔
668
                        replaced = true;
520✔
669
                    }
520✔
670

671
                    // if we get nulled, we kill the class tag and basically treat it like a delete
672
                    // but we also reset the null tag on every entry
673
                    isNull = false;
1,557,204✔
674
                    string nullAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Null);
1,557,204✔
675
                    if (nullAttribute != null)
1,557,204✔
676
                    {
249,667✔
677
                        if (!bool.TryParse(nullAttribute, out bool nullValue))
249,667✔
678
                        {
×
679
                            Dbg.Err($"{s_node.GetContext()}: Invalid `null` attribute");
×
680
                        }
×
681
                        else if (nullValue)
249,667✔
682
                        {
249,627✔
683
                            isNull = true;
249,627✔
684
                        }
249,627✔
685
                    }
249,667✔
686

687
                    // update the class based on whatever this says
688
                    string localClassAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Class);
1,557,204✔
689
                    if (localClassAttribute != null)
1,557,204✔
690
                    {
207,911✔
691
                        classAttribute = localClassAttribute;
207,911✔
692
                        classAttributeNode = s_node;
207,911✔
693
                    }
207,911✔
694
                }
1,557,204✔
695

696
                if (classAttribute != null)
1,555,524✔
697
                {
207,911✔
698
                    var possibleType = (Type)ParseString(classAttribute, typeof(Type), null, classAttributeNode.GetContext());
207,911✔
699
                    if (!type.IsAssignableFrom(possibleType))
207,911✔
700
                    {
20✔
701
                        Dbg.Err($"{classAttributeNode.GetContext()}: Explicit type {classAttribute} cannot be assigned to expected type {type}");
20✔
702
                    }
20✔
703
                    else if (!replaced && result != null && result.GetType() != possibleType)
207,891✔
704
                    {
20✔
705
                        Dbg.Err($"{classAttributeNode.GetContext()}: Explicit type {classAttribute} does not match already-provided instance {type}");
20✔
706
                    }
20✔
707
                    else
708
                    {
207,871✔
709
                        type = possibleType;
207,871✔
710
                    }
207,871✔
711
                }
207,911✔
712
            }
1,555,524✔
713

714
            var converter = ConverterFor(type);
1,555,779✔
715

716
            // Now we traverse the Mode attributes as prep for our final parse pass.
717
            // ordersOverride makes `nodes` admittedly a little unnecessary.
718
            List<(ParseCommand command, ReaderNodeParseable node)> orders = ordersOverride ?? CompileOrders(type.CalculateSerializationModeCategory(converter, isRootDec), nodes);
1,555,779✔
719

720
            // Gather info
721
            bool hasChildren = false;
1,555,779✔
722
            ReaderNode hasChildrenNode = null;
1,555,779✔
723
            bool hasText = false;
1,555,779✔
724
            ReaderNode hasTextNode = null;
1,555,779✔
725
            foreach (var (_, node) in orders)
7,782,255✔
726
            {
1,557,459✔
727
                if (!hasChildren && node.HasChildren())
1,557,459✔
728
                {
479,371✔
729
                    hasChildren = true;
479,371✔
730
                    hasChildrenNode = node;
479,371✔
731
                }
479,371✔
732
                if (!hasText && node.GetText() != null)
1,557,459✔
733
                {
356,941✔
734
                    hasText = true;
356,941✔
735
                    hasTextNode = node;
356,941✔
736
                }
356,941✔
737
            }
1,557,459✔
738

739
            // Actually handle our attributes
740
            if (refKey != null)
1,555,779✔
741
            {
357,160✔
742
                // Ref is the highest priority, largely because I think it's cool
743

744
                if (recSettings.shared == Recorder.Settings.Shared.Deny)
357,160✔
745
                {
55✔
746
                    Dbg.Err($"{refKeyNode.GetContext()}: Found a reference in a non-.Shared() context; this should happen only if you've removed the .Shared() tag since the file was generated, or if you hand-wrote a file that is questionably valid. Using the reference anyway but this might produce unexpected results");
55✔
747
                }
55✔
748

749
                if (globals.refs == null)
357,160✔
750
                {
40✔
751
                    Dbg.Err($"{refKeyNode.GetContext()}: Found a reference object {refKey} before refs are initialized (is this being used in a ConverterFactory<>.Create()?)");
40✔
752
                    return result;
40✔
753
                }
754

755
                if (!globals.refs.ContainsKey(refKey))
357,120✔
756
                {
5✔
757
                    Dbg.Err($"{refKeyNode.GetContext()}: Found a reference object {refKey} without a valid reference mapping");
5✔
758
                    return result;
5✔
759
                }
760

761
                object refObject = globals.refs[refKey];
357,115✔
762
                if (refObject == null && !type.IsValueType)
357,115✔
763
                {
80✔
764
                    // okay, good enough
765
                    return refObject;
80✔
766
                }
767

768
                if (!type.IsAssignableFrom(refObject.GetType()))
357,035✔
769
                {
25✔
770
                    Dbg.Err($"{refKeyNode.GetContext()}: Reference object {refKey} is of type {refObject.GetType()}, which cannot be converted to expected type {type}");
25✔
771
                    return result;
25✔
772
                }
773

774
                return refObject;
357,010✔
775
            }
776
            else if (isNull)
1,198,619✔
777
            {
249,602✔
778
                return null;
249,602✔
779

780
                // Note: It may seem wrong that we can return null along with a non-null model.
781
                // The problem is that this is meant to be able to override defaults. If the default is an object, explicitly setting it to null *should* clear the object out.
782
                // If we actually need a specific object to be returned, for whatever reason, the caller has to do the comparison.
783
            }
784

785
            // Basic early validation
786

787
            if (hasChildren && hasText)
949,017✔
788
            {
15✔
789
                Dbg.Err($"{hasChildrenNode.GetContext()} / {hasTextNode.GetContext()}: Cannot have both text and child nodes in XML - this is probably a typo, maybe you have the wrong number of close tags or added text somewhere you didn't mean to?");
15✔
790

791
                // we'll just fall through and try to parse anyway, though
792
            }
15✔
793

794
            if (typeof(Dec).IsAssignableFrom(type) && hasChildren && !isRootDec)
949,017✔
795
            {
×
796
                Dbg.Err($"{hasChildrenNode.GetContext()}: Defining members of an item of type {type}, derived from Dec.Dec, is not supported within an outer Dec. Either reference a {type} defined independently or remove {type}'s inheritance from Dec.");
×
797
                return null;
×
798
            }
799

800
            // Defer off to converters, whatever they feel like doing
801
            if (converter != null)
949,017✔
802
            {
605✔
803
                // string converter
804
                if (converter is ConverterString converterString)
605✔
805
                {
260✔
806
                    foreach (var (parseCommand, node) in orders)
1,300✔
807
                    {
260✔
808
                        switch (parseCommand)
260✔
809
                        {
810
                            case ParseCommand.Replace:
811
                                // easy, done
812
                                break;
260✔
813

814
                            default:
815
                                Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
816
                                break;
×
817
                        }
818

819
                        if (hasChildren)
260✔
820
                        {
15✔
821
                            Dbg.Err($"{node.GetContext()}: String converter {converter.GetType()} called with child XML nodes, which will be ignored");
15✔
822
                        }
15✔
823

824
                        // We actually accept "no text" here, though, empty-string might be valid!
825

826
                        // context might be null; that's OK at the moment
827
                        try
828
                        {
260✔
829
                            result = converterString.ReadObj(node.GetText() ?? "", node.GetContext());
260✔
830
                        }
175✔
831
                        catch (Exception e)
85✔
832
                        {
85✔
833
                            Dbg.Ex(new ConverterReadException(node.GetContext(), converter, e));
85✔
834

835
                            result = GenerateResultFallback(result, type);
85✔
836
                        }
85✔
837
                    }
260✔
838
                }
260✔
839
                else if (converter is ConverterRecord converterRecord)
345✔
840
                {
235✔
841
                    foreach (var (parseCommand, node) in orders)
1,175✔
842
                    {
235✔
843
                        switch (parseCommand)
235✔
844
                        {
845
                            case ParseCommand.Patch:
846
                                // easy, done
847
                                break;
235✔
848

849
                            case ParseCommand.Replace:
850
                                result = null;
×
851
                                break;
×
852

853
                            default:
854
                                Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
855
                                break;
×
856
                        }
857

858
                        bool isNullable = type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
235✔
859

860
                        if (result == null && !isNullable)
235✔
861
                        {
130✔
862
                            result = type.CreateInstanceSafe("converterrecord", node);
130✔
863
                        }
130✔
864

865
                        // context might be null; that's OK at the moment
866
                        if (result != null || isNullable)
235✔
867
                        {
235✔
868
                            var recorderReader = new RecorderReader(node, globals, trackUsage: true);
235✔
869
                            try
870
                            {
235✔
871
                                object returnedResult = converterRecord.RecordObj(result, recorderReader);
235✔
872

873
                                if (!type.IsValueType && result != returnedResult)
220✔
874
                                {
×
875
                                    Dbg.Err($"{node.GetContext()}: Converter {converterRecord.GetType()} changed object instance, this is disallowed");
×
876
                                }
×
877
                                else
878
                                {
220✔
879
                                    // for value types, this is fine
880
                                    result = returnedResult;
220✔
881
                                }
220✔
882

883
                                recorderReader.ReportUnusedFields();
220✔
884
                            }
220✔
885
                            catch (Exception e)
15✔
886
                            {
15✔
887
                                Dbg.Ex(new ConverterReadException(node.GetContext(), converter, e));
15✔
888

889
                                // no fallback needed, we already have a result
890
                            }
15✔
891
                        }
235✔
892
                    }
235✔
893
                }
235✔
894
                else if (converter is ConverterFactory converterFactory)
110✔
895
                {
110✔
896
                    foreach (var (parseCommand, node) in orders)
550✔
897
                    {
110✔
898
                        switch (parseCommand)
110✔
899
                        {
900
                            case ParseCommand.Patch:
901
                                // easy, done
902
                                break;
110✔
903

904
                            case ParseCommand.Replace:
905
                                result = null;
×
906
                                break;
×
907

908
                            default:
909
                                Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
910
                                break;
×
911
                        }
912

913
                        var recorderReader = new RecorderReader(node, globals, disallowShared: true, trackUsage: true);
110✔
914
                        if (result == null)
110✔
915
                        {
105✔
916
                            try
917
                            {
105✔
918
                                result = converterFactory.CreateObj(recorderReader);
105✔
919
                            }
90✔
920
                            catch (Exception e)
15✔
921
                            {
15✔
922
                                Dbg.Ex(new ConverterReadException(node.GetContext(), converter, e));
15✔
923
                            }
15✔
924
                        }
105✔
925

926
                        // context might be null; that's OK at the moment
927
                        if (result != null)
110✔
928
                        {
95✔
929
                            recorderReader.AllowShared(globals);
95✔
930
                            try
931
                            {
95✔
932
                                result = converterFactory.ReadObj(result, recorderReader);
95✔
933
                                recorderReader.ReportUnusedFields();
80✔
934
                            }
80✔
935
                            catch (Exception e)
15✔
936
                            {
15✔
937
                                Dbg.Ex(new ConverterReadException(node.GetContext(), converter, e));
15✔
938

939
                                // no fallback needed, we already have a result
940
                            }
15✔
941
                        }
95✔
942
                    }
110✔
943
                }
110✔
944
                else
945
                {
×
946
                    Dbg.Err($"Somehow ended up with an unsupported converter {converter.GetType()}");
×
947
                }
×
948

949
                return result;
605✔
950
            }
951

952
            // All our standard text-using options
953
            // Placed before IRecordable just in case we have a Dec that is IRecordable
954
            if ((typeof(Dec).IsAssignableFrom(type) && !isRootDec) ||
948,412✔
955
                    type == typeof(Type) ||
948,412✔
956
                    type == typeof(string) ||
948,412✔
957
                    type.IsPrimitive ||
948,412✔
958
                    (TypeDescriptor.GetConverter(type)?.CanConvertFrom(typeof(string)) ?? false)   // this is last because it's slow
948,412✔
959
                )
948,412✔
960
            {
397,456✔
961
                foreach (var (parseCommand, node) in orders)
1,987,280✔
962
                {
397,456✔
963
                    switch (parseCommand)
397,456✔
964
                    {
965
                        case ParseCommand.Replace:
966
                            // easy, done
967
                            break;
397,456✔
968

969
                        default:
970
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
971
                            break;
×
972
                    }
973

974
                    if (hasChildren)
397,456✔
975
                    {
20✔
976
                        Dbg.Err($"{node.GetContext()}: Child nodes are not valid when parsing {type}");
20✔
977
                    }
20✔
978

979
                    result = ParseString(node.GetText(), type, result, node.GetContext());
397,456✔
980
                }
397,456✔
981

982
                return result;
397,456✔
983
            }
984

985
            // Special case: IRecordables
986
            IRecordable recordableBuffered = null;
550,956✔
987
            if (typeof(IRecordable).IsAssignableFrom(type))
550,956✔
988
            {
388,700✔
989
                // we're going to need to make one anyway so let's just go ahead and do that
990
                IRecordable recordable = null;
388,700✔
991

992
                if (result != null)
388,700✔
993
                {
206,705✔
994
                    recordable = (IRecordable)result;
206,705✔
995
                }
206,705✔
996
                else if (recSettings.factories == null)
181,995✔
997
                {
180,965✔
998
                    recordable = (IRecordable)type.CreateInstanceSafe("recordable", orders[0].node);
180,965✔
999
                }
180,965✔
1000
                else
1001
                {
1,030✔
1002
                    recordable = recSettings.CreateRecordableFromFactory(type, "recordable", orders[0].node);
1,030✔
1003
                }
1,030✔
1004

1005
                // we hold on to this so that, *if* we end up not using this object, we can optionally reuse it later for reflection
1006
                // in an ideal world we wouldn't create it at all in the first place, but we need to create it to call IConditionalRecordable's function
1007
                recordableBuffered = recordable;
388,700✔
1008

1009
                var conditionalRecordable = recordable as IConditionalRecordable;
388,700✔
1010
                if (conditionalRecordable == null || conditionalRecordable.ShouldRecord(nodes[0].UserSettings))
388,700✔
1011
                {
388,685✔
1012
                    foreach (var (parseCommand, node) in orders)
1,943,425✔
1013
                    {
388,685✔
1014
                        switch (parseCommand)
388,685✔
1015
                        {
1016
                            case ParseCommand.Patch:
1017
                                // easy, done
1018
                                break;
388,685✔
1019

1020
                            default:
1021
                                Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1022
                                break;
×
1023
                        }
1024

1025
                        if (recordable != null)
388,685✔
1026
                        {
388,665✔
1027
                            var recorderReader = new RecorderReader(node, globals, trackUsage: true);
388,665✔
1028
                            recordable.Record(recorderReader);
388,665✔
1029
                            recorderReader.ReportUnusedFields();
388,665✔
1030

1031
                            // TODO: support indices if this is within the Dec system?
1032
                        }
388,665✔
1033
                    }
388,685✔
1034

1035
                    result = recordable;
388,685✔
1036
                    return result;
388,685✔
1037
                }
1038

1039
                // otherwise we just fall through
1040
            }
15✔
1041

1042
            // Nothing past this point even supports text, so let's just get angry and break stuff.
1043
            if (hasText)
162,271✔
1044
            {
80✔
1045
                Dbg.Err($"{hasTextNode.GetContext()}: Text detected in a situation where it is invalid; will be ignored");
80✔
1046
                return result;
80✔
1047
            }
1048

1049
            // Special case: Lists
1050
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
162,191✔
1051
            {
46,125✔
1052
                foreach (var (parseCommand, node) in orders)
230,625✔
1053
                {
46,125✔
1054
                    switch (parseCommand)
46,125✔
1055
                    {
1056
                        case ParseCommand.Replace:
1057
                            // If you have a default list, but specify it in XML, we assume this is a full override. Clear the original list to cut down on GC churn.
1058
                            // TODO: Is some bozo going to store the same "constant" global list on init, then be surprised when we re-use the list instead of creating a new one? Detect this and yell about it I guess.
1059
                            // If you are reading this because you're the bozo, [insert angry emoji here], but also feel free to be annoyed that I haven't fixed it yet despite realizing it's a problem. Ping me on Discord, I'll take care of it, sorry 'bout that.
1060
                            if (result != null)
46,045✔
1061
                            {
400✔
1062
                                ((IList)result).Clear();
400✔
1063
                            }
400✔
1064
                            break;
46,045✔
1065

1066
                        case ParseCommand.Append:
1067
                            // we're good
1068
                            break;
80✔
1069

1070
                        default:
1071
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1072
                            break;
×
1073
                    }
1074

1075
                    // List<> handling
1076
                    Type referencedType = type.GetGenericArguments()[0];
46,125✔
1077

1078
                    var list = (IList)(result ?? Activator.CreateInstance(type));
46,125✔
1079

1080
                    node.ParseList(list, referencedType, globals, recSettings);
46,125✔
1081

1082
                    result = list;
46,125✔
1083
                }
46,125✔
1084

1085
                return result;
46,125✔
1086
            }
1087

1088
            // Special case: Arrays
1089
            if (type.IsArray)
116,066✔
1090
            {
2,495✔
1091
                Type referencedType = type.GetElementType();
2,495✔
1092

1093
                foreach (var (parseCommand, node) in orders)
12,475✔
1094
                {
2,495✔
1095
                    Array array = null;
2,495✔
1096
                    int startOffset = 0;
2,495✔
1097

1098
                    // This is a bit extra-complicated because we can't append stuff after the fact, we need to figure out what our length is when we create the object.
1099
                    switch (parseCommand)
2,495✔
1100
                    {
1101
                        case ParseCommand.Replace:
1102
                        {
2,460✔
1103
                            // This is a full override, so we're going to create it here.
1104
                            // It is actually vitally important that we fall back on the model when possible, because the Recorder Ref system requires it.
1105
                            bool match = result != null && result.GetType() == type;
2,460✔
1106
                            var arrayDimensions = node.GetArrayDimensions(type.GetArrayRank());
2,460✔
1107
                            if (match)
2,460✔
1108
                            {
410✔
1109
                                array = (Array)result;
410✔
1110
                                if (array.Rank != type.GetArrayRank())
410✔
1111
                                {
×
1112
                                    match = false;
×
1113
                                }
×
1114
                                else
1115
                                {
410✔
1116
                                    for (int i = 0; i < array.Rank; i++)
1,040✔
1117
                                    {
420✔
1118
                                        if (array.GetLength(i) != arrayDimensions[i])
420✔
1119
                                        {
310✔
1120
                                            match = false;
310✔
1121
                                            break;
310✔
1122
                                        }
1123
                                    }
110✔
1124
                                }
410✔
1125
                            }
410✔
1126

1127
                            if (!match)
2,460✔
1128
                            {
2,360✔
1129
                                // Otherwise just make a new one, no harm done.
1130
                                array = Array.CreateInstance(referencedType, arrayDimensions);
2,360✔
1131
                            }
2,360✔
1132

1133
                            break;
2,460✔
1134
                        }
1135

1136
                        case ParseCommand.Append:
1137
                        {
55✔
1138
                            if (result == null)
55✔
1139
                            {
20✔
1140
                                goto case ParseCommand.Replace;
20✔
1141
                            }
1142

1143
                            // This is jankier; we create it here with the intended final length, then copy the elements over, all because arrays can't be resized
1144
                            // (yes, I know, that's the point of arrays, I'm not complaining, just . . . grumbling a little)
1145
                            var oldArray = (Array)result;
35✔
1146
                            startOffset = oldArray.Length;
35✔
1147
                            var arrayDimensions = node.GetArrayDimensions(type.GetArrayRank());
35✔
1148
                            arrayDimensions[0] += startOffset;
35✔
1149
                            array = Array.CreateInstance(referencedType, arrayDimensions);
35✔
1150
                            if (arrayDimensions.Length == 1)
35✔
1151
                            {
20✔
1152
                                oldArray.CopyTo(array, 0);
20✔
1153
                            }
20✔
1154
                            else
1155
                            {
15✔
1156
                                // oy
1157
                                void CopyArray(Array source, Array destination, int[] indices, int rank = 0)
1158
                                {
60✔
1159
                                    if (rank < source.Rank)
60✔
1160
                                    {
45✔
1161
                                        for (int i = 0; i < source.GetLength(rank); i++)
180✔
1162
                                        {
45✔
1163
                                            indices[rank] = i;
45✔
1164
                                            CopyArray(source, destination, indices, rank + 1);
45✔
1165
                                        }
45✔
1166
                                    }
45✔
1167
                                    else
1168
                                    {
15✔
1169
                                        destination.SetValue(source.GetValue(indices), indices);
15✔
1170
                                    }
15✔
1171
                                }
60✔
1172

1173
                                {
15✔
1174
                                    int[] indices = new int[arrayDimensions.Length];
15✔
1175
                                    CopyArray(oldArray, array, indices, 0);
15✔
1176
                                }
15✔
1177
                            }
15✔
1178

1179
                            break;
35✔
1180
                        }
1181

1182
                        default:
1183
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1184
                            array = null; // just to break the unassigned-local-variable
×
1185
                            break;
×
1186
                    }
1187

1188
                    node.ParseArray(array, referencedType, globals, recSettings, startOffset);
2,495✔
1189

1190
                    result = array;
2,495✔
1191
                }
2,495✔
1192

1193
                return result;
2,495✔
1194
            }
1195

1196
            // Special case: Dictionaries
1197
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
113,571✔
1198
            {
23,390✔
1199
                foreach (var (parseCommand, node) in orders)
116,950✔
1200
                {
23,390✔
1201
                    bool permitPatch = false;
23,390✔
1202
                    switch (parseCommand)
23,390✔
1203
                    {
1204
                        case ParseCommand.Replace:
1205
                            // If you have a default dict, but specify it in XML, we assume this is a full override. Clear the original list to cut down on GC churn.
1206
                            // TODO: Is some bozo going to store the same "constant" global dict on init, then be surprised when we re-use the dict instead of creating a new one? Detect this and yell about it I guess.
1207
                            // If you are reading this because you're the bozo, [insert angry emoji here], but also feel free to be annoyed that I haven't fixed it yet despite realizing it's a problem. Ping me on Discord, I'll take care of it, sorry 'bout that.
1208
                            if (result != null)
23,150✔
1209
                            {
485✔
1210
                                ((IDictionary)result).Clear();
485✔
1211
                            }
485✔
1212
                            break;
23,150✔
1213

1214
                        case ParseCommand.Patch:
1215
                            if (original != null)
160✔
1216
                            {
140✔
1217
                                permitPatch = true;
140✔
1218
                            }
140✔
1219
                            break;
160✔
1220

1221
                        case ParseCommand.Append:
1222
                            // nothing needs to be done, our existing dupe checking will solve it
1223
                            break;
80✔
1224

1225
                        default:
1226
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1227
                            break;
×
1228
                    }
1229

1230
                    // Dictionary<> handling
1231
                    Type keyType = type.GetGenericArguments()[0];
23,390✔
1232
                    Type valueType = type.GetGenericArguments()[1];
23,390✔
1233

1234
                    var dict = (IDictionary)(result ?? Activator.CreateInstance(type));
23,390✔
1235

1236
                    node.ParseDictionary(dict, keyType, valueType, globals, recSettings, permitPatch);
23,390✔
1237

1238
                    result = dict;
23,390✔
1239
                }
23,390✔
1240

1241
                return result;
23,390✔
1242
            }
1243

1244
            // Special case: HashSet
1245
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(HashSet<>))
90,181✔
1246
            {
990✔
1247
                foreach (var (parseCommand, node) in orders)
4,950✔
1248
                {
990✔
1249
                    bool permitPatch = false;
990✔
1250
                    switch (parseCommand)
990✔
1251
                    {
1252
                        case ParseCommand.Replace:
1253
                            // If you have a default set, but specify it in XML, we assume this is a full override. Clear the original set to cut down on GC churn.
1254
                            // TODO: Is some bozo going to store the same "constant" global set on init, then be surprised when we re-use the set instead of creating a new one? Detect this and yell about it I guess.
1255
                            // If you are reading this because you're the bozo, [insert angry emoji here], but also feel free to be annoyed that I haven't fixed it yet despite realizing it's a problem. Ping me on Discord, I'll take care of it, sorry 'bout that.
1256
                            if (result != null)
830✔
1257
                            {
450✔
1258
                                // Did you know there's no non-generic interface that HashSet<> supports that includes a Clear function?
1259
                                // Fun fact:
1260
                                // That thing I just wrote!
1261
                                var clearFunction = result.GetType().GetMethod("Clear");
450✔
1262
                                clearFunction.Invoke(result, null);
450✔
1263
                            }
450✔
1264
                            break;
830✔
1265

1266
                        case ParseCommand.Patch:
1267
                            if (original != null)
80✔
1268
                            {
60✔
1269
                                permitPatch = true;
60✔
1270
                            }
60✔
1271
                            break;
80✔
1272

1273
                        case ParseCommand.Append:
1274
                            // nothing needs to be done, our existing dupe checking will solve it
1275
                            break;
80✔
1276

1277
                        default:
1278
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1279
                            break;
×
1280
                    }
1281

1282
                    Type keyType = type.GetGenericArguments()[0];
990✔
1283

1284
                    var set = result ?? Activator.CreateInstance(type);
990✔
1285

1286
                    node.ParseHashset(set, keyType, globals, recSettings, permitPatch);
990✔
1287

1288
                    result = set;
990✔
1289
                }
990✔
1290

1291
                return result;
990✔
1292
            }
1293

1294
            // Special case: Stack
1295
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Stack<>))
89,191✔
1296
            {
55✔
1297
                // Stack<> handling
1298
                // Again, no sensible non-generic interface to use, so we're stuck with reflection
1299

1300
                foreach (var (parseCommand, node) in orders)
275✔
1301
                {
55✔
1302
                    switch (parseCommand)
55✔
1303
                    {
1304
                        case ParseCommand.Replace:
1305
                            // If you have a default stack, but specify it in XML, we assume this is a full override. Clear the original stack to cut down on GC churn.
1306
                            // TODO: Is some bozo going to store the same "constant" global stack on init, then be surprised when we re-use the stack instead of creating a new one? Detect this and yell about it I guess.
1307
                            // If you are reading this because you're the bozo, [insert angry emoji here], but also feel free to be annoyed that I haven't fixed it yet despite realizing it's a problem. Ping me on Discord, I'll take care of it, sorry 'bout that.
1308
                            if (result != null)
55✔
1309
                            {
5✔
1310
                                var clearFunction = result.GetType().GetMethod("Clear");
5✔
1311
                                clearFunction.Invoke(result, null);
5✔
1312
                            }
5✔
1313
                            break;
55✔
1314

1315
                        case ParseCommand.Append:
1316
                            break;
×
1317

1318
                        // There definitely starts being an argument for prepend.
1319

1320
                        default:
1321
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1322
                            break;
×
1323
                    }
1324

1325
                    Type keyType = type.GetGenericArguments()[0];
55✔
1326

1327
                    var set = result ?? Activator.CreateInstance(type);
55✔
1328

1329
                    node.ParseStack(set, keyType, globals, recSettings);
55✔
1330

1331
                    result = set;
55✔
1332
                }
55✔
1333

1334
                return result;
55✔
1335
            }
1336

1337
            // Special case: Queue
1338
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Queue<>))
89,136✔
1339
            {
55✔
1340
                // Queue<> handling
1341
                // Again, no sensible non-generic interface to use, so we're stuck with reflection
1342

1343
                foreach (var (parseCommand, node) in orders)
275✔
1344
                {
55✔
1345
                    switch (parseCommand)
55✔
1346
                    {
1347
                        case ParseCommand.Replace:
1348
                            // If you have a default queue, but specify it in XML, we assume this is a full override. Clear the original queue to cut down on GC churn.
1349
                            // TODO: Is some bozo going to store the same "constant" global queue on init, then be surprised when we re-use the queue instead of creating a new one? Detect this and yell about it I guess.
1350
                            // If you are reading this because you're the bozo, [insert angry emoji here], but also feel free to be annoyed that I haven't fixed it yet despite realizing it's a problem. Ping me on Discord, I'll take care of it, sorry 'bout that.
1351
                            if (result != null)
55✔
1352
                            {
5✔
1353
                                var clearFunction = result.GetType().GetMethod("Clear");
5✔
1354
                                clearFunction.Invoke(result, null);
5✔
1355
                            }
5✔
1356
                            break;
55✔
1357

1358
                        case ParseCommand.Append:
1359
                            break;
×
1360

1361
                        // There definitely starts being an argument for prepend.
1362

1363
                        default:
1364
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1365
                            break;
×
1366
                    }
1367

1368
                    Type keyType = type.GetGenericArguments()[0];
55✔
1369

1370
                    var set = result ?? Activator.CreateInstance(type);
55✔
1371

1372
                    node.ParseQueue(set, keyType, globals, recSettings);
55✔
1373

1374
                    result = set;
55✔
1375
                }
55✔
1376

1377
                return result;
55✔
1378
            }
1379

1380
            // Special case: A bucket of tuples
1381
            // These are all basically identical, but AFAIK there's no good way to test them all in a better way.
1382
            if (type.IsGenericType && (
89,081✔
1383
                    type.GetGenericTypeDefinition() == typeof(Tuple<>) ||
89,081✔
1384
                    type.GetGenericTypeDefinition() == typeof(Tuple<,>) ||
89,081✔
1385
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,>) ||
89,081✔
1386
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,>) ||
89,081✔
1387
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,>) ||
89,081✔
1388
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,>) ||
89,081✔
1389
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,>) ||
89,081✔
1390
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,,>) ||
89,081✔
1391
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<>) ||
89,081✔
1392
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,>) ||
89,081✔
1393
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,>) ||
89,081✔
1394
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,>) ||
89,081✔
1395
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,>) ||
89,081✔
1396
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,>) ||
89,081✔
1397
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,>) ||
89,081✔
1398
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,,>)
89,081✔
1399
                    ))
89,081✔
1400
            {
1,065✔
1401
                foreach (var (parseCommand, node) in orders)
5,325✔
1402
                {
1,065✔
1403
                    switch (parseCommand)
1,065✔
1404
                    {
1405
                        case ParseCommand.Replace:
1406
                            // easy, done
1407
                            break;
1,065✔
1408

1409
                        default:
1410
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1411
                            break;
×
1412
                    }
1413

1414
                    int expectedCount = type.GenericTypeArguments.Length;
1,065✔
1415
                    object[] parameters = new object[expectedCount];
1,065✔
1416

1417
                    node.ParseTuple(parameters, type, fieldInfo?.GetCustomAttribute<System.Runtime.CompilerServices.TupleElementNamesAttribute>()?.TransformNames, globals, recSettings);
1,065✔
1418

1419
                    // construct!
1420
                    result = Activator.CreateInstance(type, parameters);
1,065✔
1421
                }
1,065✔
1422

1423
                return result;
1,065✔
1424
            }
1425

1426
            // At this point, we're either a class or a struct, and we need to do the reflection thing
1427

1428
            // If we have refs, something has gone wrong; we should never be doing reflection inside a Record system.
1429
            // This is a really ad-hoc way of testing this and should be fixed.
1430
            // One big problem here is that I'm OK with security vulnerabilities in dec xmls. Those are either supplied by the developer or by mod authors who are intended to have full code support anyway.
1431
            // I'm less OK with security vulnerabilities in save files. Nobody expects a savefile can compromise their system.
1432
            // And the full reflection system is probably impossible to secure, whereas the Record system should be secureable.
1433
            if (!globals.allowReflection)
88,016✔
1434
            {
40✔
1435
                // just pick the first node to get something to go on
1436
                Dbg.Err($"{orders[0].node.GetContext()}: Falling back to reflection within a Record system while parsing a {type}; this is currently not allowed for security reasons. Either you shouldn't be trying to serialize this, or it should implement Dec.IRecorder (https://zorbathut.github.io/dec/release/documentation/serialization.html), or you need a Dec.Converter (https://zorbathut.github.io/dec/release/documentation/custom.html)");
40✔
1437
                return result;
40✔
1438
            }
1439

1440
            foreach (var (parseCommand, node) in orders)
443,160✔
1441
            {
89,656✔
1442
                if (!isRootDec)
89,656✔
1443
                {
44,021✔
1444
                    switch (parseCommand)
44,021✔
1445
                    {
1446
                        case ParseCommand.Patch:
1447
                            // easy, done
1448
                            break;
44,021✔
1449

1450
                        default:
1451
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1452
                            break;
×
1453
                    }
1454
                }
44,021✔
1455
                else
1456
                {
45,635✔
1457
                    if (parseCommand != ParseCommand.Patch)
45,635✔
1458
                    {
×
1459
                        Dbg.Err($"{node.GetContext()}: Mode provided for root Dec; this is currently not supported in any form");
×
1460
                    }
×
1461
                }
45,635✔
1462

1463
                // If we haven't been given a generic class from our parent, go ahead and init to defaults
1464
                if (result == null && recordableBuffered != null)
89,656✔
1465
                {
15✔
1466
                    result = recordableBuffered;
15✔
1467
                }
15✔
1468

1469
                if (result == null)
89,656✔
1470
                {
17,640✔
1471
                    // okay fine
1472
                    result = type.CreateInstanceSafe("object", node);
17,640✔
1473

1474
                    if (result == null)
17,640✔
1475
                    {
80✔
1476
                        // error already reported
1477
                        return result;
80✔
1478
                    }
1479
                }
17,560✔
1480

1481
                node.ParseReflection(result, globals, recSettings);
89,576✔
1482
            }
89,576✔
1483

1484
            // Set up our index fields; this has to happen last in case we're a struct
1485
            Index.Register(ref result);
87,896✔
1486

1487
            return result;
87,896✔
1488
        }
1,555,779✔
1489

1490
        internal static object ParseString(string text, Type type, object original, Context context)
1491
        {
814,162✔
1492
            // Special case: Converter override
1493
            // This is redundant if we're being called from ParseElement, but we aren't always.
1494
            if (ConverterFor(type) is Converter converter)
814,162✔
1495
            {
45✔
1496
                object result = original;
45✔
1497

1498
                try
1499
                {
45✔
1500
                    // string converter
1501
                    if (converter is ConverterString converterString)
45✔
1502
                    {
45✔
1503
                        // context might be null; that's OK at the moment
1504
                        try
1505
                        {
45✔
1506
                            result = converterString.ReadObj(text, context);
45✔
1507
                        }
45✔
1508
                        catch (Exception e)
×
1509
                        {
×
1510
                            Dbg.Ex(new ConverterReadException(context, converter, e));
×
1511

1512
                            result = GenerateResultFallback(result, type);
×
1513
                        }
×
1514
                    }
45✔
1515
                    else if (converter is ConverterRecord converterRecord)
×
1516
                    {
×
1517
                        // string parsing really doesn't apply here, we can't get a full Recorder context out anymore
1518
                        // in theory this could be done with RecordAsThis() but I'm just going to skip it for now
1519
                        Dbg.Err($"{context}: Attempt to string-parse with a ConverterRecord, this is currently not supported, contact developers if you need this feature");
×
1520
                    }
×
1521
                    else if (converter is ConverterFactory converterFactory)
×
1522
                    {
×
1523
                        // string parsing really doesn't apply here, we can't get a full Recorder context out anymore
1524
                        // in theory this could be done with RecordAsThis() but I'm just going to skip it for now
1525
                        Dbg.Err($"{context}: Attempt to string-parse with a ConverterFactory, this is currently not supported, contact developers if you need this feature");
×
1526
                    }
×
1527
                    else
1528
                    {
×
1529
                        Dbg.Err($"Somehow ended up with an unsupported converter {converter.GetType()}");
×
1530
                    }
×
1531
                }
45✔
1532
                catch (Exception e)
×
1533
                {
×
1534
                    Dbg.Ex(e);
×
1535
                }
×
1536

1537
                return result;
45✔
1538
            }
1539

1540
            // Special case: decs
1541
            if (typeof(Dec).IsAssignableFrom(type))
814,117✔
1542
            {
48,285✔
1543
                if (text == "" || text == null)
48,285✔
1544
                {
40,695✔
1545
                    // you reference nothing, you get the null (even if this isn't a specified type; null is null, after all)
1546
                    return null;
40,695✔
1547
                }
1548
                else
1549
                {
7,590✔
1550
                    if (type.GetDecRootType() == null)
7,590✔
1551
                    {
80✔
1552
                        Dbg.Err($"{context}: Non-hierarchy decs cannot be used as references");
80✔
1553
                        return null;
80✔
1554
                    }
1555

1556
                    Dec result = Database.Get(type, text);
7,510✔
1557
                    if (result == null)
7,510✔
1558
                    {
85✔
1559
                        if (UtilMisc.ValidateDecName(text, context))
85✔
1560
                        {
85✔
1561
                            Dbg.Err($"{context}: Couldn't find {type} named `{text}`");
85✔
1562
                        }
85✔
1563

1564
                        // If we're an invalid name, we already spat out the error
1565
                    }
85✔
1566
                    return result;
7,510✔
1567
                }
1568
            }
1569

1570
            // Special case: types
1571
            if (type == typeof(Type))
765,832✔
1572
            {
415,056✔
1573
                if (text == "")
415,056✔
1574
                {
×
1575
                    return null;
×
1576
                }
1577

1578
                return UtilType.ParseDecFormatted(text, context);
415,056✔
1579
            }
1580

1581
            // Various non-composite-type special-cases
1582
            if (text != "")
350,776✔
1583
            {
350,776✔
1584
                // If we've got text, treat us as an object of appropriate type
1585
                try
1586
                {
350,776✔
1587
                    if (type == typeof(float))
350,776✔
1588
                    {
28,250✔
1589
                        // first check the various strings, case-insensitive
1590
                        if (String.Compare(text, "nan", true) == 0)
28,250✔
1591
                        {
90✔
1592
                            return float.NaN;
90✔
1593
                        }
1594

1595
                        if (String.Compare(text, "infinity", true) == 0)
28,160✔
1596
                        {
40✔
1597
                            return float.PositiveInfinity;
40✔
1598
                        }
1599

1600
                        if (String.Compare(text, "-infinity", true) == 0)
28,120✔
1601
                        {
40✔
1602
                            return float.NegativeInfinity;
40✔
1603
                        }
1604

1605
                        if (text.StartsWith("nanbox", StringComparison.CurrentCultureIgnoreCase))
28,080✔
1606
                        {
85✔
1607
                            const int expectedFloatSize = 6 + 8;
1608

1609
                            if (type == typeof(float) && text.Length != expectedFloatSize)
85✔
1610
                            {
×
1611
                                Dbg.Err($"{context}: Found nanboxed value without the expected number of characters, expected {expectedFloatSize} but got {text.Length}");
×
1612
                                return float.NaN;
×
1613
                            }
1614

1615
                            int number = Convert.ToInt32(text.Substring(6), 16);
85✔
1616
                            return BitConverter.Int32BitsToSingle(number);
85✔
1617
                        }
1618
                    }
27,995✔
1619

1620
                    if (type == typeof(double))
350,521✔
1621
                    {
2,840✔
1622
                        // first check the various strings, case-insensitive
1623
                        if (String.Compare(text, "nan", true) == 0)
2,840✔
1624
                        {
1,815✔
1625
                            return double.NaN;
1,815✔
1626
                        }
1627

1628
                        if (String.Compare(text, "infinity", true) == 0)
1,025✔
1629
                        {
40✔
1630
                            return double.PositiveInfinity;
40✔
1631
                        }
1632

1633
                        if (String.Compare(text, "-infinity", true) == 0)
985✔
1634
                        {
40✔
1635
                            return double.NegativeInfinity;
40✔
1636
                        }
1637

1638
                        if (text.StartsWith("nanbox", StringComparison.CurrentCultureIgnoreCase))
945✔
1639
                        {
75✔
1640
                            const int expectedDoubleSize = 6 + 16;
1641

1642
                            if (type == typeof(double) && text.Length != expectedDoubleSize)
75✔
1643
                            {
×
1644
                                Dbg.Err($"{context}: Found nanboxed value without the expected number of characters, expected {expectedDoubleSize} but got {text.Length}");
×
1645
                                return double.NaN;
×
1646
                            }
1647

1648
                            long number = Convert.ToInt64(text.Substring(6), 16);
75✔
1649
                            return BitConverter.Int64BitsToDouble(number);
75✔
1650
                        }
1651
                    }
870✔
1652

1653
                    return TypeDescriptor.GetConverter(type).ConvertFromString(text);
348,551✔
1654
                }
1655
                catch (System.Exception e)  // I would normally not catch System.Exception, but TypeConverter is wrapping FormatException in an Exception for some reason
180✔
1656
                {
180✔
1657
                    Dbg.Err($"{context}: {e.ToString()}");
180✔
1658
                    return original;
180✔
1659
                }
1660
            }
1661
            else if (type == typeof(string))
×
1662
            {
×
1663
                // If we don't have text, and we're a string, return ""
1664
                return "";
×
1665
            }
1666
            else
1667
            {
×
1668
                // If we don't have text, and we've fallen down to this point, that's an error (and return original value I guess)
1669
                Dbg.Err($"{context}: Empty field provided for type {type}");
×
1670
                return original;
×
1671
            }
1672
        }
814,162✔
1673

1674
        internal static Type TypeSystemRuntimeType = Type.GetType("System.RuntimeType");
10✔
1675
        internal static void ComposeElement(WriterNode node, object value, Type fieldType, FieldInfo fieldInfo = null, bool isRootDec = false, bool asThis = false)
1676
        {
1,459,759✔
1677
            // Verify our Shared flags as the *very* first step to ensure nothing gets past us.
1678
            // In theory this should be fine with Flexible; Flexible only happens on an outer wrapper that was shared, and therefore was null, and therefore this is default also
1679
            bool canBeShared = fieldType.CanBeShared();
1,459,759✔
1680
            if (node.RecorderSettings.shared == Recorder.Settings.Shared.Allow && !asThis)
1,459,759✔
1681
            {
801,740✔
1682
                // If this is an `asThis` parameter, then we may not be writing the field type it looks like, and we're just going to trust that they're doing something sensible.
1683
                if (!canBeShared)
801,740✔
1684
                {
120✔
1685
                    // If shared, make sure our type is appropriate for sharing
1686
                    // this really needs the recorder name and the field name too
1687
                    Dbg.Wrn($"Value type `{fieldType}` tagged as Shared in recorder, this is meaningless but harmless");
120✔
1688
                }
120✔
1689
            }
801,740✔
1690

1691
            // Handle Dec types, if this isn't a root (otherwise we'd just reference ourselves and that's kind of pointless)
1692
            if (!isRootDec && value is Dec)
1,459,759✔
1693
            {
3,870✔
1694
                // Dec types are special in a few ways.
1695
                // First off, they don't include their type data, because we assume it's of a type provided by the structure.
1696
                // Second, we represent null values as an empty string, not as a null tag.
1697
                // (We'll accept the null tag if you insist, we just have a cleaner special case.)
1698
                // Null tag stuff is done further down, in the null check.
1699

1700
                var rootType = value.GetType().GetDecRootType();
3,870✔
1701
                if (!rootType.IsAssignableFrom(fieldType))
3,870✔
1702
                {
30✔
1703
                    // The user has a Dec.Dec or similar, and it has a Dec assigned to it.
1704
                    // This is a bit weird and is something we're not happy with; this means we need to include the Dec type along with it.
1705
                    // But we're OK with that, honestly. We just do that.
1706
                    // If you're saving something like this you don't get to rename Dec classes later on, but, hey, deal with it.
1707
                    // We do, however, tag it with the root type, not the derived type; this is the most general type that still lets us search things in the future.
1708
                    node.TagClass(rootType);
30✔
1709
                }
30✔
1710

1711
                node.WriteDec(value as Dec);
3,870✔
1712

1713
                return;
3,870✔
1714
            }
1715

1716
            // Everything represents "null" with an explicit XML tag, so let's just do that
1717
            // Maybe at some point we want to special-case this for the empty Dec link
1718
            if (value == null)
1,455,889✔
1719
            {
366,392✔
1720
                if (typeof(Dec).IsAssignableFrom(fieldType))
366,392✔
1721
                {
33,950✔
1722
                    node.WriteDec(null);
33,950✔
1723
                }
33,950✔
1724
                else
1725
                {
332,442✔
1726
                    node.WriteExplicitNull();
332,442✔
1727
                }
332,442✔
1728

1729
                return;
366,392✔
1730
            }
1731

1732
            var valType = value.GetType();
1,089,497✔
1733

1734
            // This is our value's type, but we may need a little bit of tinkering to make it useful.
1735
            // The current case I know of is System.RuntimeType, which appears if we call .GetType() on a Type.
1736
            // I assume there is a complicated internal reason for this; good news, we can ignore it and just pretend it's a System.Type.
1737
            // Bad news: it's actually really hard to detect this case because System.RuntimeType is private.
1738
            // That's why we have the annoying `static` up above.
1739
            if (valType == TypeSystemRuntimeType)
1,089,497✔
1740
            {
255✔
1741
                valType = typeof(Type);
255✔
1742
            }
255✔
1743

1744
            // Do all our unreferencables first
1745
            bool unreferenceableComplete = false;
1,089,497✔
1746

1747
            if (valType.IsPrimitive)
1,089,497✔
1748
            {
249,581✔
1749
                node.WritePrimitive(value);
249,581✔
1750

1751
                unreferenceableComplete = true;
249,581✔
1752
            }
249,581✔
1753
            else if (value is System.Enum)
839,916✔
1754
            {
305✔
1755
                node.WriteEnum(value);
305✔
1756

1757
                unreferenceableComplete = true;
305✔
1758
            }
305✔
1759
            else if (value is string)
839,611✔
1760
            {
27,510✔
1761
                node.WriteString(value as string);
27,510✔
1762

1763
                unreferenceableComplete = true;
27,510✔
1764
            }
27,510✔
1765
            else if (value is Type)
812,101✔
1766
            {
255✔
1767
                node.WriteType(value as Type);
255✔
1768

1769
                unreferenceableComplete = true;
255✔
1770
            }
255✔
1771

1772
            // Check to see if we should make this into a ref (yes, even if we're not tagged as Shared)
1773
            // Do this *before* we do the class tagging, otherwise we may add ref/class tags to a single node, which is invalid.
1774
            // Note that it's important we don't write a reference if we had an unreferenceable; it's unnecessarily slow and some of our writer types don't support it.
1775
            if (Util.CanBeShared(valType) && !asThis)
1,089,497✔
1776
            {
782,626✔
1777
                if (node.WriteReference(value))
782,626✔
1778
                {
150,755✔
1779
                    // The ref system has set up the appropriate tagging, so we're done!
1780
                    return;
150,755✔
1781
                }
1782

1783
                // If we support references, then this object has not previously shown up in the reference system; keep going so we finish serializing it.
1784
                // If we don't support references at all then obviously we *really* need to finish serializing it.
1785
            }
631,871✔
1786

1787
            // If we have a type that isn't the expected type, tag it. We may need this even for unreferencable value types because everything fits in an `object`.
1788
            if (valType != fieldType)
938,742✔
1789
            {
1,196✔
1790
                if (asThis)
1,196✔
1791
                {
20✔
1792
                    Dbg.Err($"RecordAsThis() call attempted to add a class tag, which is currently not allowed; AsThis() calls must not be polymorphic (ask the devs for chained class tags if this is a thing you need)");
20✔
1793
                    // . . . I guess we just keep going?
1794
                }
20✔
1795
                else
1796
                {
1,176✔
1797
                    node.TagClass(valType);
1,176✔
1798
                }
1,176✔
1799
            }
1,196✔
1800

1801
            // Did we actually write our node type? Alright, we're done.
1802
            if (unreferenceableComplete)
938,742✔
1803
            {
277,651✔
1804
                return;
277,651✔
1805
            }
1806

1807
            // Now we have things that *could* be references, but aren't.
1808

1809
            if (node.AllowCloning && UtilType.CanBeCloneCopied(valType))
661,091✔
1810
            {
20✔
1811
                node.WriteCloneCopy(value);
20✔
1812

1813
                return;
20✔
1814
            }
1815

1816
            if (valType.IsArray)
661,071✔
1817
            {
2,630✔
1818
                node.WriteArray(value as Array);
2,630✔
1819

1820
                return;
2,630✔
1821
            }
1822

1823
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(List<>))
658,441✔
1824
            {
26,485✔
1825
                node.WriteList(value as IList);
26,485✔
1826

1827
                return;
26,485✔
1828
            }
1829

1830
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
631,956✔
1831
            {
11,810✔
1832
                node.WriteDictionary(value as IDictionary);
11,810✔
1833

1834
                return;
11,810✔
1835
            }
1836

1837
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(HashSet<>))
620,146✔
1838
            {
590✔
1839
                node.WriteHashSet(value as IEnumerable);
590✔
1840

1841
                return;
590✔
1842
            }
1843

1844
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(Queue<>))
619,556✔
1845
            {
50✔
1846
                node.WriteQueue(value as IEnumerable);
50✔
1847

1848
                return;
50✔
1849
            }
1850

1851
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(Stack<>))
619,506✔
1852
            {
50✔
1853
                node.WriteStack(value as IEnumerable);
50✔
1854

1855
                return;
50✔
1856
            }
1857

1858
            if (valType.IsGenericType && (
619,456✔
1859
                    valType.GetGenericTypeDefinition() == typeof(Tuple<>) ||
619,456✔
1860
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,>) ||
619,456✔
1861
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,>) ||
619,456✔
1862
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,>) ||
619,456✔
1863
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,>) ||
619,456✔
1864
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,,>) ||
619,456✔
1865
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,>) ||
619,456✔
1866
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,,>)
619,456✔
1867
                ))
619,456✔
1868
            {
215✔
1869
                node.WriteTuple(value, fieldInfo?.GetCustomAttribute<System.Runtime.CompilerServices.TupleElementNamesAttribute>());
215✔
1870

1871
                return;
215✔
1872
            }
1873

1874
            if (valType.IsGenericType && (
619,241✔
1875
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<>) ||
619,241✔
1876
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,>) ||
619,241✔
1877
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,>) ||
619,241✔
1878
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,>) ||
619,241✔
1879
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,>) ||
619,241✔
1880
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,>) ||
619,241✔
1881
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,>) ||
619,241✔
1882
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,,>)
619,241✔
1883
                ))
619,241✔
1884
            {
385✔
1885
                node.WriteValueTuple(value, fieldInfo?.GetCustomAttribute<System.Runtime.CompilerServices.TupleElementNamesAttribute>());
385✔
1886

1887
                return;
385✔
1888
            }
1889

1890
            if (value is IRecordable
618,856✔
1891
                && (!(value is IConditionalRecordable) || (value as IConditionalRecordable).ShouldRecord(node.UserSettings)))
618,856✔
1892
            {
562,810✔
1893
                node.WriteRecord(value as IRecordable);
562,810✔
1894

1895
                return;
562,810✔
1896
            }
1897

1898
            {
56,046✔
1899
                // Look for a converter; that's the only way to handle this before we fall back to reflection
1900
                var converter = Serialization.ConverterFor(valType);
56,046✔
1901
                if (converter != null)
56,046✔
1902
                {
595✔
1903
                    node.WriteConvertible(converter, value);
595✔
1904
                    return;
595✔
1905
                }
1906
            }
55,451✔
1907

1908
            if (!node.AllowReflection)
55,451✔
1909
            {
45✔
1910
                Dbg.Err($"Couldn't find a composition method for type {valType}; either you shouldn't be trying to serialize it, or it should implement Dec.IRecorder (https://zorbathut.github.io/dec/release/documentation/serialization.html), or you need a Dec.Converter (https://zorbathut.github.io/dec/release/documentation/custom.html)");
45✔
1911
                node.WriteError();
45✔
1912
                return;
45✔
1913
            }
1914

1915
            // We absolutely should not be doing reflection when in recorder mode; that way lies madness.
1916

1917
            foreach (var field in valType.GetSerializableFieldsFromHierarchy())
758,576✔
1918
            {
296,179✔
1919
                ComposeElement(node.CreateReflectionChild(field, node.RecorderSettings), field.GetValue(value), field.FieldType, fieldInfo: field);
296,179✔
1920
            }
296,179✔
1921

1922
            return;
55,406✔
1923
        }
1,459,759✔
1924

1925
        internal static void Clear()
1926
        {
26,395✔
1927
            ConverterInitialized = false;
26,395✔
1928
            ConverterObjects = new System.Collections.Concurrent.ConcurrentDictionary<Type, Converter>();
26,395✔
1929
            ConverterGenericPrototypes = new System.Collections.Concurrent.ConcurrentDictionary<Type, Type>();
26,395✔
1930
        }
26,395✔
1931
    }
1932
}
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