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

zorbathut / dec / 12497558494

25 Dec 2024 11:56PM UTC coverage: 89.807% (+0.03%) from 89.778%
12497558494

push

github

zorbathut
Fix: Cloning objects with children with missing Converters could result in an unhandled exception.

4784 of 5327 relevant lines covered (89.81%)

193255.31 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

123
        internal static Converter ConverterFor(Type inputType)
124
        {
5,346,236✔
125
            if (ConverterObjects.TryGetValue(inputType, out var converter))
5,346,236✔
126
            {
5,296,839✔
127
                return converter;
5,296,839✔
128
            }
129

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

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

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

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

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

185
            var factoriedConverter = Config.ConverterFactory?.Invoke(inputType);
49,272✔
186
            ConverterObjects[inputType] = factoriedConverter;   // cache this so we don't generate a million of them
49,272✔
187
            return factoriedConverter;
49,272✔
188
        }
5,346,236✔
189

190

191
        internal static void Initialize()
192
        {
20,100✔
193
            if (ConverterInitialized)
20,100✔
194
            {
4,765✔
195
                return;
4,765✔
196
            }
197

198
            // this is here just so we don't keep thrashing if something breaks
199
            ConverterInitialized = true;
15,335✔
200

201
            ConverterObjects = new System.Collections.Concurrent.ConcurrentDictionary<Type, Converter>();
15,335✔
202

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

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

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

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

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

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

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

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

262
                    ConverterObjects[convertedType] = converter;
715✔
263
                    continue;
715✔
264
                }
265
            }
5✔
266
        }
20,100✔
267

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

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

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

356
                return ParseMode.Default;
840✔
357
            }
358
        }
3,128,243✔
359

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

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

376
            foreach (var node in nodes)
7,589,595✔
377
            {
1,517,919✔
378
                var context = node.GetContext();
1,517,919✔
379
                var s_parseMode = ParseModeFromString(context, node.GetMetadata(ReaderNodeParseable.Metadata.Mode));
1,517,919✔
380

381
                ParseCommand s_parseCommand;
382

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

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

405
                            case ParseMode.Default:
406
                            case ParseMode.Replace:
407
                                s_parseCommand = ParseCommand.Replace;
132,120✔
408
                                break;
132,120✔
409

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

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

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

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

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

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

456
                if (s_parseCommand == ParseCommand.Replace)
1,517,919✔
457
                {
569,796✔
458
                    orders.Clear();
569,796✔
459
                }
569,796✔
460

461
                orders.Add((s_parseCommand, node));
1,517,919✔
462
            }
1,517,919✔
463

464
            return orders;
1,517,919✔
465
        }
1,517,919✔
466

467
        internal static List<ReaderFileDec.ReaderDec> CompileDecOrders(List<ReaderFileDec.ReaderDec> decs)
468
        {
45,645✔
469
            var orders = new List<ReaderFileDec.ReaderDec>();
45,645✔
470
            bool everExisted = false;
45,645✔
471
            foreach (var item in decs)
230,465✔
472
            {
46,765✔
473
                // this is also horribly inefficient
474
                var s_parseMode = ParseModeFromString(item.context, item.nodeFactory(null).GetMetadata(ReaderNodeParseable.Metadata.Mode));
46,765✔
475

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

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

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

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

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

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

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

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

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

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

556
                    case ParseMode.DeleteIfExists:
557
                        orders.Clear();
140✔
558
                        break;
140✔
559
                }
560
            }
46,765✔
561

562
            return orders;
45,645✔
563
        }
45,645✔
564

565
        internal static object ParseElement(List<ReaderNodeParseable> nodes, Type type, object original,
566
            ReaderGlobals globals, Recorder.Settings recSettings, FieldInfo fieldInfo = null, bool isRootDec = false,
567
            bool hasReferenceId = false, bool asThis = false,
568
            List<(ParseCommand command, ReaderNodeParseable node)> ordersOverride = null)
569
        {
1,562,429✔
570
            var result = ParseElement_Worker(nodes, type, original, globals, recSettings, fieldInfo, isRootDec, hasReferenceId, asThis, ordersOverride);
1,562,429✔
571

572
            // I just really don't want to put this code at the end of *every single return*, that would be insane
573
            // we don't allow dec references, we've already got those!
574
            if (globals.decPathLookup != null && result != null)
1,562,429✔
575
            {
495,972✔
576
                var resultType = result.GetType();
495,972✔
577

578
                // I really feel like whatever I'm expressing here must exist elsewhere in the codebase, but I can't find it
579
                if (!resultType.IsValueType && resultType != typeof(string) && resultType != typeof(Type) && !typeof(Dec).IsAssignableFrom(resultType))
495,972✔
580
                {
110,476✔
581
                    // these paths *should* all match up, so we're just choosing one
582
                    var newPath = nodes[0].GetContext().path;
110,476✔
583

584
                    if (newPath == null)
110,476✔
585
                    {
×
586
                        Dbg.Err("Internal error; missing path somehow? Please report this, thanks!");
×
587
                    }
×
588
                    else
589
                    {
110,476✔
590
                        // right now we strictly overwrite previous instances of the result
591
                        // this is not a great solution because it's very error-prone
592
                        // the problem is that the inheritance/patch system has a tendency to spam this function repeatedly with the same object
593
                        // I'm currently not sure how to deal with this, so . . . I'm not! I'm just doing it the bad way.
594
                        globals.decPathLookup[result] = newPath;
110,476✔
595
                    }
110,476✔
596
                }
110,476✔
597
            }
495,972✔
598

599
            return result;
1,562,429✔
600
        }
1,562,429✔
601

602
        internal static T ParseElementTyped<T>(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)
603
        {
3,300✔
604
            var result = ParseElement(nodes, type, original, globals, recSettings, fieldInfo, isRootDec, hasReferenceId, asThis, ordersOverride);
3,300✔
605
            if (result != null)
3,300✔
606
            {
3,265✔
607
                return (T)result;
3,265✔
608
            }
609
            else
610
            {
35✔
611
                return default;
35✔
612
            }
613
        }
3,300✔
614

615
        internal static object ParseElement_Worker(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)
616
        {
1,562,429✔
617
            if (nodes == null || nodes.Count == 0)
1,562,429✔
618
            {
×
619
                Dbg.Err("Internal error, Dec failed to provide nodes to ParseElement. Please report this!");
×
620
                return original;
×
621
            }
622

623
            if (!globals.allowReflection && nodes.Count > 1)
1,562,429✔
624
            {
×
625
                Dbg.Err("Internal error, multiple nodes provided for recorder-mode behavior. Please report this!");
×
626
            }
×
627

628
            // We keep the original around in case of error, but do all our manipulation on a result object.
629
            object result = original;
1,562,429✔
630

631
            // Verify our Shared flags as the *very* first step to ensure nothing gets past us.
632
            // 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
633
            if (recSettings.shared == Recorder.Settings.Shared.Allow)
1,562,429✔
634
            {
808,270✔
635
                if (!type.CanBeShared())
808,270✔
636
                {
100✔
637
                    // If shared, make sure our input is null and our type is appropriate for sharing
638
                    Dbg.Wrn($"{nodes[0].GetContext()}: Value type `{type}` tagged as Shared in recorder, this is meaningless but harmless");
100✔
639
                }
100✔
640
                else if (original != null && !hasReferenceId)
808,170✔
641
                {
25✔
642
                    // We need to create objects without context if it's shared, so we kind of panic in this case
643
                    Dbg.Err($"{nodes[0].GetContext()}: Shared `{type}` provided with non-null default object, this may result in unexpected behavior");
25✔
644
                }
25✔
645
            }
808,270✔
646

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

649
            // Validate all combinations here
650
            // This could definitely be more efficient and skip at least one traversal pass
651
            foreach (var s_node in nodes)
7,815,535✔
652
            {
1,564,124✔
653
                string nullAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Null);
1,564,124✔
654
                string refAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Ref);
1,564,124✔
655
                string classAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Class);
1,564,124✔
656
                string modeAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Mode);
1,564,124✔
657

658
                // Some of these are redundant and that's OK
659
                if (nullAttribute != null && (refAttribute != null || classAttribute != null || modeAttribute != null))
1,564,124✔
660
                {
180✔
661
                    Dbg.Err($"{s_node.GetContext()}: Null element may not have ref, class, or mode specified; guessing wildly at intentions");
180✔
662
                }
180✔
663
                else if (refAttribute != null && (nullAttribute != null || classAttribute != null || modeAttribute != null))
1,563,944✔
664
                {
75✔
665
                    Dbg.Err($"{s_node.GetContext()}: Ref element may not have null, class, or mode specified; guessing wildly at intentions");
75✔
666
                }
75✔
667
                else if (classAttribute != null && (nullAttribute != null || refAttribute != null))
1,563,869✔
668
                {
×
669
                    Dbg.Err($"{s_node.GetContext()}: Class-specified element may not have null or ref specified; guessing wildly at intentions");
×
670
                }
×
671
                else if (modeAttribute != null && (nullAttribute != null || refAttribute != null))
1,563,869✔
672
                {
×
673
                    Dbg.Err($"{s_node.GetContext()}: Mode-specified element may not have null or ref specified; guessing wildly at intentions");
×
674
                }
×
675

676
                var unrecognized = s_node.GetMetadataUnrecognized();
1,564,124✔
677
                if (unrecognized != null)
1,564,124✔
678
                {
65✔
679
                    Dbg.Err($"{s_node.GetContext()}: Has unknown attributes {unrecognized}");
65✔
680
                }
65✔
681
            }
1,564,124✔
682

683
            // Doesn't mean anything outside recorderMode, so we check it for validity just in case
684
            string refKey;
685
            ReaderNode refKeyNode = null; // stored entirely for error reporting
1,562,429✔
686
            if (!globals.allowRefs)
1,562,429✔
687
            {
614,014✔
688
                refKey = null;
614,014✔
689
                foreach (var s_node in nodes)
3,073,460✔
690
                {
615,709✔
691
                    string nodeRefAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Ref);
615,709✔
692
                    if (nodeRefAttribute != null)
615,709✔
693
                    {
200✔
694
                        Dbg.Err($"{s_node.GetContext()}: Found a reference tag while not evaluating Recorder mode, ignoring it");
200✔
695
                    }
200✔
696
                }
615,709✔
697
            }
614,014✔
698
            else
699
            {
948,415✔
700
                (refKey, refKeyNode) = nodes.Select(node => (node.GetMetadata(ReaderNodeParseable.Metadata.Ref), node)).Where(anp => anp.Item1 != null).LastOrDefault();
2,845,245✔
701
            }
948,415✔
702

703
            // First figure out type. We actually need type to be set before we can properly analyze and validate the mode flags.
704
            // If we're in an asThis block, it refers to the outer item, not the inner item; just skip this entirely
705
            bool isNull = false;
1,562,429✔
706
            if (!asThis)
1,562,429✔
707
            {
1,561,864✔
708
                string classAttribute = null;
1,561,864✔
709
                ReaderNode classAttributeNode = null; // stored entirely for error reporting
1,561,864✔
710
                bool replaced = false;
1,561,864✔
711
                foreach (var s_node in nodes)
7,812,710✔
712
                {
1,563,559✔
713
                    // However, we do need to watch for Replace, because that means we should nuke the class attribute and start over.
714
                    string modeAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Mode);
1,563,559✔
715
                    ParseMode s_parseMode = ParseModeFromString(s_node.GetContext(), modeAttribute);
1,563,559✔
716
                    if (s_parseMode == ParseMode.Replace)
1,563,559✔
717
                    {
520✔
718
                        // we also should maybe be doing this if we're a list, map, or set?
719
                        classAttribute = null;
520✔
720
                        replaced = true;
520✔
721
                    }
520✔
722

723
                    // if we get nulled, we kill the class tag and basically treat it like a delete
724
                    // but we also reset the null tag on every entry
725
                    isNull = false;
1,563,559✔
726
                    string nullAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Null);
1,563,559✔
727
                    if (nullAttribute != null)
1,563,559✔
728
                    {
250,147✔
729
                        if (!bool.TryParse(nullAttribute, out bool nullValue))
250,147✔
730
                        {
×
731
                            Dbg.Err($"{s_node.GetContext()}: Invalid `null` attribute");
×
732
                        }
×
733
                        else if (nullValue)
250,147✔
734
                        {
250,107✔
735
                            isNull = true;
250,107✔
736
                        }
250,107✔
737
                    }
250,147✔
738

739
                    // update the class based on whatever this says
740
                    string localClassAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Class);
1,563,559✔
741
                    if (localClassAttribute != null)
1,563,559✔
742
                    {
208,121✔
743
                        classAttribute = localClassAttribute;
208,121✔
744
                        classAttributeNode = s_node;
208,121✔
745
                    }
208,121✔
746
                }
1,563,559✔
747

748
                if (classAttribute != null)
1,561,864✔
749
                {
208,121✔
750
                    var possibleType = (Type)ParseString(classAttribute, typeof(Type), null, classAttributeNode.GetContext());
208,121✔
751
                    if (!type.IsAssignableFrom(possibleType))
208,121✔
752
                    {
20✔
753
                        Dbg.Err($"{classAttributeNode.GetContext()}: Explicit type {classAttribute} cannot be assigned to expected type {type}");
20✔
754
                    }
20✔
755
                    else if (!replaced && result != null && result.GetType() != possibleType)
208,101✔
756
                    {
20✔
757
                        Dbg.Err($"{classAttributeNode.GetContext()}: Explicit type {classAttribute} does not match already-provided instance {type}");
20✔
758
                    }
20✔
759
                    else
760
                    {
208,081✔
761
                        type = possibleType;
208,081✔
762
                    }
208,081✔
763
                }
208,121✔
764
            }
1,561,864✔
765

766
            var converter = ConverterFor(type);
1,562,429✔
767

768
            // Now we traverse the Mode attributes as prep for our final parse pass.
769
            // ordersOverride makes `nodes` admittedly a little unnecessary.
770
            List<(ParseCommand command, ReaderNodeParseable node)> orders = ordersOverride ?? CompileOrders(type.CalculateSerializationModeCategory(converter, isRootDec), nodes);
1,562,429✔
771

772
            // Gather info
773
            bool hasChildren = false;
1,562,429✔
774
            ReaderNode hasChildrenNode = null;
1,562,429✔
775
            bool hasText = false;
1,562,429✔
776
            ReaderNode hasTextNode = null;
1,562,429✔
777
            foreach (var (_, node) in orders)
7,815,535✔
778
            {
1,564,124✔
779
                if (!hasChildren && node.HasChildren())
1,564,124✔
780
                {
481,301✔
781
                    hasChildren = true;
481,301✔
782
                    hasChildrenNode = node;
481,301✔
783
                }
481,301✔
784
                if (!hasText && node.GetText() != null)
1,564,124✔
785
                {
357,601✔
786
                    hasText = true;
357,601✔
787
                    hasTextNode = node;
357,601✔
788
                }
357,601✔
789
            }
1,564,124✔
790

791
            // Actually handle our attributes
792
            if (refKey != null)
1,562,429✔
793
            {
357,500✔
794
                // Ref is the highest priority, largely because I think it's cool
795

796
                // First we check if this is a valid Dec path ref; those don't require .Shared()
797
                if (Database.DecPathLookupReverse.TryGetValue(refKey, out var defPathRef))
357,500✔
798
                {
225✔
799
                    // check types
800
                    if (!type.IsAssignableFrom(defPathRef.GetType()))
225✔
801
                    {
×
802
                        Dbg.Err($"{refKeyNode.GetContext()}: Dec path reference object [{refKey}] is of type {defPathRef.GetType()}, which cannot be converted to expected type {type}");
×
803
                        return result;
×
804
                    }
805

806
                    // if it's a conflict, be unhappy
807
                    if (Database.DecPathLookupInvalid.Contains(refKey))
225✔
808
                    {
×
809
                        Dbg.Err($"{refKeyNode.GetContext()}: Deserializes improper Dec path reference [{refKey}]; this should probably not have been serialized in the first place, doing our best though");
×
810
                    }
×
811
                    else if (Database.DecPathLookupConflicts.Contains(refKey))
225✔
812
                    {
×
813
                        Dbg.Err($"{refKeyNode.GetContext()}: Multiple objects claiming Dec path reference [{refKey}]; this should probably not have been serialized in the first place, doing our best though");
×
814
                    }
×
815

816
                    // toot
817
                    return defPathRef;
225✔
818
                }
819

820
                if (recSettings.shared == Recorder.Settings.Shared.Deny)
357,275✔
821
                {
55✔
822
                    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✔
823
                }
55✔
824

825
                if (globals.refs == null)
357,275✔
826
                {
40✔
827
                    Dbg.Err($"{refKeyNode.GetContext()}: Found a reference object {refKey} before refs are initialized (is this being used in a ConverterFactory<>.Create()?)");
40✔
828
                    return result;
40✔
829
                }
830

831
                if (!globals.refs.ContainsKey(refKey))
357,235✔
832
                {
5✔
833
                    Dbg.Err($"{refKeyNode.GetContext()}: Found a reference object {refKey} without a valid reference mapping");
5✔
834
                    return result;
5✔
835
                }
836

837
                object refObject = globals.refs[refKey];
357,230✔
838
                if (refObject == null && !type.IsValueType)
357,230✔
839
                {
80✔
840
                    // okay, good enough
841
                    return refObject;
80✔
842
                }
843

844
                if (!type.IsAssignableFrom(refObject.GetType()))
357,150✔
845
                {
25✔
846
                    Dbg.Err($"{refKeyNode.GetContext()}: Reference object {refKey} is of type {refObject.GetType()}, which cannot be converted to expected type {type}");
25✔
847
                    return result;
25✔
848
                }
849

850
                return refObject;
357,125✔
851
            }
852
            else if (isNull)
1,204,929✔
853
            {
250,082✔
854
                return null;
250,082✔
855

856
                // Note: It may seem wrong that we can return null along with a non-null model.
857
                // 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.
858
                // If we actually need a specific object to be returned, for whatever reason, the caller has to do the comparison.
859
            }
860

861
            // Basic early validation
862

863
            if (hasChildren && hasText)
954,847✔
864
            {
15✔
865
                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✔
866

867
                // we'll just fall through and try to parse anyway, though
868
            }
15✔
869

870
            if (typeof(Dec).IsAssignableFrom(type) && hasChildren && !isRootDec)
954,847✔
871
            {
×
872
                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.");
×
873
                return null;
×
874
            }
875

876
            // Defer off to converters, whatever they feel like doing
877
            if (converter != null)
954,847✔
878
            {
605✔
879
                // string converter
880
                if (converter is ConverterString converterString)
605✔
881
                {
260✔
882
                    foreach (var (parseCommand, node) in orders)
1,300✔
883
                    {
260✔
884
                        switch (parseCommand)
260✔
885
                        {
886
                            case ParseCommand.Replace:
887
                                // easy, done
888
                                break;
260✔
889

890
                            default:
891
                                Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
892
                                break;
×
893
                        }
894

895
                        if (hasChildren)
260✔
896
                        {
15✔
897
                            Dbg.Err($"{node.GetContext()}: String converter {converter.GetType()} called with child XML nodes, which will be ignored");
15✔
898
                        }
15✔
899

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

902
                        // context might be null; that's OK at the moment
903
                        try
904
                        {
260✔
905
                            result = converterString.ReadObj(node.GetText() ?? "", node.GetContext());
260✔
906
                        }
175✔
907
                        catch (Exception e)
85✔
908
                        {
85✔
909
                            Dbg.Ex(new ConverterReadException(node.GetContext(), converter, e));
85✔
910

911
                            result = GenerateResultFallback(result, type);
85✔
912
                        }
85✔
913
                    }
260✔
914
                }
260✔
915
                else if (converter is ConverterRecord converterRecord)
345✔
916
                {
235✔
917
                    foreach (var (parseCommand, node) in orders)
1,175✔
918
                    {
235✔
919
                        switch (parseCommand)
235✔
920
                        {
921
                            case ParseCommand.Patch:
922
                                // easy, done
923
                                break;
235✔
924

925
                            case ParseCommand.Replace:
926
                                result = null;
×
927
                                break;
×
928

929
                            default:
930
                                Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
931
                                break;
×
932
                        }
933

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

936
                        if (result == null && !isNullable)
235✔
937
                        {
130✔
938
                            result = type.CreateInstanceSafe("converterrecord", node);
130✔
939
                        }
130✔
940

941
                        // context might be null; that's OK at the moment
942
                        if (result != null || isNullable)
235✔
943
                        {
235✔
944
                            var recorderReader = new RecorderReader(node, globals, trackUsage: true);
235✔
945
                            try
946
                            {
235✔
947
                                object returnedResult = converterRecord.RecordObj(result, recorderReader);
235✔
948

949
                                if (!type.IsValueType && result != returnedResult)
220✔
950
                                {
×
951
                                    Dbg.Err($"{node.GetContext()}: Converter {converterRecord.GetType()} changed object instance, this is disallowed");
×
952
                                }
×
953
                                else
954
                                {
220✔
955
                                    // for value types, this is fine
956
                                    result = returnedResult;
220✔
957
                                }
220✔
958

959
                                recorderReader.ReportUnusedFields();
220✔
960
                            }
220✔
961
                            catch (Exception e)
15✔
962
                            {
15✔
963
                                Dbg.Ex(new ConverterReadException(node.GetContext(), converter, e));
15✔
964

965
                                // no fallback needed, we already have a result
966
                            }
15✔
967
                        }
235✔
968
                    }
235✔
969
                }
235✔
970
                else if (converter is ConverterFactory converterFactory)
110✔
971
                {
110✔
972
                    foreach (var (parseCommand, node) in orders)
550✔
973
                    {
110✔
974
                        switch (parseCommand)
110✔
975
                        {
976
                            case ParseCommand.Patch:
977
                                // easy, done
978
                                break;
110✔
979

980
                            case ParseCommand.Replace:
981
                                result = null;
×
982
                                break;
×
983

984
                            default:
985
                                Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
986
                                break;
×
987
                        }
988

989
                        var recorderReader = new RecorderReader(node, globals, disallowShared: true, trackUsage: true);
110✔
990
                        if (result == null)
110✔
991
                        {
105✔
992
                            try
993
                            {
105✔
994
                                result = converterFactory.CreateObj(recorderReader);
105✔
995
                            }
90✔
996
                            catch (Exception e)
15✔
997
                            {
15✔
998
                                Dbg.Ex(new ConverterReadException(node.GetContext(), converter, e));
15✔
999
                            }
15✔
1000
                        }
105✔
1001

1002
                        // context might be null; that's OK at the moment
1003
                        if (result != null)
110✔
1004
                        {
95✔
1005
                            recorderReader.AllowShared(globals);
95✔
1006
                            try
1007
                            {
95✔
1008
                                result = converterFactory.ReadObj(result, recorderReader);
95✔
1009
                                recorderReader.ReportUnusedFields();
80✔
1010
                            }
80✔
1011
                            catch (Exception e)
15✔
1012
                            {
15✔
1013
                                Dbg.Ex(new ConverterReadException(node.GetContext(), converter, e));
15✔
1014

1015
                                // no fallback needed, we already have a result
1016
                            }
15✔
1017
                        }
95✔
1018
                    }
110✔
1019
                }
110✔
1020
                else
1021
                {
×
1022
                    Dbg.Err($"Somehow ended up with an unsupported converter {converter.GetType()}");
×
1023
                }
×
1024

1025
                return result;
605✔
1026
            }
1027

1028
            // All our standard text-using options
1029
            // Placed before IRecordable just in case we have a Dec that is IRecordable
1030
            if ((typeof(Dec).IsAssignableFrom(type) && !isRootDec) ||
954,242✔
1031
                    type == typeof(Type) ||
954,242✔
1032
                    type == typeof(string) ||
954,242✔
1033
                    type.IsPrimitive ||
954,242✔
1034
                    (TypeDescriptor.GetConverter(type)?.CanConvertFrom(typeof(string)) ?? false)   // this is last because it's slow
954,242✔
1035
                )
954,242✔
1036
            {
397,811✔
1037
                foreach (var (parseCommand, node) in orders)
1,989,055✔
1038
                {
397,811✔
1039
                    switch (parseCommand)
397,811✔
1040
                    {
1041
                        case ParseCommand.Replace:
1042
                            // easy, done
1043
                            break;
397,811✔
1044

1045
                        default:
1046
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1047
                            break;
×
1048
                    }
1049

1050
                    if (hasChildren)
397,811✔
1051
                    {
20✔
1052
                        Dbg.Err($"{node.GetContext()}: Child nodes are not valid when parsing {type}");
20✔
1053
                    }
20✔
1054

1055
                    result = ParseString(node.GetText(), type, result, node.GetContext());
397,811✔
1056
                }
397,811✔
1057

1058
                return result;
397,811✔
1059
            }
1060

1061
            // Special case: IRecordables
1062
            IRecordable recordableBuffered = null;
556,431✔
1063
            if (typeof(IRecordable).IsAssignableFrom(type))
556,431✔
1064
            {
392,390✔
1065
                // we're going to need to make one anyway so let's just go ahead and do that
1066
                IRecordable recordable = null;
392,390✔
1067

1068
                if (result != null)
392,390✔
1069
                {
206,775✔
1070
                    recordable = (IRecordable)result;
206,775✔
1071
                }
206,775✔
1072
                else if (recSettings.factories == null)
185,615✔
1073
                {
184,585✔
1074
                    recordable = (IRecordable)type.CreateInstanceSafe("recordable", orders[0].node);
184,585✔
1075
                }
184,585✔
1076
                else
1077
                {
1,030✔
1078
                    recordable = recSettings.CreateRecordableFromFactory(type, "recordable", orders[0].node);
1,030✔
1079
                }
1,030✔
1080

1081
                // we hold on to this so that, *if* we end up not using this object, we can optionally reuse it later for reflection
1082
                // 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
1083
                recordableBuffered = recordable;
392,390✔
1084

1085
                var conditionalRecordable = recordable as IConditionalRecordable;
392,390✔
1086
                if (conditionalRecordable == null || conditionalRecordable.ShouldRecord(nodes[0].UserSettings))
392,390✔
1087
                {
392,375✔
1088
                    foreach (var (parseCommand, node) in orders)
1,961,875✔
1089
                    {
392,375✔
1090
                        switch (parseCommand)
392,375✔
1091
                        {
1092
                            case ParseCommand.Patch:
1093
                                // easy, done
1094
                                break;
392,375✔
1095

1096
                            default:
1097
                                Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1098
                                break;
×
1099
                        }
1100

1101
                        if (recordable != null)
392,375✔
1102
                        {
392,355✔
1103
                            var recorderReader = new RecorderReader(node, globals, trackUsage: true);
392,355✔
1104
                            recordable.Record(recorderReader);
392,355✔
1105
                            recorderReader.ReportUnusedFields();
392,355✔
1106

1107
                            // TODO: support indices if this is within the Dec system?
1108
                        }
392,355✔
1109
                    }
392,375✔
1110

1111
                    result = recordable;
392,375✔
1112
                    return result;
392,375✔
1113
                }
1114

1115
                // otherwise we just fall through
1116
            }
15✔
1117

1118
            // Nothing past this point even supports text, so let's just get angry and break stuff.
1119
            if (hasText)
164,056✔
1120
            {
80✔
1121
                Dbg.Err($"{hasTextNode.GetContext()}: Text detected in a situation where it is invalid; will be ignored");
80✔
1122
                return result;
80✔
1123
            }
1124

1125
            // Special case: Lists
1126
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
163,976✔
1127
            {
46,505✔
1128
                foreach (var (parseCommand, node) in orders)
232,525✔
1129
                {
46,505✔
1130
                    switch (parseCommand)
46,505✔
1131
                    {
1132
                        case ParseCommand.Replace:
1133
                            // 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.
1134
                            // 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.
1135
                            // 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.
1136
                            if (result != null)
46,425✔
1137
                            {
420✔
1138
                                ((IList)result).Clear();
420✔
1139
                            }
420✔
1140
                            break;
46,425✔
1141

1142
                        case ParseCommand.Append:
1143
                            // we're good
1144
                            break;
80✔
1145

1146
                        default:
1147
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1148
                            break;
×
1149
                    }
1150

1151
                    // List<> handling
1152
                    Type referencedType = type.GetGenericArguments()[0];
46,505✔
1153

1154
                    var list = (IList)(result ?? Activator.CreateInstance(type));
46,505✔
1155

1156
                    node.ParseList(list, referencedType, globals, recSettings);
46,505✔
1157

1158
                    result = list;
46,505✔
1159
                }
46,505✔
1160

1161
                return result;
46,505✔
1162
            }
1163

1164
            // Special case: Arrays
1165
            if (type.IsArray)
117,471✔
1166
            {
3,095✔
1167
                Type referencedType = type.GetElementType();
3,095✔
1168

1169
                foreach (var (parseCommand, node) in orders)
15,475✔
1170
                {
3,095✔
1171
                    Array array = null;
3,095✔
1172
                    int startOffset = 0;
3,095✔
1173

1174
                    // 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.
1175
                    switch (parseCommand)
3,095✔
1176
                    {
1177
                        case ParseCommand.Replace:
1178
                        {
3,060✔
1179
                            // This is a full override, so we're going to create it here.
1180
                            // It is actually vitally important that we fall back on the model when possible, because the Recorder Ref system requires it.
1181
                            bool match = result != null && result.GetType() == type;
3,060✔
1182
                            var arrayDimensions = node.GetArrayDimensions(type.GetArrayRank());
3,060✔
1183
                            if (match)
3,060✔
1184
                            {
410✔
1185
                                array = (Array)result;
410✔
1186
                                if (array.Rank != type.GetArrayRank())
410✔
1187
                                {
×
1188
                                    match = false;
×
1189
                                }
×
1190
                                else
1191
                                {
410✔
1192
                                    for (int i = 0; i < array.Rank; i++)
1,040✔
1193
                                    {
420✔
1194
                                        if (array.GetLength(i) != arrayDimensions[i])
420✔
1195
                                        {
310✔
1196
                                            match = false;
310✔
1197
                                            break;
310✔
1198
                                        }
1199
                                    }
110✔
1200
                                }
410✔
1201
                            }
410✔
1202

1203
                            if (!match)
3,060✔
1204
                            {
2,960✔
1205
                                // Otherwise just make a new one, no harm done.
1206
                                array = Array.CreateInstance(referencedType, arrayDimensions);
2,960✔
1207
                            }
2,960✔
1208

1209
                            break;
3,060✔
1210
                        }
1211

1212
                        case ParseCommand.Append:
1213
                        {
55✔
1214
                            if (result == null)
55✔
1215
                            {
20✔
1216
                                goto case ParseCommand.Replace;
20✔
1217
                            }
1218

1219
                            // This is jankier; we create it here with the intended final length, then copy the elements over, all because arrays can't be resized
1220
                            // (yes, I know, that's the point of arrays, I'm not complaining, just . . . grumbling a little)
1221
                            var oldArray = (Array)result;
35✔
1222
                            startOffset = oldArray.Length;
35✔
1223
                            var arrayDimensions = node.GetArrayDimensions(type.GetArrayRank());
35✔
1224
                            arrayDimensions[0] += startOffset;
35✔
1225
                            array = Array.CreateInstance(referencedType, arrayDimensions);
35✔
1226
                            if (arrayDimensions.Length == 1)
35✔
1227
                            {
20✔
1228
                                oldArray.CopyTo(array, 0);
20✔
1229
                            }
20✔
1230
                            else
1231
                            {
15✔
1232
                                // oy
1233
                                void CopyArray(Array source, Array destination, int[] indices, int rank = 0)
1234
                                {
60✔
1235
                                    if (rank < source.Rank)
60✔
1236
                                    {
45✔
1237
                                        for (int i = 0; i < source.GetLength(rank); i++)
180✔
1238
                                        {
45✔
1239
                                            indices[rank] = i;
45✔
1240
                                            CopyArray(source, destination, indices, rank + 1);
45✔
1241
                                        }
45✔
1242
                                    }
45✔
1243
                                    else
1244
                                    {
15✔
1245
                                        destination.SetValue(source.GetValue(indices), indices);
15✔
1246
                                    }
15✔
1247
                                }
60✔
1248

1249
                                {
15✔
1250
                                    int[] indices = new int[arrayDimensions.Length];
15✔
1251
                                    CopyArray(oldArray, array, indices, 0);
15✔
1252
                                }
15✔
1253
                            }
15✔
1254

1255
                            break;
35✔
1256
                        }
1257

1258
                        default:
1259
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1260
                            array = null; // just to break the unassigned-local-variable
×
1261
                            break;
×
1262
                    }
1263

1264
                    node.ParseArray(array, referencedType, globals, recSettings, startOffset);
3,095✔
1265

1266
                    result = array;
3,095✔
1267
                }
3,095✔
1268

1269
                return result;
3,095✔
1270
            }
1271

1272
            // Special case: Dictionaries
1273
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
114,376✔
1274
            {
23,710✔
1275
                foreach (var (parseCommand, node) in orders)
118,550✔
1276
                {
23,710✔
1277
                    bool permitPatch = false;
23,710✔
1278
                    switch (parseCommand)
23,710✔
1279
                    {
1280
                        case ParseCommand.Replace:
1281
                            // 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.
1282
                            // 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.
1283
                            // 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.
1284
                            if (result != null)
23,470✔
1285
                            {
485✔
1286
                                ((IDictionary)result).Clear();
485✔
1287
                            }
485✔
1288
                            break;
23,470✔
1289

1290
                        case ParseCommand.Patch:
1291
                            if (original != null)
160✔
1292
                            {
140✔
1293
                                permitPatch = true;
140✔
1294
                            }
140✔
1295
                            break;
160✔
1296

1297
                        case ParseCommand.Append:
1298
                            // nothing needs to be done, our existing dupe checking will solve it
1299
                            break;
80✔
1300

1301
                        default:
1302
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1303
                            break;
×
1304
                    }
1305

1306
                    // Dictionary<> handling
1307
                    Type keyType = type.GetGenericArguments()[0];
23,710✔
1308
                    Type valueType = type.GetGenericArguments()[1];
23,710✔
1309

1310
                    var dict = (IDictionary)(result ?? Activator.CreateInstance(type));
23,710✔
1311

1312
                    node.ParseDictionary(dict, keyType, valueType, globals, recSettings, permitPatch);
23,710✔
1313

1314
                    result = dict;
23,710✔
1315
                }
23,710✔
1316

1317
                return result;
23,710✔
1318
            }
1319

1320
            // Special case: HashSet
1321
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(HashSet<>))
90,666✔
1322
            {
1,010✔
1323
                foreach (var (parseCommand, node) in orders)
5,050✔
1324
                {
1,010✔
1325
                    bool permitPatch = false;
1,010✔
1326
                    switch (parseCommand)
1,010✔
1327
                    {
1328
                        case ParseCommand.Replace:
1329
                            // 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.
1330
                            // 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.
1331
                            // 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.
1332
                            if (result != null)
850✔
1333
                            {
450✔
1334
                                // Did you know there's no non-generic interface that HashSet<> supports that includes a Clear function?
1335
                                // Fun fact:
1336
                                // That thing I just wrote!
1337
                                var clearFunction = result.GetType().GetMethod("Clear");
450✔
1338
                                clearFunction.Invoke(result, null);
450✔
1339
                            }
450✔
1340
                            break;
850✔
1341

1342
                        case ParseCommand.Patch:
1343
                            if (original != null)
80✔
1344
                            {
60✔
1345
                                permitPatch = true;
60✔
1346
                            }
60✔
1347
                            break;
80✔
1348

1349
                        case ParseCommand.Append:
1350
                            // nothing needs to be done, our existing dupe checking will solve it
1351
                            break;
80✔
1352

1353
                        default:
1354
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1355
                            break;
×
1356
                    }
1357

1358
                    Type keyType = type.GetGenericArguments()[0];
1,010✔
1359

1360
                    var set = result ?? Activator.CreateInstance(type);
1,010✔
1361

1362
                    node.ParseHashset(set, keyType, globals, recSettings, permitPatch);
1,010✔
1363

1364
                    result = set;
1,010✔
1365
                }
1,010✔
1366

1367
                return result;
1,010✔
1368
            }
1369

1370
            // Special case: Stack
1371
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Stack<>))
89,656✔
1372
            {
55✔
1373
                // Stack<> handling
1374
                // Again, no sensible non-generic interface to use, so we're stuck with reflection
1375

1376
                foreach (var (parseCommand, node) in orders)
275✔
1377
                {
55✔
1378
                    switch (parseCommand)
55✔
1379
                    {
1380
                        case ParseCommand.Replace:
1381
                            // 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.
1382
                            // 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.
1383
                            // 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.
1384
                            if (result != null)
55✔
1385
                            {
5✔
1386
                                var clearFunction = result.GetType().GetMethod("Clear");
5✔
1387
                                clearFunction.Invoke(result, null);
5✔
1388
                            }
5✔
1389
                            break;
55✔
1390

1391
                        case ParseCommand.Append:
1392
                            break;
×
1393

1394
                        // There definitely starts being an argument for prepend.
1395

1396
                        default:
1397
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1398
                            break;
×
1399
                    }
1400

1401
                    Type keyType = type.GetGenericArguments()[0];
55✔
1402

1403
                    var set = result ?? Activator.CreateInstance(type);
55✔
1404

1405
                    node.ParseStack(set, keyType, globals, recSettings);
55✔
1406

1407
                    result = set;
55✔
1408
                }
55✔
1409

1410
                return result;
55✔
1411
            }
1412

1413
            // Special case: Queue
1414
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Queue<>))
89,601✔
1415
            {
55✔
1416
                // Queue<> handling
1417
                // Again, no sensible non-generic interface to use, so we're stuck with reflection
1418

1419
                foreach (var (parseCommand, node) in orders)
275✔
1420
                {
55✔
1421
                    switch (parseCommand)
55✔
1422
                    {
1423
                        case ParseCommand.Replace:
1424
                            // 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.
1425
                            // 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.
1426
                            // 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.
1427
                            if (result != null)
55✔
1428
                            {
5✔
1429
                                var clearFunction = result.GetType().GetMethod("Clear");
5✔
1430
                                clearFunction.Invoke(result, null);
5✔
1431
                            }
5✔
1432
                            break;
55✔
1433

1434
                        case ParseCommand.Append:
1435
                            break;
×
1436

1437
                        // There definitely starts being an argument for prepend.
1438

1439
                        default:
1440
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1441
                            break;
×
1442
                    }
1443

1444
                    Type keyType = type.GetGenericArguments()[0];
55✔
1445

1446
                    var set = result ?? Activator.CreateInstance(type);
55✔
1447

1448
                    node.ParseQueue(set, keyType, globals, recSettings);
55✔
1449

1450
                    result = set;
55✔
1451
                }
55✔
1452

1453
                return result;
55✔
1454
            }
1455

1456
            // Special case: A bucket of tuples
1457
            // These are all basically identical, but AFAIK there's no good way to test them all in a better way.
1458
            if (type.IsGenericType && (
89,546✔
1459
                    type.GetGenericTypeDefinition() == typeof(Tuple<>) ||
89,546✔
1460
                    type.GetGenericTypeDefinition() == typeof(Tuple<,>) ||
89,546✔
1461
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,>) ||
89,546✔
1462
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,>) ||
89,546✔
1463
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,>) ||
89,546✔
1464
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,>) ||
89,546✔
1465
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,>) ||
89,546✔
1466
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,,>) ||
89,546✔
1467
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<>) ||
89,546✔
1468
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,>) ||
89,546✔
1469
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,>) ||
89,546✔
1470
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,>) ||
89,546✔
1471
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,>) ||
89,546✔
1472
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,>) ||
89,546✔
1473
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,>) ||
89,546✔
1474
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,,>)
89,546✔
1475
                    ))
89,546✔
1476
            {
1,070✔
1477
                foreach (var (parseCommand, node) in orders)
5,350✔
1478
                {
1,070✔
1479
                    switch (parseCommand)
1,070✔
1480
                    {
1481
                        case ParseCommand.Replace:
1482
                            // easy, done
1483
                            break;
1,070✔
1484

1485
                        default:
1486
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1487
                            break;
×
1488
                    }
1489

1490
                    int expectedCount = type.GenericTypeArguments.Length;
1,070✔
1491
                    object[] parameters = new object[expectedCount];
1,070✔
1492

1493
                    node.ParseTuple(parameters, type, fieldInfo?.GetCustomAttribute<System.Runtime.CompilerServices.TupleElementNamesAttribute>()?.TransformNames, globals, recSettings);
1,070✔
1494

1495
                    // construct!
1496
                    result = Activator.CreateInstance(type, parameters);
1,070✔
1497
                }
1,070✔
1498

1499
                return result;
1,070✔
1500
            }
1501

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

1504
            // If we have refs, something has gone wrong; we should never be doing reflection inside a Record system.
1505
            // This is a really ad-hoc way of testing this and should be fixed.
1506
            // 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.
1507
            // I'm less OK with security vulnerabilities in save files. Nobody expects a savefile can compromise their system.
1508
            // And the full reflection system is probably impossible to secure, whereas the Record system should be secureable.
1509
            if (!globals.allowReflection)
88,476✔
1510
            {
55✔
1511
                // just pick the first node to get something to go on
1512
                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)");
55✔
1513
                return result;
55✔
1514
            }
1515

1516
            foreach (var (parseCommand, node) in orders)
445,415✔
1517
            {
90,116✔
1518
                if (!isRootDec)
90,116✔
1519
                {
44,021✔
1520
                    switch (parseCommand)
44,021✔
1521
                    {
1522
                        case ParseCommand.Patch:
1523
                            // easy, done
1524
                            break;
44,021✔
1525

1526
                        default:
1527
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1528
                            break;
×
1529
                    }
1530
                }
44,021✔
1531
                else
1532
                {
46,095✔
1533
                    if (parseCommand != ParseCommand.Patch)
46,095✔
1534
                    {
×
1535
                        Dbg.Err($"{node.GetContext()}: Mode provided for root Dec; this is currently not supported in any form");
×
1536
                    }
×
1537
                }
46,095✔
1538

1539
                // If we haven't been given a generic class from our parent, go ahead and init to defaults
1540
                if (result == null && recordableBuffered != null)
90,116✔
1541
                {
15✔
1542
                    result = recordableBuffered;
15✔
1543
                }
15✔
1544

1545
                if (result == null)
90,116✔
1546
                {
17,640✔
1547
                    // okay fine
1548
                    result = type.CreateInstanceSafe("object", node);
17,640✔
1549

1550
                    if (result == null)
17,640✔
1551
                    {
80✔
1552
                        // error already reported
1553
                        return result;
80✔
1554
                    }
1555
                }
17,560✔
1556

1557
                node.ParseReflection(result, globals, recSettings);
90,036✔
1558
            }
90,036✔
1559

1560
            // Set up our index fields; this has to happen last in case we're a struct
1561
            Index.Register(ref result);
88,341✔
1562

1563
            return result;
88,341✔
1564
        }
1,562,429✔
1565

1566
        internal static object ParseString(string text, Type type, object original, Context context)
1567
        {
815,392✔
1568
            // Special case: Converter override
1569
            // This is redundant if we're being called from ParseElement, but we aren't always.
1570
            if (ConverterFor(type) is Converter converter)
815,392✔
1571
            {
45✔
1572
                object result = original;
45✔
1573

1574
                try
1575
                {
45✔
1576
                    // string converter
1577
                    if (converter is ConverterString converterString)
45✔
1578
                    {
45✔
1579
                        // context might be null; that's OK at the moment
1580
                        try
1581
                        {
45✔
1582
                            result = converterString.ReadObj(text, context);
45✔
1583
                        }
45✔
1584
                        catch (Exception e)
×
1585
                        {
×
1586
                            Dbg.Ex(new ConverterReadException(context, converter, e));
×
1587

1588
                            result = GenerateResultFallback(result, type);
×
1589
                        }
×
1590
                    }
45✔
1591
                    else if (converter is ConverterRecord converterRecord)
×
1592
                    {
×
1593
                        // string parsing really doesn't apply here, we can't get a full Recorder context out anymore
1594
                        // in theory this could be done with RecordAsThis() but I'm just going to skip it for now
1595
                        Dbg.Err($"{context}: Attempt to string-parse with a ConverterRecord, this is currently not supported, contact developers if you need this feature");
×
1596
                    }
×
1597
                    else if (converter is ConverterFactory converterFactory)
×
1598
                    {
×
1599
                        // string parsing really doesn't apply here, we can't get a full Recorder context out anymore
1600
                        // in theory this could be done with RecordAsThis() but I'm just going to skip it for now
1601
                        Dbg.Err($"{context}: Attempt to string-parse with a ConverterFactory, this is currently not supported, contact developers if you need this feature");
×
1602
                    }
×
1603
                    else
1604
                    {
×
1605
                        Dbg.Err($"Somehow ended up with an unsupported converter {converter.GetType()}");
×
1606
                    }
×
1607
                }
45✔
1608
                catch (Exception e)
×
1609
                {
×
1610
                    Dbg.Ex(e);
×
1611
                }
×
1612

1613
                return result;
45✔
1614
            }
1615

1616
            // Special case: decs
1617
            if (typeof(Dec).IsAssignableFrom(type))
815,347✔
1618
            {
48,285✔
1619
                if (text == "" || text == null)
48,285✔
1620
                {
40,695✔
1621
                    // you reference nothing, you get the null (even if this isn't a specified type; null is null, after all)
1622
                    return null;
40,695✔
1623
                }
1624
                else
1625
                {
7,590✔
1626
                    if (type.GetDecRootType() == null)
7,590✔
1627
                    {
80✔
1628
                        Dbg.Err($"{context}: Non-hierarchy decs cannot be used as references");
80✔
1629
                        return null;
80✔
1630
                    }
1631

1632
                    Dec result = Database.Get(type, text);
7,510✔
1633
                    if (result == null)
7,510✔
1634
                    {
85✔
1635
                        if (UtilMisc.ValidateDecName(text, context))
85✔
1636
                        {
85✔
1637
                            Dbg.Err($"{context}: Couldn't find {type} named `{text}`");
85✔
1638
                        }
85✔
1639

1640
                        // If we're an invalid name, we already spat out the error
1641
                    }
85✔
1642
                    return result;
7,510✔
1643
                }
1644
            }
1645

1646
            // Special case: types
1647
            if (type == typeof(Type))
767,062✔
1648
            {
415,356✔
1649
                if (text == "")
415,356✔
1650
                {
×
1651
                    return null;
×
1652
                }
1653

1654
                return UtilType.ParseDecFormatted(text, context);
415,356✔
1655
            }
1656

1657
            // Various non-composite-type special-cases
1658
            if (text != "")
351,706✔
1659
            {
351,706✔
1660
                // If we've got text, treat us as an object of appropriate type
1661
                try
1662
                {
351,706✔
1663
                    if (type == typeof(float))
351,706✔
1664
                    {
28,250✔
1665
                        // first check the various strings, case-insensitive
1666
                        if (String.Compare(text, "nan", true) == 0)
28,250✔
1667
                        {
90✔
1668
                            return float.NaN;
90✔
1669
                        }
1670

1671
                        if (String.Compare(text, "infinity", true) == 0)
28,160✔
1672
                        {
40✔
1673
                            return float.PositiveInfinity;
40✔
1674
                        }
1675

1676
                        if (String.Compare(text, "-infinity", true) == 0)
28,120✔
1677
                        {
40✔
1678
                            return float.NegativeInfinity;
40✔
1679
                        }
1680

1681
                        if (text.StartsWith("nanbox", StringComparison.CurrentCultureIgnoreCase))
28,080✔
1682
                        {
85✔
1683
                            const int expectedFloatSize = 6 + 8;
1684

1685
                            if (type == typeof(float) && text.Length != expectedFloatSize)
85✔
1686
                            {
×
1687
                                Dbg.Err($"{context}: Found nanboxed value without the expected number of characters, expected {expectedFloatSize} but got {text.Length}");
×
1688
                                return float.NaN;
×
1689
                            }
1690

1691
                            int number = Convert.ToInt32(text.Substring(6), 16);
85✔
1692
                            return BitConverter.Int32BitsToSingle(number);
85✔
1693
                        }
1694
                    }
27,995✔
1695

1696
                    if (type == typeof(double))
351,451✔
1697
                    {
2,840✔
1698
                        // first check the various strings, case-insensitive
1699
                        if (String.Compare(text, "nan", true) == 0)
2,840✔
1700
                        {
1,815✔
1701
                            return double.NaN;
1,815✔
1702
                        }
1703

1704
                        if (String.Compare(text, "infinity", true) == 0)
1,025✔
1705
                        {
40✔
1706
                            return double.PositiveInfinity;
40✔
1707
                        }
1708

1709
                        if (String.Compare(text, "-infinity", true) == 0)
985✔
1710
                        {
40✔
1711
                            return double.NegativeInfinity;
40✔
1712
                        }
1713

1714
                        if (text.StartsWith("nanbox", StringComparison.CurrentCultureIgnoreCase))
945✔
1715
                        {
75✔
1716
                            const int expectedDoubleSize = 6 + 16;
1717

1718
                            if (type == typeof(double) && text.Length != expectedDoubleSize)
75✔
1719
                            {
×
1720
                                Dbg.Err($"{context}: Found nanboxed value without the expected number of characters, expected {expectedDoubleSize} but got {text.Length}");
×
1721
                                return double.NaN;
×
1722
                            }
1723

1724
                            long number = Convert.ToInt64(text.Substring(6), 16);
75✔
1725
                            return BitConverter.Int64BitsToDouble(number);
75✔
1726
                        }
1727
                    }
870✔
1728

1729
                    return TypeDescriptor.GetConverter(type).ConvertFromString(text);
349,481✔
1730
                }
1731
                catch (System.Exception e)  // I would normally not catch System.Exception, but TypeConverter is wrapping FormatException in an Exception for some reason
180✔
1732
                {
180✔
1733
                    Dbg.Err($"{context}: {e.ToString()}");
180✔
1734
                    return original;
180✔
1735
                }
1736
            }
1737
            else if (type == typeof(string))
×
1738
            {
×
1739
                // If we don't have text, and we're a string, return ""
1740
                return "";
×
1741
            }
1742
            else
1743
            {
×
1744
                // If we don't have text, and we've fallen down to this point, that's an error (and return original value I guess)
1745
                Dbg.Err($"{context}: Empty field provided for type {type}");
×
1746
                return original;
×
1747
            }
1748
        }
815,392✔
1749

1750
        internal static Type TypeSystemRuntimeType = Type.GetType("System.RuntimeType");
10✔
1751
        internal static void ComposeElement(WriterNode node, object value, Type fieldType, FieldInfo fieldInfo = null, bool isRootDec = false, bool asThis = false)
1752
        {
1,461,574✔
1753
            // Verify our Shared flags as the *very* first step to ensure nothing gets past us.
1754
            // 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
1755
            bool canBeShared = fieldType.CanBeShared();
1,461,574✔
1756
            if (node.RecorderSettings.shared == Recorder.Settings.Shared.Allow && !asThis)
1,461,574✔
1757
            {
801,820✔
1758
                // 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.
1759
                if (!canBeShared)
801,820✔
1760
                {
120✔
1761
                    // If shared, make sure our type is appropriate for sharing
1762
                    // this really needs the recorder name and the field name too
1763
                    Dbg.Wrn($"Value type `{fieldType}` tagged as Shared in recorder, this is meaningless but harmless");
120✔
1764
                }
120✔
1765
            }
801,820✔
1766

1767
            // Handle Dec types, if this isn't a root (otherwise we'd just reference ourselves and that's kind of pointless)
1768
            if (!isRootDec && value is Dec)
1,461,574✔
1769
            {
3,870✔
1770
                // Dec types are special in a few ways.
1771
                // First off, they don't include their type data, because we assume it's of a type provided by the structure.
1772
                // Second, we represent null values as an empty string, not as a null tag.
1773
                // (We'll accept the null tag if you insist, we just have a cleaner special case.)
1774
                // Null tag stuff is done further down, in the null check.
1775

1776
                var rootType = value.GetType().GetDecRootType();
3,870✔
1777
                if (!rootType.IsAssignableFrom(fieldType))
3,870✔
1778
                {
30✔
1779
                    // The user has a Dec.Dec or similar, and it has a Dec assigned to it.
1780
                    // 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.
1781
                    // But we're OK with that, honestly. We just do that.
1782
                    // If you're saving something like this you don't get to rename Dec classes later on, but, hey, deal with it.
1783
                    // 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.
1784
                    node.TagClass(rootType);
30✔
1785
                }
30✔
1786

1787
                node.WriteDec(value as Dec);
3,870✔
1788

1789
                return;
3,870✔
1790
            }
1791

1792
            // Everything represents "null" with an explicit XML tag, so let's just do that
1793
            // Maybe at some point we want to special-case this for the empty Dec link
1794
            if (value == null)
1,457,704✔
1795
            {
366,872✔
1796
                if (typeof(Dec).IsAssignableFrom(fieldType))
366,872✔
1797
                {
33,950✔
1798
                    node.WriteDec(null);
33,950✔
1799
                }
33,950✔
1800
                else
1801
                {
332,922✔
1802
                    node.WriteExplicitNull();
332,922✔
1803
                }
332,922✔
1804

1805
                return;
366,872✔
1806
            }
1807

1808

1809
            if (node.AllowDecPath)
1,090,832✔
1810
            {
744,405✔
1811
                // Try to snag a Dec path
1812
                var decPath = Database.DecPathLookup.TryGetValue(value);
744,405✔
1813

1814
                if (decPath != null)
744,405✔
1815
                {
280✔
1816
                    node.WriteDecPathRef(value);
280✔
1817
                    return;
280✔
1818
                }
1819
            }
744,125✔
1820

1821
            var valType = value.GetType();
1,090,552✔
1822

1823
            // This is our value's type, but we may need a little bit of tinkering to make it useful.
1824
            // The current case I know of is System.RuntimeType, which appears if we call .GetType() on a Type.
1825
            // I assume there is a complicated internal reason for this; good news, we can ignore it and just pretend it's a System.Type.
1826
            // Bad news: it's actually really hard to detect this case because System.RuntimeType is private.
1827
            // That's why we have the annoying `static` up above.
1828
            if (valType == TypeSystemRuntimeType)
1,090,552✔
1829
            {
255✔
1830
                valType = typeof(Type);
255✔
1831
            }
255✔
1832

1833
            // Do all our unreferencables first
1834
            bool unreferenceableComplete = false;
1,090,552✔
1835

1836
            if (valType.IsPrimitive)
1,090,552✔
1837
            {
249,581✔
1838
                node.WritePrimitive(value);
249,581✔
1839

1840
                unreferenceableComplete = true;
249,581✔
1841
            }
249,581✔
1842
            else if (value is System.Enum)
840,971✔
1843
            {
305✔
1844
                node.WriteEnum(value);
305✔
1845

1846
                unreferenceableComplete = true;
305✔
1847
            }
305✔
1848
            else if (value is string)
840,666✔
1849
            {
27,695✔
1850
                node.WriteString(value as string);
27,695✔
1851

1852
                unreferenceableComplete = true;
27,695✔
1853
            }
27,695✔
1854
            else if (value is Type)
812,971✔
1855
            {
255✔
1856
                node.WriteType(value as Type);
255✔
1857

1858
                unreferenceableComplete = true;
255✔
1859
            }
255✔
1860

1861
            // Check to see if we should make this into a ref (yes, even if we're not tagged as Shared)
1862
            // Do this *before* we do the class tagging, otherwise we may add ref/class tags to a single node, which is invalid.
1863
            // 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.
1864
            if (Util.CanBeShared(valType) && !asThis)
1,090,552✔
1865
            {
783,401✔
1866
                if (node.WriteReference(value, node.Path))
783,401✔
1867
                {
150,860✔
1868
                    // The ref system has set up the appropriate tagging, so we're done!
1869
                    return;
150,860✔
1870
                }
1871

1872
                // If we support references, then this object has not previously shown up in the reference system; keep going so we finish serializing it.
1873
                // If we don't support references at all then obviously we *really* need to finish serializing it.
1874
            }
632,541✔
1875

1876
            // 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`.
1877
            if (valType != fieldType)
939,692✔
1878
            {
1,396✔
1879
                if (asThis)
1,396✔
1880
                {
20✔
1881
                    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✔
1882
                    // . . . I guess we just keep going?
1883
                }
20✔
1884
                else
1885
                {
1,376✔
1886
                    node.TagClass(valType);
1,376✔
1887
                }
1,376✔
1888
            }
1,396✔
1889

1890
            // Did we actually write our node type? Alright, we're done.
1891
            if (unreferenceableComplete)
939,692✔
1892
            {
277,836✔
1893
                return;
277,836✔
1894
            }
1895

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

1898
            if (node.AllowCloning && UtilType.CanBeCloneCopied(valType))
661,856✔
1899
            {
20✔
1900
                node.WriteCloneCopy(value);
20✔
1901

1902
                return;
20✔
1903
            }
1904

1905
            if (valType.IsArray)
661,836✔
1906
            {
2,650✔
1907
                node.WriteArray(value as Array);
2,650✔
1908

1909
                return;
2,650✔
1910
            }
1911

1912
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(List<>))
659,186✔
1913
            {
26,595✔
1914
                node.WriteList(value as IList);
26,595✔
1915

1916
                return;
26,595✔
1917
            }
1918

1919
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
632,591✔
1920
            {
11,830✔
1921
                node.WriteDictionary(value as IDictionary);
11,830✔
1922

1923
                return;
11,830✔
1924
            }
1925

1926
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(HashSet<>))
620,761✔
1927
            {
600✔
1928
                node.WriteHashSet(value as IEnumerable);
600✔
1929

1930
                return;
600✔
1931
            }
1932

1933
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(Queue<>))
620,161✔
1934
            {
50✔
1935
                node.WriteQueue(value as IEnumerable);
50✔
1936

1937
                return;
50✔
1938
            }
1939

1940
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(Stack<>))
620,111✔
1941
            {
50✔
1942
                node.WriteStack(value as IEnumerable);
50✔
1943

1944
                return;
50✔
1945
            }
1946

1947
            if (valType.IsGenericType && (
620,061✔
1948
                    valType.GetGenericTypeDefinition() == typeof(Tuple<>) ||
620,061✔
1949
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,>) ||
620,061✔
1950
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,>) ||
620,061✔
1951
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,>) ||
620,061✔
1952
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,>) ||
620,061✔
1953
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,,>) ||
620,061✔
1954
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,>) ||
620,061✔
1955
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,,>)
620,061✔
1956
                ))
620,061✔
1957
            {
215✔
1958
                node.WriteTuple(value, fieldInfo?.GetCustomAttribute<System.Runtime.CompilerServices.TupleElementNamesAttribute>());
215✔
1959

1960
                return;
215✔
1961
            }
1962

1963
            if (valType.IsGenericType && (
619,846✔
1964
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<>) ||
619,846✔
1965
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,>) ||
619,846✔
1966
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,>) ||
619,846✔
1967
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,>) ||
619,846✔
1968
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,>) ||
619,846✔
1969
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,>) ||
619,846✔
1970
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,>) ||
619,846✔
1971
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,,>)
619,846✔
1972
                ))
619,846✔
1973
            {
390✔
1974
                node.WriteValueTuple(value, fieldInfo?.GetCustomAttribute<System.Runtime.CompilerServices.TupleElementNamesAttribute>());
390✔
1975

1976
                return;
390✔
1977
            }
1978

1979
            if (value is IRecordable
619,456✔
1980
                && (!(value is IConditionalRecordable) || (value as IConditionalRecordable).ShouldRecord(node.UserSettings)))
619,456✔
1981
            {
563,325✔
1982
                node.WriteRecord(value as IRecordable);
563,325✔
1983

1984
                return;
563,325✔
1985
            }
1986

1987
            {
56,131✔
1988
                // Look for a converter; that's the only way to handle this before we fall back to reflection
1989
                var converter = Serialization.ConverterFor(valType);
56,131✔
1990
                if (converter != null)
56,131✔
1991
                {
595✔
1992
                    node.WriteConvertible(converter, value);
595✔
1993
                    return;
595✔
1994
                }
1995
            }
55,536✔
1996

1997
            if (!node.AllowReflection)
55,536✔
1998
            {
50✔
1999
                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)");
50✔
2000
                node.WriteError();
50✔
2001
                return;
50✔
2002
            }
2003

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

2006
            foreach (var field in valType.GetSerializableFieldsFromHierarchy())
759,936✔
2007
            {
296,739✔
2008
                ComposeElement(node.CreateReflectionChild(field, node.RecorderSettings), field.GetValue(value), field.FieldType, fieldInfo: field);
296,739✔
2009
            }
296,739✔
2010

2011
            return;
55,486✔
2012
        }
1,461,574✔
2013

2014
        internal static void Clear()
2015
        {
27,435✔
2016
            ConverterInitialized = false;
27,435✔
2017
            ConverterObjects = new System.Collections.Concurrent.ConcurrentDictionary<Type, Converter>();
27,435✔
2018
            ConverterGenericPrototypes = new System.Collections.Concurrent.ConcurrentDictionary<Type, Type>();
27,435✔
2019
        }
27,435✔
2020
    }
2021
}
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

© 2025 Coveralls, Inc