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

zorbathut / dec / 11709205202

06 Nov 2024 05:57PM UTC coverage: 90.198% (-0.4%) from 90.637%
11709205202

push

github

zorbathut
Added the ability to reference class objects contained within Decs.

4748 of 5264 relevant lines covered (90.2%)

194623.74 hits per line

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

87.63
/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,343,836✔
125
            if (ConverterObjects.TryGetValue(inputType, out var converter))
5,343,836✔
126
            {
5,295,304✔
127
                return converter;
5,295,304✔
128
            }
129

130
            // check for Nullable
131
            if (inputType.IsConstructedGenericType && inputType.GetGenericTypeDefinition() == typeof(Nullable<>))
48,532✔
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)
48,457✔
165
            {
9,237✔
166
                var genericType = inputType.GetGenericTypeDefinition();
9,237✔
167
                if (ConverterGenericPrototypes.TryGetValue(genericType, out var converterType))
9,237✔
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,187✔
180
                    // stub it out so we can do the fast path next time
181
                    ConverterObjects[inputType] = null;
9,187✔
182
                }
9,187✔
183
            }
9,187✔
184

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

190

191
        internal static void Initialize()
192
        {
19,925✔
193
            if (ConverterInitialized)
19,925✔
194
            {
4,665✔
195
                return;
4,665✔
196
            }
197

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

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

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

217
            foreach (var type in conversionTypes)
47,275✔
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
        }
19,925✔
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,124,913✔
304
            if (str == null)
3,124,913✔
305
            {
3,118,403✔
306
                return ParseMode.Default;
3,118,403✔
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,124,913✔
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,516,294✔
368
            var orders = new List<(ParseCommand command, ReaderNodeParseable payload)>();
1,516,294✔
369

370
            if (modeCategory == UtilType.ParseModeCategory.Dec)
1,516,294✔
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,581,470✔
377
            {
1,516,294✔
378
                var context = node.GetContext();
1,516,294✔
379
                var s_parseMode = ParseModeFromString(context, node.GetMetadata(ReaderNodeParseable.Metadata.Mode));
1,516,294✔
380

381
                ParseCommand s_parseCommand;
382

383
                switch (modeCategory)
1,516,294✔
384
                {
385
                    case UtilType.ParseModeCategory.Object:
386
                        switch (s_parseMode)
946,443✔
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;
946,443✔
395
                                break;
946,443✔
396
                        }
397
                        break;
946,443✔
398
                    case UtilType.ParseModeCategory.OrderedContainer:
399
                        switch (s_parseMode)
132,090✔
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;
131,955✔
408
                                break;
131,955✔
409

410
                            case ParseMode.Append:
411
                                s_parseCommand = ParseCommand.Append;
135✔
412
                                break;
135✔
413
                        }
414
                        break;
132,090✔
415
                    case UtilType.ParseModeCategory.UnorderedContainer:
416
                        switch (s_parseMode)
35,950✔
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,550✔
425
                                break;
35,550✔
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;
35,950✔
437
                    case UtilType.ParseModeCategory.Value:
438
                        switch (s_parseMode)
401,811✔
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,811✔
447
                                break;
401,811✔
448
                        }
449
                        break;
401,811✔
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,516,294✔
457
                {
569,316✔
458
                    orders.Clear();
569,316✔
459
                }
569,316✔
460

461
                orders.Add((s_parseCommand, node));
1,516,294✔
462
            }
1,516,294✔
463

464
            return orders;
1,516,294✔
465
        }
1,516,294✔
466

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

476
                switch (s_parseMode)
46,710✔
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,590✔
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,510✔
490
                        everExisted = true;
45,510✔
491
                        break;
45,510✔
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,710✔
561

562
            return orders;
45,590✔
563
        }
45,590✔
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,560,749✔
570
            var result = ParseElement_Worker(nodes, type, original, globals, recSettings, fieldInfo, isRootDec, hasReferenceId, asThis, ordersOverride);
1,560,749✔
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,560,749✔
575
            {
494,357✔
576
                var resultType = result.GetType();
494,357✔
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))
494,357✔
580
                {
108,951✔
581
                    // these paths *should* all match up, so we're just choosing one
582
                    var newPath = nodes[0].GetContext().path;
108,951✔
583

584
                    if (newPath == null)
108,951✔
585
                    {
×
586
                        Dbg.Err("Internal error; missing path somehow? Please report this, thanks!");
×
587
                    }
×
588
                    else
589
                    {
108,951✔
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;
108,951✔
595
                    }
108,951✔
596
                }
108,951✔
597
            }
494,357✔
598

599
            return result;
1,560,749✔
600
        }
1,560,749✔
601

602
        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)
603
        {
1,560,749✔
604
            if (nodes == null || nodes.Count == 0)
1,560,749✔
605
            {
×
606
                Dbg.Err("Internal error, Dec failed to provide nodes to ParseElement. Please report this!");
×
607
                return original;
×
608
            }
609

610
            if (!globals.allowReflection && nodes.Count > 1)
1,560,749✔
611
            {
×
612
                Dbg.Err("Internal error, multiple nodes provided for recorder-mode behavior. Please report this!");
×
613
            }
×
614

615
            // We keep the original around in case of error, but do all our manipulation on a result object.
616
            object result = original;
1,560,749✔
617

618
            // Verify our Shared flags as the *very* first step to ensure nothing gets past us.
619
            // 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
620
            if (recSettings.shared == Recorder.Settings.Shared.Allow)
1,560,749✔
621
            {
808,270✔
622
                if (!type.CanBeShared())
808,270✔
623
                {
100✔
624
                    // If shared, make sure our input is null and our type is appropriate for sharing
625
                    Dbg.Wrn($"{nodes[0].GetContext()}: Value type `{type}` tagged as Shared in recorder, this is meaningless but harmless");
100✔
626
                }
100✔
627
                else if (original != null && !hasReferenceId)
808,170✔
628
                {
25✔
629
                    // We need to create objects without context if it's shared, so we kind of panic in this case
630
                    Dbg.Err($"{nodes[0].GetContext()}: Shared `{type}` provided with non-null default object, this may result in unexpected behavior");
25✔
631
                }
25✔
632
            }
808,270✔
633

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

636
            // Validate all combinations here
637
            // This could definitely be more efficient and skip at least one traversal pass
638
            foreach (var s_node in nodes)
7,807,135✔
639
            {
1,562,444✔
640
                string nullAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Null);
1,562,444✔
641
                string refAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Ref);
1,562,444✔
642
                string classAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Class);
1,562,444✔
643
                string modeAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Mode);
1,562,444✔
644

645
                // Some of these are redundant and that's OK
646
                if (nullAttribute != null && (refAttribute != null || classAttribute != null || modeAttribute != null))
1,562,444✔
647
                {
180✔
648
                    Dbg.Err($"{s_node.GetContext()}: Null element may not have ref, class, or mode specified; guessing wildly at intentions");
180✔
649
                }
180✔
650
                else if (refAttribute != null && (nullAttribute != null || classAttribute != null || modeAttribute != null))
1,562,264✔
651
                {
75✔
652
                    Dbg.Err($"{s_node.GetContext()}: Ref element may not have null, class, or mode specified; guessing wildly at intentions");
75✔
653
                }
75✔
654
                else if (classAttribute != null && (nullAttribute != null || refAttribute != null))
1,562,189✔
655
                {
×
656
                    Dbg.Err($"{s_node.GetContext()}: Class-specified element may not have null or ref specified; guessing wildly at intentions");
×
657
                }
×
658
                else if (modeAttribute != null && (nullAttribute != null || refAttribute != null))
1,562,189✔
659
                {
×
660
                    Dbg.Err($"{s_node.GetContext()}: Mode-specified element may not have null or ref specified; guessing wildly at intentions");
×
661
                }
×
662

663
                var unrecognized = s_node.GetMetadataUnrecognized();
1,562,444✔
664
                if (unrecognized != null)
1,562,444✔
665
                {
65✔
666
                    Dbg.Err($"{s_node.GetContext()}: Has unknown attributes {unrecognized}");
65✔
667
                }
65✔
668
            }
1,562,444✔
669

670
            // Doesn't mean anything outside recorderMode, so we check it for validity just in case
671
            string refKey;
672
            ReaderNode refKeyNode = null; // stored entirely for error reporting
1,560,749✔
673
            if (!globals.allowRefs)
1,560,749✔
674
            {
612,394✔
675
                refKey = null;
612,394✔
676
                foreach (var s_node in nodes)
3,065,360✔
677
                {
614,089✔
678
                    string nodeRefAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Ref);
614,089✔
679
                    if (nodeRefAttribute != null)
614,089✔
680
                    {
200✔
681
                        Dbg.Err($"{s_node.GetContext()}: Found a reference tag while not evaluating Recorder mode, ignoring it");
200✔
682
                    }
200✔
683
                }
614,089✔
684
            }
612,394✔
685
            else
686
            {
948,355✔
687
                (refKey, refKeyNode) = nodes.Select(node => (node.GetMetadata(ReaderNodeParseable.Metadata.Ref), node)).Where(anp => anp.Item1 != null).LastOrDefault();
2,845,065✔
688
            }
948,355✔
689

690
            // First figure out type. We actually need type to be set before we can properly analyze and validate the mode flags.
691
            // If we're in an asThis block, it refers to the outer item, not the inner item; just skip this entirely
692
            bool isNull = false;
1,560,749✔
693
            if (!asThis)
1,560,749✔
694
            {
1,560,214✔
695
                string classAttribute = null;
1,560,214✔
696
                ReaderNode classAttributeNode = null; // stored entirely for error reporting
1,560,214✔
697
                bool replaced = false;
1,560,214✔
698
                foreach (var s_node in nodes)
7,804,460✔
699
                {
1,561,909✔
700
                    // However, we do need to watch for Replace, because that means we should nuke the class attribute and start over.
701
                    string modeAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Mode);
1,561,909✔
702
                    ParseMode s_parseMode = ParseModeFromString(s_node.GetContext(), modeAttribute);
1,561,909✔
703
                    if (s_parseMode == ParseMode.Replace)
1,561,909✔
704
                    {
520✔
705
                        // we also should maybe be doing this if we're a list, map, or set?
706
                        classAttribute = null;
520✔
707
                        replaced = true;
520✔
708
                    }
520✔
709

710
                    // if we get nulled, we kill the class tag and basically treat it like a delete
711
                    // but we also reset the null tag on every entry
712
                    isNull = false;
1,561,909✔
713
                    string nullAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Null);
1,561,909✔
714
                    if (nullAttribute != null)
1,561,909✔
715
                    {
250,147✔
716
                        if (!bool.TryParse(nullAttribute, out bool nullValue))
250,147✔
717
                        {
×
718
                            Dbg.Err($"{s_node.GetContext()}: Invalid `null` attribute");
×
719
                        }
×
720
                        else if (nullValue)
250,147✔
721
                        {
250,107✔
722
                            isNull = true;
250,107✔
723
                        }
250,107✔
724
                    }
250,147✔
725

726
                    // update the class based on whatever this says
727
                    string localClassAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Class);
1,561,909✔
728
                    if (localClassAttribute != null)
1,561,909✔
729
                    {
208,121✔
730
                        classAttribute = localClassAttribute;
208,121✔
731
                        classAttributeNode = s_node;
208,121✔
732
                    }
208,121✔
733
                }
1,561,909✔
734

735
                if (classAttribute != null)
1,560,214✔
736
                {
208,121✔
737
                    var possibleType = (Type)ParseString(classAttribute, typeof(Type), null, classAttributeNode.GetContext());
208,121✔
738
                    if (!type.IsAssignableFrom(possibleType))
208,121✔
739
                    {
20✔
740
                        Dbg.Err($"{classAttributeNode.GetContext()}: Explicit type {classAttribute} cannot be assigned to expected type {type}");
20✔
741
                    }
20✔
742
                    else if (!replaced && result != null && result.GetType() != possibleType)
208,101✔
743
                    {
20✔
744
                        Dbg.Err($"{classAttributeNode.GetContext()}: Explicit type {classAttribute} does not match already-provided instance {type}");
20✔
745
                    }
20✔
746
                    else
747
                    {
208,081✔
748
                        type = possibleType;
208,081✔
749
                    }
208,081✔
750
                }
208,121✔
751
            }
1,560,214✔
752

753
            var converter = ConverterFor(type);
1,560,749✔
754

755
            // Now we traverse the Mode attributes as prep for our final parse pass.
756
            // ordersOverride makes `nodes` admittedly a little unnecessary.
757
            List<(ParseCommand command, ReaderNodeParseable node)> orders = ordersOverride ?? CompileOrders(type.CalculateSerializationModeCategory(converter, isRootDec), nodes);
1,560,749✔
758

759
            // Gather info
760
            bool hasChildren = false;
1,560,749✔
761
            ReaderNode hasChildrenNode = null;
1,560,749✔
762
            bool hasText = false;
1,560,749✔
763
            ReaderNode hasTextNode = null;
1,560,749✔
764
            foreach (var (_, node) in orders)
7,807,135✔
765
            {
1,562,444✔
766
                if (!hasChildren && node.HasChildren())
1,562,444✔
767
                {
480,801✔
768
                    hasChildren = true;
480,801✔
769
                    hasChildrenNode = node;
480,801✔
770
                }
480,801✔
771
                if (!hasText && node.GetText() != null)
1,562,444✔
772
                {
357,541✔
773
                    hasText = true;
357,541✔
774
                    hasTextNode = node;
357,541✔
775
                }
357,541✔
776
            }
1,562,444✔
777

778
            // Actually handle our attributes
779
            if (refKey != null)
1,560,749✔
780
            {
357,455✔
781
                // Ref is the highest priority, largely because I think it's cool
782

783
                // First we check if this is a valid Dec path ref; those don't require .Shared()
784
                if (Database.DecPathLookupReverse.TryGetValue(refKey, out var defPathRef))
357,455✔
785
                {
180✔
786
                    // check types
787
                    if (!type.IsAssignableFrom(defPathRef.GetType()))
180✔
788
                    {
×
789
                        Dbg.Err($"{refKeyNode.GetContext()}: Dec path reference object [{refKey}] is of type {defPathRef.GetType()}, which cannot be converted to expected type {type}");
×
790
                        return result;
×
791
                    }
792

793
                    // if it's a conflict, be unhappy
794
                    if (Database.DecPathLookupInvalid.Contains(refKey))
180✔
795
                    {
×
796
                        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");
×
797
                    }
×
798
                    else if (Database.DecPathLookupConflicts.Contains(refKey))
180✔
799
                    {
×
800
                        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");
×
801
                    }
×
802

803
                    // toot
804
                    return defPathRef;
180✔
805
                }
806

807
                if (recSettings.shared == Recorder.Settings.Shared.Deny)
357,275✔
808
                {
55✔
809
                    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✔
810
                }
55✔
811

812
                if (globals.refs == null)
357,275✔
813
                {
40✔
814
                    Dbg.Err($"{refKeyNode.GetContext()}: Found a reference object {refKey} before refs are initialized (is this being used in a ConverterFactory<>.Create()?)");
40✔
815
                    return result;
40✔
816
                }
817

818
                if (!globals.refs.ContainsKey(refKey))
357,235✔
819
                {
5✔
820
                    Dbg.Err($"{refKeyNode.GetContext()}: Found a reference object {refKey} without a valid reference mapping");
5✔
821
                    return result;
5✔
822
                }
823

824
                object refObject = globals.refs[refKey];
357,230✔
825
                if (refObject == null && !type.IsValueType)
357,230✔
826
                {
80✔
827
                    // okay, good enough
828
                    return refObject;
80✔
829
                }
830

831
                if (!type.IsAssignableFrom(refObject.GetType()))
357,150✔
832
                {
25✔
833
                    Dbg.Err($"{refKeyNode.GetContext()}: Reference object {refKey} is of type {refObject.GetType()}, which cannot be converted to expected type {type}");
25✔
834
                    return result;
25✔
835
                }
836

837
                return refObject;
357,125✔
838
            }
839
            else if (isNull)
1,203,294✔
840
            {
250,082✔
841
                return null;
250,082✔
842

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

848
            // Basic early validation
849

850
            if (hasChildren && hasText)
953,212✔
851
            {
15✔
852
                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✔
853

854
                // we'll just fall through and try to parse anyway, though
855
            }
15✔
856

857
            if (typeof(Dec).IsAssignableFrom(type) && hasChildren && !isRootDec)
953,212✔
858
            {
×
859
                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.");
×
860
                return null;
×
861
            }
862

863
            // Defer off to converters, whatever they feel like doing
864
            if (converter != null)
953,212✔
865
            {
605✔
866
                // string converter
867
                if (converter is ConverterString converterString)
605✔
868
                {
260✔
869
                    foreach (var (parseCommand, node) in orders)
1,300✔
870
                    {
260✔
871
                        switch (parseCommand)
260✔
872
                        {
873
                            case ParseCommand.Replace:
874
                                // easy, done
875
                                break;
260✔
876

877
                            default:
878
                                Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
879
                                break;
×
880
                        }
881

882
                        if (hasChildren)
260✔
883
                        {
15✔
884
                            Dbg.Err($"{node.GetContext()}: String converter {converter.GetType()} called with child XML nodes, which will be ignored");
15✔
885
                        }
15✔
886

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

889
                        // context might be null; that's OK at the moment
890
                        try
891
                        {
260✔
892
                            result = converterString.ReadObj(node.GetText() ?? "", node.GetContext());
260✔
893
                        }
175✔
894
                        catch (Exception e)
85✔
895
                        {
85✔
896
                            Dbg.Ex(new ConverterReadException(node.GetContext(), converter, e));
85✔
897

898
                            result = GenerateResultFallback(result, type);
85✔
899
                        }
85✔
900
                    }
260✔
901
                }
260✔
902
                else if (converter is ConverterRecord converterRecord)
345✔
903
                {
235✔
904
                    foreach (var (parseCommand, node) in orders)
1,175✔
905
                    {
235✔
906
                        switch (parseCommand)
235✔
907
                        {
908
                            case ParseCommand.Patch:
909
                                // easy, done
910
                                break;
235✔
911

912
                            case ParseCommand.Replace:
913
                                result = null;
×
914
                                break;
×
915

916
                            default:
917
                                Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
918
                                break;
×
919
                        }
920

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

923
                        if (result == null && !isNullable)
235✔
924
                        {
130✔
925
                            result = type.CreateInstanceSafe("converterrecord", node);
130✔
926
                        }
130✔
927

928
                        // context might be null; that's OK at the moment
929
                        if (result != null || isNullable)
235✔
930
                        {
235✔
931
                            var recorderReader = new RecorderReader(node, globals, trackUsage: true);
235✔
932
                            try
933
                            {
235✔
934
                                object returnedResult = converterRecord.RecordObj(result, recorderReader);
235✔
935

936
                                if (!type.IsValueType && result != returnedResult)
220✔
937
                                {
×
938
                                    Dbg.Err($"{node.GetContext()}: Converter {converterRecord.GetType()} changed object instance, this is disallowed");
×
939
                                }
×
940
                                else
941
                                {
220✔
942
                                    // for value types, this is fine
943
                                    result = returnedResult;
220✔
944
                                }
220✔
945

946
                                recorderReader.ReportUnusedFields();
220✔
947
                            }
220✔
948
                            catch (Exception e)
15✔
949
                            {
15✔
950
                                Dbg.Ex(new ConverterReadException(node.GetContext(), converter, e));
15✔
951

952
                                // no fallback needed, we already have a result
953
                            }
15✔
954
                        }
235✔
955
                    }
235✔
956
                }
235✔
957
                else if (converter is ConverterFactory converterFactory)
110✔
958
                {
110✔
959
                    foreach (var (parseCommand, node) in orders)
550✔
960
                    {
110✔
961
                        switch (parseCommand)
110✔
962
                        {
963
                            case ParseCommand.Patch:
964
                                // easy, done
965
                                break;
110✔
966

967
                            case ParseCommand.Replace:
968
                                result = null;
×
969
                                break;
×
970

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

976
                        var recorderReader = new RecorderReader(node, globals, disallowShared: true, trackUsage: true);
110✔
977
                        if (result == null)
110✔
978
                        {
105✔
979
                            try
980
                            {
105✔
981
                                result = converterFactory.CreateObj(recorderReader);
105✔
982
                            }
90✔
983
                            catch (Exception e)
15✔
984
                            {
15✔
985
                                Dbg.Ex(new ConverterReadException(node.GetContext(), converter, e));
15✔
986
                            }
15✔
987
                        }
105✔
988

989
                        // context might be null; that's OK at the moment
990
                        if (result != null)
110✔
991
                        {
95✔
992
                            recorderReader.AllowShared(globals);
95✔
993
                            try
994
                            {
95✔
995
                                result = converterFactory.ReadObj(result, recorderReader);
95✔
996
                                recorderReader.ReportUnusedFields();
80✔
997
                            }
80✔
998
                            catch (Exception e)
15✔
999
                            {
15✔
1000
                                Dbg.Ex(new ConverterReadException(node.GetContext(), converter, e));
15✔
1001

1002
                                // no fallback needed, we already have a result
1003
                            }
15✔
1004
                        }
95✔
1005
                    }
110✔
1006
                }
110✔
1007
                else
1008
                {
×
1009
                    Dbg.Err($"Somehow ended up with an unsupported converter {converter.GetType()}");
×
1010
                }
×
1011

1012
                return result;
605✔
1013
            }
1014

1015
            // All our standard text-using options
1016
            // Placed before IRecordable just in case we have a Dec that is IRecordable
1017
            if ((typeof(Dec).IsAssignableFrom(type) && !isRootDec) ||
952,607✔
1018
                    type == typeof(Type) ||
952,607✔
1019
                    type == typeof(string) ||
952,607✔
1020
                    type.IsPrimitive ||
952,607✔
1021
                    (TypeDescriptor.GetConverter(type)?.CanConvertFrom(typeof(string)) ?? false)   // this is last because it's slow
952,607✔
1022
                )
952,607✔
1023
            {
397,776✔
1024
                foreach (var (parseCommand, node) in orders)
1,988,880✔
1025
                {
397,776✔
1026
                    switch (parseCommand)
397,776✔
1027
                    {
1028
                        case ParseCommand.Replace:
1029
                            // easy, done
1030
                            break;
397,776✔
1031

1032
                        default:
1033
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1034
                            break;
×
1035
                    }
1036

1037
                    if (hasChildren)
397,776✔
1038
                    {
20✔
1039
                        Dbg.Err($"{node.GetContext()}: Child nodes are not valid when parsing {type}");
20✔
1040
                    }
20✔
1041

1042
                    result = ParseString(node.GetText(), type, result, node.GetContext());
397,776✔
1043
                }
397,776✔
1044

1045
                return result;
397,776✔
1046
            }
1047

1048
            // Special case: IRecordables
1049
            IRecordable recordableBuffered = null;
554,831✔
1050
            if (typeof(IRecordable).IsAssignableFrom(type))
554,831✔
1051
            {
391,305✔
1052
                // we're going to need to make one anyway so let's just go ahead and do that
1053
                IRecordable recordable = null;
391,305✔
1054

1055
                if (result != null)
391,305✔
1056
                {
206,775✔
1057
                    recordable = (IRecordable)result;
206,775✔
1058
                }
206,775✔
1059
                else if (recSettings.factories == null)
184,530✔
1060
                {
183,500✔
1061
                    recordable = (IRecordable)type.CreateInstanceSafe("recordable", orders[0].node);
183,500✔
1062
                }
183,500✔
1063
                else
1064
                {
1,030✔
1065
                    recordable = recSettings.CreateRecordableFromFactory(type, "recordable", orders[0].node);
1,030✔
1066
                }
1,030✔
1067

1068
                // we hold on to this so that, *if* we end up not using this object, we can optionally reuse it later for reflection
1069
                // 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
1070
                recordableBuffered = recordable;
391,305✔
1071

1072
                var conditionalRecordable = recordable as IConditionalRecordable;
391,305✔
1073
                if (conditionalRecordable == null || conditionalRecordable.ShouldRecord(nodes[0].UserSettings))
391,305✔
1074
                {
391,290✔
1075
                    foreach (var (parseCommand, node) in orders)
1,956,450✔
1076
                    {
391,290✔
1077
                        switch (parseCommand)
391,290✔
1078
                        {
1079
                            case ParseCommand.Patch:
1080
                                // easy, done
1081
                                break;
391,290✔
1082

1083
                            default:
1084
                                Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1085
                                break;
×
1086
                        }
1087

1088
                        if (recordable != null)
391,290✔
1089
                        {
391,270✔
1090
                            var recorderReader = new RecorderReader(node, globals, trackUsage: true);
391,270✔
1091
                            recordable.Record(recorderReader);
391,270✔
1092
                            recorderReader.ReportUnusedFields();
391,270✔
1093

1094
                            // TODO: support indices if this is within the Dec system?
1095
                        }
391,270✔
1096
                    }
391,290✔
1097

1098
                    result = recordable;
391,290✔
1099
                    return result;
391,290✔
1100
                }
1101

1102
                // otherwise we just fall through
1103
            }
15✔
1104

1105
            // Nothing past this point even supports text, so let's just get angry and break stuff.
1106
            if (hasText)
163,541✔
1107
            {
80✔
1108
                Dbg.Err($"{hasTextNode.GetContext()}: Text detected in a situation where it is invalid; will be ignored");
80✔
1109
                return result;
80✔
1110
            }
1111

1112
            // Special case: Lists
1113
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
163,461✔
1114
            {
46,450✔
1115
                foreach (var (parseCommand, node) in orders)
232,250✔
1116
                {
46,450✔
1117
                    switch (parseCommand)
46,450✔
1118
                    {
1119
                        case ParseCommand.Replace:
1120
                            // 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.
1121
                            // 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.
1122
                            // 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.
1123
                            if (result != null)
46,370✔
1124
                            {
420✔
1125
                                ((IList)result).Clear();
420✔
1126
                            }
420✔
1127
                            break;
46,370✔
1128

1129
                        case ParseCommand.Append:
1130
                            // we're good
1131
                            break;
80✔
1132

1133
                        default:
1134
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1135
                            break;
×
1136
                    }
1137

1138
                    // List<> handling
1139
                    Type referencedType = type.GetGenericArguments()[0];
46,450✔
1140

1141
                    var list = (IList)(result ?? Activator.CreateInstance(type));
46,450✔
1142

1143
                    node.ParseList(list, referencedType, globals, recSettings);
46,450✔
1144

1145
                    result = list;
46,450✔
1146
                }
46,450✔
1147

1148
                return result;
46,450✔
1149
            }
1150

1151
            // Special case: Arrays
1152
            if (type.IsArray)
117,011✔
1153
            {
2,985✔
1154
                Type referencedType = type.GetElementType();
2,985✔
1155

1156
                foreach (var (parseCommand, node) in orders)
14,925✔
1157
                {
2,985✔
1158
                    Array array = null;
2,985✔
1159
                    int startOffset = 0;
2,985✔
1160

1161
                    // 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.
1162
                    switch (parseCommand)
2,985✔
1163
                    {
1164
                        case ParseCommand.Replace:
1165
                        {
2,950✔
1166
                            // This is a full override, so we're going to create it here.
1167
                            // It is actually vitally important that we fall back on the model when possible, because the Recorder Ref system requires it.
1168
                            bool match = result != null && result.GetType() == type;
2,950✔
1169
                            var arrayDimensions = node.GetArrayDimensions(type.GetArrayRank());
2,950✔
1170
                            if (match)
2,950✔
1171
                            {
410✔
1172
                                array = (Array)result;
410✔
1173
                                if (array.Rank != type.GetArrayRank())
410✔
1174
                                {
×
1175
                                    match = false;
×
1176
                                }
×
1177
                                else
1178
                                {
410✔
1179
                                    for (int i = 0; i < array.Rank; i++)
1,040✔
1180
                                    {
420✔
1181
                                        if (array.GetLength(i) != arrayDimensions[i])
420✔
1182
                                        {
310✔
1183
                                            match = false;
310✔
1184
                                            break;
310✔
1185
                                        }
1186
                                    }
110✔
1187
                                }
410✔
1188
                            }
410✔
1189

1190
                            if (!match)
2,950✔
1191
                            {
2,850✔
1192
                                // Otherwise just make a new one, no harm done.
1193
                                array = Array.CreateInstance(referencedType, arrayDimensions);
2,850✔
1194
                            }
2,850✔
1195

1196
                            break;
2,950✔
1197
                        }
1198

1199
                        case ParseCommand.Append:
1200
                        {
55✔
1201
                            if (result == null)
55✔
1202
                            {
20✔
1203
                                goto case ParseCommand.Replace;
20✔
1204
                            }
1205

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

1236
                                {
15✔
1237
                                    int[] indices = new int[arrayDimensions.Length];
15✔
1238
                                    CopyArray(oldArray, array, indices, 0);
15✔
1239
                                }
15✔
1240
                            }
15✔
1241

1242
                            break;
35✔
1243
                        }
1244

1245
                        default:
1246
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1247
                            array = null; // just to break the unassigned-local-variable
×
1248
                            break;
×
1249
                    }
1250

1251
                    node.ParseArray(array, referencedType, globals, recSettings, startOffset);
2,985✔
1252

1253
                    result = array;
2,985✔
1254
                }
2,985✔
1255

1256
                return result;
2,985✔
1257
            }
1258

1259
            // Special case: Dictionaries
1260
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
114,026✔
1261
            {
23,430✔
1262
                foreach (var (parseCommand, node) in orders)
117,150✔
1263
                {
23,430✔
1264
                    bool permitPatch = false;
23,430✔
1265
                    switch (parseCommand)
23,430✔
1266
                    {
1267
                        case ParseCommand.Replace:
1268
                            // 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.
1269
                            // 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.
1270
                            // 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.
1271
                            if (result != null)
23,190✔
1272
                            {
485✔
1273
                                ((IDictionary)result).Clear();
485✔
1274
                            }
485✔
1275
                            break;
23,190✔
1276

1277
                        case ParseCommand.Patch:
1278
                            if (original != null)
160✔
1279
                            {
140✔
1280
                                permitPatch = true;
140✔
1281
                            }
140✔
1282
                            break;
160✔
1283

1284
                        case ParseCommand.Append:
1285
                            // nothing needs to be done, our existing dupe checking will solve it
1286
                            break;
80✔
1287

1288
                        default:
1289
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1290
                            break;
×
1291
                    }
1292

1293
                    // Dictionary<> handling
1294
                    Type keyType = type.GetGenericArguments()[0];
23,430✔
1295
                    Type valueType = type.GetGenericArguments()[1];
23,430✔
1296

1297
                    var dict = (IDictionary)(result ?? Activator.CreateInstance(type));
23,430✔
1298

1299
                    node.ParseDictionary(dict, keyType, valueType, globals, recSettings, permitPatch);
23,430✔
1300

1301
                    result = dict;
23,430✔
1302
                }
23,430✔
1303

1304
                return result;
23,430✔
1305
            }
1306

1307
            // Special case: HashSet
1308
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(HashSet<>))
90,596✔
1309
            {
1,010✔
1310
                foreach (var (parseCommand, node) in orders)
5,050✔
1311
                {
1,010✔
1312
                    bool permitPatch = false;
1,010✔
1313
                    switch (parseCommand)
1,010✔
1314
                    {
1315
                        case ParseCommand.Replace:
1316
                            // 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.
1317
                            // 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.
1318
                            // 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.
1319
                            if (result != null)
850✔
1320
                            {
450✔
1321
                                // Did you know there's no non-generic interface that HashSet<> supports that includes a Clear function?
1322
                                // Fun fact:
1323
                                // That thing I just wrote!
1324
                                var clearFunction = result.GetType().GetMethod("Clear");
450✔
1325
                                clearFunction.Invoke(result, null);
450✔
1326
                            }
450✔
1327
                            break;
850✔
1328

1329
                        case ParseCommand.Patch:
1330
                            if (original != null)
80✔
1331
                            {
60✔
1332
                                permitPatch = true;
60✔
1333
                            }
60✔
1334
                            break;
80✔
1335

1336
                        case ParseCommand.Append:
1337
                            // nothing needs to be done, our existing dupe checking will solve it
1338
                            break;
80✔
1339

1340
                        default:
1341
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1342
                            break;
×
1343
                    }
1344

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

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

1349
                    node.ParseHashset(set, keyType, globals, recSettings, permitPatch);
1,010✔
1350

1351
                    result = set;
1,010✔
1352
                }
1,010✔
1353

1354
                return result;
1,010✔
1355
            }
1356

1357
            // Special case: Stack
1358
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Stack<>))
89,586✔
1359
            {
55✔
1360
                // Stack<> handling
1361
                // Again, no sensible non-generic interface to use, so we're stuck with reflection
1362

1363
                foreach (var (parseCommand, node) in orders)
275✔
1364
                {
55✔
1365
                    switch (parseCommand)
55✔
1366
                    {
1367
                        case ParseCommand.Replace:
1368
                            // 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.
1369
                            // 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.
1370
                            // 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.
1371
                            if (result != null)
55✔
1372
                            {
5✔
1373
                                var clearFunction = result.GetType().GetMethod("Clear");
5✔
1374
                                clearFunction.Invoke(result, null);
5✔
1375
                            }
5✔
1376
                            break;
55✔
1377

1378
                        case ParseCommand.Append:
1379
                            break;
×
1380

1381
                        // There definitely starts being an argument for prepend.
1382

1383
                        default:
1384
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1385
                            break;
×
1386
                    }
1387

1388
                    Type keyType = type.GetGenericArguments()[0];
55✔
1389

1390
                    var set = result ?? Activator.CreateInstance(type);
55✔
1391

1392
                    node.ParseStack(set, keyType, globals, recSettings);
55✔
1393

1394
                    result = set;
55✔
1395
                }
55✔
1396

1397
                return result;
55✔
1398
            }
1399

1400
            // Special case: Queue
1401
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Queue<>))
89,531✔
1402
            {
55✔
1403
                // Queue<> handling
1404
                // Again, no sensible non-generic interface to use, so we're stuck with reflection
1405

1406
                foreach (var (parseCommand, node) in orders)
275✔
1407
                {
55✔
1408
                    switch (parseCommand)
55✔
1409
                    {
1410
                        case ParseCommand.Replace:
1411
                            // 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.
1412
                            // 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.
1413
                            // 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.
1414
                            if (result != null)
55✔
1415
                            {
5✔
1416
                                var clearFunction = result.GetType().GetMethod("Clear");
5✔
1417
                                clearFunction.Invoke(result, null);
5✔
1418
                            }
5✔
1419
                            break;
55✔
1420

1421
                        case ParseCommand.Append:
1422
                            break;
×
1423

1424
                        // There definitely starts being an argument for prepend.
1425

1426
                        default:
1427
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1428
                            break;
×
1429
                    }
1430

1431
                    Type keyType = type.GetGenericArguments()[0];
55✔
1432

1433
                    var set = result ?? Activator.CreateInstance(type);
55✔
1434

1435
                    node.ParseQueue(set, keyType, globals, recSettings);
55✔
1436

1437
                    result = set;
55✔
1438
                }
55✔
1439

1440
                return result;
55✔
1441
            }
1442

1443
            // Special case: A bucket of tuples
1444
            // These are all basically identical, but AFAIK there's no good way to test them all in a better way.
1445
            if (type.IsGenericType && (
89,476✔
1446
                    type.GetGenericTypeDefinition() == typeof(Tuple<>) ||
89,476✔
1447
                    type.GetGenericTypeDefinition() == typeof(Tuple<,>) ||
89,476✔
1448
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,>) ||
89,476✔
1449
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,>) ||
89,476✔
1450
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,>) ||
89,476✔
1451
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,>) ||
89,476✔
1452
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,>) ||
89,476✔
1453
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,,>) ||
89,476✔
1454
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<>) ||
89,476✔
1455
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,>) ||
89,476✔
1456
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,>) ||
89,476✔
1457
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,>) ||
89,476✔
1458
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,>) ||
89,476✔
1459
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,>) ||
89,476✔
1460
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,>) ||
89,476✔
1461
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,,>)
89,476✔
1462
                    ))
89,476✔
1463
            {
1,070✔
1464
                foreach (var (parseCommand, node) in orders)
5,350✔
1465
                {
1,070✔
1466
                    switch (parseCommand)
1,070✔
1467
                    {
1468
                        case ParseCommand.Replace:
1469
                            // easy, done
1470
                            break;
1,070✔
1471

1472
                        default:
1473
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1474
                            break;
×
1475
                    }
1476

1477
                    int expectedCount = type.GenericTypeArguments.Length;
1,070✔
1478
                    object[] parameters = new object[expectedCount];
1,070✔
1479

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

1482
                    // construct!
1483
                    result = Activator.CreateInstance(type, parameters);
1,070✔
1484
                }
1,070✔
1485

1486
                return result;
1,070✔
1487
            }
1488

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

1491
            // If we have refs, something has gone wrong; we should never be doing reflection inside a Record system.
1492
            // This is a really ad-hoc way of testing this and should be fixed.
1493
            // 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.
1494
            // I'm less OK with security vulnerabilities in save files. Nobody expects a savefile can compromise their system.
1495
            // And the full reflection system is probably impossible to secure, whereas the Record system should be secureable.
1496
            if (!globals.allowReflection)
88,406✔
1497
            {
40✔
1498
                // just pick the first node to get something to go on
1499
                Dbg.Err($"{orders[0].node.GetContext()}: Falling back to reflection within a Record system while parsing a {type}; this is currently not allowed for security reasons. Either you shouldn't be trying to serialize this, or it should implement Dec.IRecorder (https://zorbathut.github.io/dec/release/documentation/serialization.html), or you need a Dec.Converter (https://zorbathut.github.io/dec/release/documentation/custom.html)");
40✔
1500
                return result;
40✔
1501
            }
1502

1503
            foreach (var (parseCommand, node) in orders)
445,140✔
1504
            {
90,061✔
1505
                if (!isRootDec)
90,061✔
1506
                {
44,021✔
1507
                    switch (parseCommand)
44,021✔
1508
                    {
1509
                        case ParseCommand.Patch:
1510
                            // easy, done
1511
                            break;
44,021✔
1512

1513
                        default:
1514
                            Dbg.Err($"{node.GetContext()}: Internal error, got invalid mode {parseCommand}");
×
1515
                            break;
×
1516
                    }
1517
                }
44,021✔
1518
                else
1519
                {
46,040✔
1520
                    if (parseCommand != ParseCommand.Patch)
46,040✔
1521
                    {
×
1522
                        Dbg.Err($"{node.GetContext()}: Mode provided for root Dec; this is currently not supported in any form");
×
1523
                    }
×
1524
                }
46,040✔
1525

1526
                // If we haven't been given a generic class from our parent, go ahead and init to defaults
1527
                if (result == null && recordableBuffered != null)
90,061✔
1528
                {
15✔
1529
                    result = recordableBuffered;
15✔
1530
                }
15✔
1531

1532
                if (result == null)
90,061✔
1533
                {
17,640✔
1534
                    // okay fine
1535
                    result = type.CreateInstanceSafe("object", node);
17,640✔
1536

1537
                    if (result == null)
17,640✔
1538
                    {
80✔
1539
                        // error already reported
1540
                        return result;
80✔
1541
                    }
1542
                }
17,560✔
1543

1544
                node.ParseReflection(result, globals, recSettings);
89,981✔
1545
            }
89,981✔
1546

1547
            // Set up our index fields; this has to happen last in case we're a struct
1548
            Index.Register(ref result);
88,286✔
1549

1550
            return result;
88,286✔
1551
        }
1,560,749✔
1552

1553
        internal static object ParseString(string text, Type type, object original, Context context)
1554
        {
814,782✔
1555
            // Special case: Converter override
1556
            // This is redundant if we're being called from ParseElement, but we aren't always.
1557
            if (ConverterFor(type) is Converter converter)
814,782✔
1558
            {
45✔
1559
                object result = original;
45✔
1560

1561
                try
1562
                {
45✔
1563
                    // string converter
1564
                    if (converter is ConverterString converterString)
45✔
1565
                    {
45✔
1566
                        // context might be null; that's OK at the moment
1567
                        try
1568
                        {
45✔
1569
                            result = converterString.ReadObj(text, context);
45✔
1570
                        }
45✔
1571
                        catch (Exception e)
×
1572
                        {
×
1573
                            Dbg.Ex(new ConverterReadException(context, converter, e));
×
1574

1575
                            result = GenerateResultFallback(result, type);
×
1576
                        }
×
1577
                    }
45✔
1578
                    else if (converter is ConverterRecord converterRecord)
×
1579
                    {
×
1580
                        // string parsing really doesn't apply here, we can't get a full Recorder context out anymore
1581
                        // in theory this could be done with RecordAsThis() but I'm just going to skip it for now
1582
                        Dbg.Err($"{context}: Attempt to string-parse with a ConverterRecord, this is currently not supported, contact developers if you need this feature");
×
1583
                    }
×
1584
                    else if (converter is ConverterFactory converterFactory)
×
1585
                    {
×
1586
                        // string parsing really doesn't apply here, we can't get a full Recorder context out anymore
1587
                        // in theory this could be done with RecordAsThis() but I'm just going to skip it for now
1588
                        Dbg.Err($"{context}: Attempt to string-parse with a ConverterFactory, this is currently not supported, contact developers if you need this feature");
×
1589
                    }
×
1590
                    else
1591
                    {
×
1592
                        Dbg.Err($"Somehow ended up with an unsupported converter {converter.GetType()}");
×
1593
                    }
×
1594
                }
45✔
1595
                catch (Exception e)
×
1596
                {
×
1597
                    Dbg.Ex(e);
×
1598
                }
×
1599

1600
                return result;
45✔
1601
            }
1602

1603
            // Special case: decs
1604
            if (typeof(Dec).IsAssignableFrom(type))
814,737✔
1605
            {
48,285✔
1606
                if (text == "" || text == null)
48,285✔
1607
                {
40,695✔
1608
                    // you reference nothing, you get the null (even if this isn't a specified type; null is null, after all)
1609
                    return null;
40,695✔
1610
                }
1611
                else
1612
                {
7,590✔
1613
                    if (type.GetDecRootType() == null)
7,590✔
1614
                    {
80✔
1615
                        Dbg.Err($"{context}: Non-hierarchy decs cannot be used as references");
80✔
1616
                        return null;
80✔
1617
                    }
1618

1619
                    Dec result = Database.Get(type, text);
7,510✔
1620
                    if (result == null)
7,510✔
1621
                    {
85✔
1622
                        if (UtilMisc.ValidateDecName(text, context))
85✔
1623
                        {
85✔
1624
                            Dbg.Err($"{context}: Couldn't find {type} named `{text}`");
85✔
1625
                        }
85✔
1626

1627
                        // If we're an invalid name, we already spat out the error
1628
                    }
85✔
1629
                    return result;
7,510✔
1630
                }
1631
            }
1632

1633
            // Special case: types
1634
            if (type == typeof(Type))
766,452✔
1635
            {
415,356✔
1636
                if (text == "")
415,356✔
1637
                {
×
1638
                    return null;
×
1639
                }
1640

1641
                return UtilType.ParseDecFormatted(text, context);
415,356✔
1642
            }
1643

1644
            // Various non-composite-type special-cases
1645
            if (text != "")
351,096✔
1646
            {
351,096✔
1647
                // If we've got text, treat us as an object of appropriate type
1648
                try
1649
                {
351,096✔
1650
                    if (type == typeof(float))
351,096✔
1651
                    {
28,250✔
1652
                        // first check the various strings, case-insensitive
1653
                        if (String.Compare(text, "nan", true) == 0)
28,250✔
1654
                        {
90✔
1655
                            return float.NaN;
90✔
1656
                        }
1657

1658
                        if (String.Compare(text, "infinity", true) == 0)
28,160✔
1659
                        {
40✔
1660
                            return float.PositiveInfinity;
40✔
1661
                        }
1662

1663
                        if (String.Compare(text, "-infinity", true) == 0)
28,120✔
1664
                        {
40✔
1665
                            return float.NegativeInfinity;
40✔
1666
                        }
1667

1668
                        if (text.StartsWith("nanbox", StringComparison.CurrentCultureIgnoreCase))
28,080✔
1669
                        {
85✔
1670
                            const int expectedFloatSize = 6 + 8;
1671

1672
                            if (type == typeof(float) && text.Length != expectedFloatSize)
85✔
1673
                            {
×
1674
                                Dbg.Err($"{context}: Found nanboxed value without the expected number of characters, expected {expectedFloatSize} but got {text.Length}");
×
1675
                                return float.NaN;
×
1676
                            }
1677

1678
                            int number = Convert.ToInt32(text.Substring(6), 16);
85✔
1679
                            return BitConverter.Int32BitsToSingle(number);
85✔
1680
                        }
1681
                    }
27,995✔
1682

1683
                    if (type == typeof(double))
350,841✔
1684
                    {
2,840✔
1685
                        // first check the various strings, case-insensitive
1686
                        if (String.Compare(text, "nan", true) == 0)
2,840✔
1687
                        {
1,815✔
1688
                            return double.NaN;
1,815✔
1689
                        }
1690

1691
                        if (String.Compare(text, "infinity", true) == 0)
1,025✔
1692
                        {
40✔
1693
                            return double.PositiveInfinity;
40✔
1694
                        }
1695

1696
                        if (String.Compare(text, "-infinity", true) == 0)
985✔
1697
                        {
40✔
1698
                            return double.NegativeInfinity;
40✔
1699
                        }
1700

1701
                        if (text.StartsWith("nanbox", StringComparison.CurrentCultureIgnoreCase))
945✔
1702
                        {
75✔
1703
                            const int expectedDoubleSize = 6 + 16;
1704

1705
                            if (type == typeof(double) && text.Length != expectedDoubleSize)
75✔
1706
                            {
×
1707
                                Dbg.Err($"{context}: Found nanboxed value without the expected number of characters, expected {expectedDoubleSize} but got {text.Length}");
×
1708
                                return double.NaN;
×
1709
                            }
1710

1711
                            long number = Convert.ToInt64(text.Substring(6), 16);
75✔
1712
                            return BitConverter.Int64BitsToDouble(number);
75✔
1713
                        }
1714
                    }
870✔
1715

1716
                    return TypeDescriptor.GetConverter(type).ConvertFromString(text);
348,871✔
1717
                }
1718
                catch (System.Exception e)  // I would normally not catch System.Exception, but TypeConverter is wrapping FormatException in an Exception for some reason
180✔
1719
                {
180✔
1720
                    Dbg.Err($"{context}: {e.ToString()}");
180✔
1721
                    return original;
180✔
1722
                }
1723
            }
1724
            else if (type == typeof(string))
×
1725
            {
×
1726
                // If we don't have text, and we're a string, return ""
1727
                return "";
×
1728
            }
1729
            else
1730
            {
×
1731
                // If we don't have text, and we've fallen down to this point, that's an error (and return original value I guess)
1732
                Dbg.Err($"{context}: Empty field provided for type {type}");
×
1733
                return original;
×
1734
            }
1735
        }
814,782✔
1736

1737
        internal static Type TypeSystemRuntimeType = Type.GetType("System.RuntimeType");
10✔
1738
        internal static void ComposeElement(WriterNode node, object value, Type fieldType, FieldInfo fieldInfo = null, bool isRootDec = false, bool asThis = false)
1739
        {
1,461,469✔
1740
            // Verify our Shared flags as the *very* first step to ensure nothing gets past us.
1741
            // 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
1742
            bool canBeShared = fieldType.CanBeShared();
1,461,469✔
1743
            if (node.RecorderSettings.shared == Recorder.Settings.Shared.Allow && !asThis)
1,461,469✔
1744
            {
801,820✔
1745
                // 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.
1746
                if (!canBeShared)
801,820✔
1747
                {
120✔
1748
                    // If shared, make sure our type is appropriate for sharing
1749
                    // this really needs the recorder name and the field name too
1750
                    Dbg.Wrn($"Value type `{fieldType}` tagged as Shared in recorder, this is meaningless but harmless");
120✔
1751
                }
120✔
1752
            }
801,820✔
1753

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

1763
                var rootType = value.GetType().GetDecRootType();
3,870✔
1764
                if (!rootType.IsAssignableFrom(fieldType))
3,870✔
1765
                {
30✔
1766
                    // The user has a Dec.Dec or similar, and it has a Dec assigned to it.
1767
                    // 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.
1768
                    // But we're OK with that, honestly. We just do that.
1769
                    // If you're saving something like this you don't get to rename Dec classes later on, but, hey, deal with it.
1770
                    // 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.
1771
                    node.TagClass(rootType);
30✔
1772
                }
30✔
1773

1774
                node.WriteDec(value as Dec);
3,870✔
1775

1776
                return;
3,870✔
1777
            }
1778

1779
            // Everything represents "null" with an explicit XML tag, so let's just do that
1780
            // Maybe at some point we want to special-case this for the empty Dec link
1781
            if (value == null)
1,457,599✔
1782
            {
366,872✔
1783
                if (typeof(Dec).IsAssignableFrom(fieldType))
366,872✔
1784
                {
33,950✔
1785
                    node.WriteDec(null);
33,950✔
1786
                }
33,950✔
1787
                else
1788
                {
332,922✔
1789
                    node.WriteExplicitNull();
332,922✔
1790
                }
332,922✔
1791

1792
                return;
366,872✔
1793
            }
1794

1795

1796
            if (node.AllowDecPath)
1,090,727✔
1797
            {
744,340✔
1798
                // Try to snag a Dec path
1799
                var decPath = Database.DecPathLookup.TryGetValue(value);
744,340✔
1800

1801
                if (decPath != null)
744,340✔
1802
                {
225✔
1803
                    node.WriteDecPathRef(value);
225✔
1804
                    return;
225✔
1805
                }
1806
            }
744,115✔
1807

1808
            var valType = value.GetType();
1,090,502✔
1809

1810
            // This is our value's type, but we may need a little bit of tinkering to make it useful.
1811
            // The current case I know of is System.RuntimeType, which appears if we call .GetType() on a Type.
1812
            // I assume there is a complicated internal reason for this; good news, we can ignore it and just pretend it's a System.Type.
1813
            // Bad news: it's actually really hard to detect this case because System.RuntimeType is private.
1814
            // That's why we have the annoying `static` up above.
1815
            if (valType == TypeSystemRuntimeType)
1,090,502✔
1816
            {
255✔
1817
                valType = typeof(Type);
255✔
1818
            }
255✔
1819

1820
            // Do all our unreferencables first
1821
            bool unreferenceableComplete = false;
1,090,502✔
1822

1823
            if (valType.IsPrimitive)
1,090,502✔
1824
            {
249,581✔
1825
                node.WritePrimitive(value);
249,581✔
1826

1827
                unreferenceableComplete = true;
249,581✔
1828
            }
249,581✔
1829
            else if (value is System.Enum)
840,921✔
1830
            {
305✔
1831
                node.WriteEnum(value);
305✔
1832

1833
                unreferenceableComplete = true;
305✔
1834
            }
305✔
1835
            else if (value is string)
840,616✔
1836
            {
27,675✔
1837
                node.WriteString(value as string);
27,675✔
1838

1839
                unreferenceableComplete = true;
27,675✔
1840
            }
27,675✔
1841
            else if (value is Type)
812,941✔
1842
            {
255✔
1843
                node.WriteType(value as Type);
255✔
1844

1845
                unreferenceableComplete = true;
255✔
1846
            }
255✔
1847

1848
            // Check to see if we should make this into a ref (yes, even if we're not tagged as Shared)
1849
            // Do this *before* we do the class tagging, otherwise we may add ref/class tags to a single node, which is invalid.
1850
            // 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.
1851
            if (Util.CanBeShared(valType) && !asThis)
1,090,502✔
1852
            {
783,381✔
1853
                if (node.WriteReference(value, node.Path))
783,381✔
1854
                {
150,860✔
1855
                    // The ref system has set up the appropriate tagging, so we're done!
1856
                    return;
150,860✔
1857
                }
1858

1859
                // If we support references, then this object has not previously shown up in the reference system; keep going so we finish serializing it.
1860
                // If we don't support references at all then obviously we *really* need to finish serializing it.
1861
            }
632,521✔
1862

1863
            // 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`.
1864
            if (valType != fieldType)
939,642✔
1865
            {
1,396✔
1866
                if (asThis)
1,396✔
1867
                {
20✔
1868
                    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✔
1869
                    // . . . I guess we just keep going?
1870
                }
20✔
1871
                else
1872
                {
1,376✔
1873
                    node.TagClass(valType);
1,376✔
1874
                }
1,376✔
1875
            }
1,396✔
1876

1877
            // Did we actually write our node type? Alright, we're done.
1878
            if (unreferenceableComplete)
939,642✔
1879
            {
277,816✔
1880
                return;
277,816✔
1881
            }
1882

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

1885
            if (node.AllowCloning && UtilType.CanBeCloneCopied(valType))
661,826✔
1886
            {
20✔
1887
                node.WriteCloneCopy(value);
20✔
1888

1889
                return;
20✔
1890
            }
1891

1892
            if (valType.IsArray)
661,806✔
1893
            {
2,650✔
1894
                node.WriteArray(value as Array);
2,650✔
1895

1896
                return;
2,650✔
1897
            }
1898

1899
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(List<>))
659,156✔
1900
            {
26,595✔
1901
                node.WriteList(value as IList);
26,595✔
1902

1903
                return;
26,595✔
1904
            }
1905

1906
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
632,561✔
1907
            {
11,830✔
1908
                node.WriteDictionary(value as IDictionary);
11,830✔
1909

1910
                return;
11,830✔
1911
            }
1912

1913
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(HashSet<>))
620,731✔
1914
            {
600✔
1915
                node.WriteHashSet(value as IEnumerable);
600✔
1916

1917
                return;
600✔
1918
            }
1919

1920
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(Queue<>))
620,131✔
1921
            {
50✔
1922
                node.WriteQueue(value as IEnumerable);
50✔
1923

1924
                return;
50✔
1925
            }
1926

1927
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(Stack<>))
620,081✔
1928
            {
50✔
1929
                node.WriteStack(value as IEnumerable);
50✔
1930

1931
                return;
50✔
1932
            }
1933

1934
            if (valType.IsGenericType && (
620,031✔
1935
                    valType.GetGenericTypeDefinition() == typeof(Tuple<>) ||
620,031✔
1936
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,>) ||
620,031✔
1937
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,>) ||
620,031✔
1938
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,>) ||
620,031✔
1939
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,>) ||
620,031✔
1940
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,,>) ||
620,031✔
1941
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,>) ||
620,031✔
1942
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,,>)
620,031✔
1943
                ))
620,031✔
1944
            {
215✔
1945
                node.WriteTuple(value, fieldInfo?.GetCustomAttribute<System.Runtime.CompilerServices.TupleElementNamesAttribute>());
215✔
1946

1947
                return;
215✔
1948
            }
1949

1950
            if (valType.IsGenericType && (
619,816✔
1951
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<>) ||
619,816✔
1952
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,>) ||
619,816✔
1953
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,>) ||
619,816✔
1954
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,>) ||
619,816✔
1955
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,>) ||
619,816✔
1956
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,>) ||
619,816✔
1957
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,>) ||
619,816✔
1958
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,,>)
619,816✔
1959
                ))
619,816✔
1960
            {
390✔
1961
                node.WriteValueTuple(value, fieldInfo?.GetCustomAttribute<System.Runtime.CompilerServices.TupleElementNamesAttribute>());
390✔
1962

1963
                return;
390✔
1964
            }
1965

1966
            if (value is IRecordable
619,426✔
1967
                && (!(value is IConditionalRecordable) || (value as IConditionalRecordable).ShouldRecord(node.UserSettings)))
619,426✔
1968
            {
563,300✔
1969
                node.WriteRecord(value as IRecordable);
563,300✔
1970

1971
                return;
563,300✔
1972
            }
1973

1974
            {
56,126✔
1975
                // Look for a converter; that's the only way to handle this before we fall back to reflection
1976
                var converter = Serialization.ConverterFor(valType);
56,126✔
1977
                if (converter != null)
56,126✔
1978
                {
595✔
1979
                    node.WriteConvertible(converter, value);
595✔
1980
                    return;
595✔
1981
                }
1982
            }
55,531✔
1983

1984
            if (!node.AllowReflection)
55,531✔
1985
            {
45✔
1986
                Dbg.Err($"Couldn't find a composition method for type {valType}; either you shouldn't be trying to serialize it, or it should implement Dec.IRecorder (https://zorbathut.github.io/dec/release/documentation/serialization.html), or you need a Dec.Converter (https://zorbathut.github.io/dec/release/documentation/custom.html)");
45✔
1987
                node.WriteError();
45✔
1988
                return;
45✔
1989
            }
1990

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

1993
            foreach (var field in valType.GetSerializableFieldsFromHierarchy())
759,936✔
1994
            {
296,739✔
1995
                ComposeElement(node.CreateReflectionChild(field, node.RecorderSettings), field.GetValue(value), field.FieldType, fieldInfo: field);
296,739✔
1996
            }
296,739✔
1997

1998
            return;
55,486✔
1999
        }
1,461,469✔
2000

2001
        internal static void Clear()
2002
        {
27,285✔
2003
            ConverterInitialized = false;
27,285✔
2004
            ConverterObjects = new System.Collections.Concurrent.ConcurrentDictionary<Type, Converter>();
27,285✔
2005
            ConverterGenericPrototypes = new System.Collections.Concurrent.ConcurrentDictionary<Type, Type>();
27,285✔
2006
        }
27,285✔
2007
    }
2008
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc