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

zorbathut / dec / 10660866206

02 Sep 2024 04:39AM UTC coverage: 90.614% (-0.5%) from 91.082%
10660866206

push

github

zorbathut
Proper support for Nullable.

4470 of 4933 relevant lines covered (90.61%)

195278.61 hits per line

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

88.36
/src/Serialization.cs
1
namespace Dec
2
{
3
    using System;
4
    using System.Collections;
5
    using System.Collections.Generic;
6
    using System.ComponentModel;
7
    using System.Linq;
8
    using System.Reflection;
9
    using System.Xml.Schema;
10

11
    /// <summary>
12
    /// Information on the current cursor position when reading files.
13
    /// </summary>
14
    /// <remarks>
15
    /// Standard output format is $"{inputContext}: Your Error Text Here!". This abstracts out the requirements for generating the locational-context text.
16
    /// </remarks>
17
    public struct InputContext
18
    {
19
        internal string filename;
20
        internal System.Xml.Linq.XElement handle;
21

22
        public InputContext(string filename)
23
        {
35✔
24
            this.filename = filename;
35✔
25
            this.handle = null;
35✔
26
        }
35✔
27

28
        public InputContext(string filename, System.Xml.Linq.XElement handle)
29
        {
3,993,915✔
30
            this.filename = filename;
3,993,915✔
31
            this.handle = handle;
3,993,915✔
32
        }
3,993,915✔
33

34
        public override string ToString()
35
        {
5,335✔
36
            if (this.handle != null)
5,335✔
37
            {
5,135✔
38
                return $"{filename}:{handle.LineNumber()}";
5,135✔
39
            }
40
            else
41
            {
200✔
42
                return filename;
200✔
43
            }
44
        }
5,335✔
45
    }
46

47
    /// <summary>
48
    /// Internal serialization utilities.
49
    /// </summary>
50
    internal static class Serialization
51
    {
52
        // Initialize it to empty in order to support Recorder operations without Dec initialization.
53
        // At some point we'll figure out how to support Converters at that point as well.
54
        internal static bool ConverterInitialized = false;
10✔
55
        internal static System.Collections.Concurrent.ConcurrentDictionary<Type, Converter> ConverterObjects = new System.Collections.Concurrent.ConcurrentDictionary<Type, Converter>();
10✔
56
        internal static System.Collections.Concurrent.ConcurrentDictionary<Type, Type> ConverterGenericPrototypes = new System.Collections.Concurrent.ConcurrentDictionary<Type, Type>();
10✔
57

58
        internal class ConverterNullableString<T> : ConverterString<T?> where T : struct
59
        {
60
            private ConverterString<T> child;
61

62
            public ConverterNullableString(ConverterString<T> child)
25✔
63
            {
25✔
64
                this.child = child;
25✔
65
            }
25✔
66

67
            public override string Write(T? input)
68
            {
×
69
                if (!input.HasValue)
×
70
                {
×
71
                    Dbg.Err("Internal error: ConverterNullableString.Write called with null value; this should never happen");
×
72
                    return "";
×
73
                }
74

75
                return child.Write(input.Value);
×
76
            }
×
77

78
            public override T? Read(string input, InputContext context)
79
            {
15✔
80
                // if we're null, we must have already handled this elsewhere
81
                return child.Read(input, context);
15✔
82
            }
15✔
83
        }
84

85
        internal class ConverterNullableRecord<T> : ConverterRecord<T?> where T : struct
86
        {
87
            private ConverterRecord<T> child;
88

89
            public ConverterNullableRecord(ConverterRecord<T> child)
25✔
90
            {
25✔
91
                this.child = child;
25✔
92
            }
25✔
93

94
            public override void Record(ref T? input, Recorder recorder)
95
            {
15✔
96
                if (recorder.Mode == Recorder.Direction.Write)
15✔
97
                {
×
98
                    if (!input.HasValue)
×
99
                    {
×
100
                        Dbg.Err("Internal error: ConverterNullableRecord called with null value in write mode; this should never happen");
×
101
                        return;
×
102
                    }
103

104
                    var value = input.Value;
×
105
                    child.Record(ref value, recorder);
×
106
                }
×
107
                else if (recorder.Mode == Recorder.Direction.Read)
15✔
108
                {
15✔
109
                    T value = default;
15✔
110
                    child.Record(ref value, recorder);
15✔
111
                    input = value;
15✔
112
                }
15✔
113
                else
114
                {
×
115
                    Dbg.Err("Internal error: ConverterNullableRecord called with unknown mode; this should never happen");
×
116
                }
×
117
            }
15✔
118
        }
119

120
        internal class ConverterNullableFactory<T> : ConverterFactory<T?> where T : struct
121
        {
122
            private ConverterFactory<T> child;
123

124
            public ConverterNullableFactory(ConverterFactory<T> child)
25✔
125
            {
25✔
126
                this.child = child;
25✔
127
            }
25✔
128

129
            public override T? Create(Recorder recorder)
130
            {
15✔
131
                return child.Create(recorder);
15✔
132
            }
15✔
133

134
            public override void Read(ref T? input, Recorder recorder)
135
            {
15✔
136
                if (!input.HasValue)
15✔
137
                {
×
138
                    Dbg.Err("Internal error: ConverterNullableFactory.Read called with null value; this should never happen");
×
139
                    return;
×
140
                }
141

142
                var value = input.Value;
15✔
143
                child.Read(ref value, recorder);
15✔
144
                input = value;
15✔
145
            }
15✔
146

147
            public override void Write(T? input, Recorder recorder)
148
            {
×
149
                if (!input.HasValue)
×
150
                {
×
151
                    Dbg.Err("Internal error: ConverterNullableFactory.Write called with null value; this should never happen");
×
152
                    return;
×
153
                }
154

155
                child.Write(input.Value, recorder);
×
156
            }
×
157
        }
158

159
        internal static Converter ConverterFor(Type inputType)
160
        {
5,334,706✔
161
            if (ConverterObjects.TryGetValue(inputType, out var converter))
5,334,706✔
162
            {
5,289,624✔
163
                return converter;
5,289,624✔
164
            }
165

166
            // check for Nullable
167
            if (inputType.IsConstructedGenericType && inputType.GetGenericTypeDefinition() == typeof(Nullable<>))
45,082✔
168
            {
200✔
169
                var nullableType = inputType.GenericTypeArguments[0];
200✔
170

171
                // handle all types of converters
172
                var originalConverter = ConverterFor(nullableType);
200✔
173
                if (originalConverter is ConverterString nullableStringConverter)
200✔
174
                {
25✔
175
                    var nullableConverterType = typeof(ConverterNullableString<>).MakeGenericType(nullableType);
25✔
176
                    var nullableConverter = (ConverterString)Activator.CreateInstance(nullableConverterType, new object[] { originalConverter });
25✔
177
                    ConverterObjects[inputType] = nullableConverter;
25✔
178
                    return nullableConverter;
25✔
179
                }
180
                else if (originalConverter is ConverterRecord nullableRecordConverter)
175✔
181
                {
25✔
182
                    var nullableConverterType = typeof(ConverterNullableRecord<>).MakeGenericType(nullableType);
25✔
183
                    var nullableConverter = (ConverterRecord)Activator.CreateInstance(nullableConverterType, new object[] { originalConverter });
25✔
184
                    ConverterObjects[inputType] = nullableConverter;
25✔
185
                    return nullableConverter;
25✔
186
                }
187
                else if (originalConverter is ConverterFactory nullableFactoryConverter)
150✔
188
                {
25✔
189
                    var nullableConverterType = typeof(ConverterNullableFactory<>).MakeGenericType(nullableType);
25✔
190
                    var nullableConverter = (ConverterFactory)Activator.CreateInstance(nullableConverterType, new object[] { originalConverter });
25✔
191
                    ConverterObjects[inputType] = nullableConverter;
25✔
192
                    return nullableConverter;
25✔
193
                }
194
                else if (originalConverter != null)
125✔
195
                {
×
196
                    Dbg.Err($"Found converter {originalConverter} which is not a string, record, or factory converter. This is not allowed.");
×
197
                }
×
198
            }
125✔
199

200
            if (inputType.IsConstructedGenericType)
45,007✔
201
            {
8,222✔
202
                var genericType = inputType.GetGenericTypeDefinition();
8,222✔
203
                if (ConverterGenericPrototypes.TryGetValue(genericType, out var converterType))
8,222✔
204
                {
50✔
205
                    // construct `prototype` with the same generic arguments that `type` has
206
                    var concreteConverterType = converterType.MakeGenericType(inputType.GenericTypeArguments);
50✔
207
                    converter = (Converter)concreteConverterType.CreateInstanceSafe("converter", null);
50✔
208

209
                    // yes, do this even if it's null
210
                    ConverterObjects[inputType] = converter;
50✔
211

212
                    return converter;
50✔
213
                }
214
                else
215
                {
8,172✔
216
                    // stub it out so we can do the fast path next time
217
                    ConverterObjects[inputType] = null;
8,172✔
218
                }
8,172✔
219
            }
8,172✔
220

221
            var factoriedConverter = Config.ConverterFactory?.Invoke(inputType);
44,957✔
222
            ConverterObjects[inputType] = factoriedConverter;   // cache this so we don't generate a million of them
44,957✔
223
            return factoriedConverter;
44,957✔
224
        }
5,334,706✔
225

226

227
        internal static void Initialize()
228
        {
18,855✔
229
            if (ConverterInitialized)
18,855✔
230
            {
4,165✔
231
                return;
4,165✔
232
            }
233

234
            // this is here just so we don't keep thrashing if something breaks
235
            ConverterInitialized = true;
14,690✔
236

237
            ConverterObjects = new System.Collections.Concurrent.ConcurrentDictionary<Type, Converter>();
14,690✔
238

239
            IEnumerable<Type> conversionTypes;
240
            if (Config.TestParameters == null)
14,690✔
241
            {
5✔
242
                conversionTypes = UtilReflection.GetAllUserTypes().Where(t => t.IsSubclassOf(typeof(Converter)));
7,110✔
243
            }
5✔
244
            else if (Config.TestParameters.explicitConverters != null)
14,685✔
245
            {
2,695✔
246
                conversionTypes = Config.TestParameters.explicitConverters;
2,695✔
247
            }
2,695✔
248
            else
249
            {
11,990✔
250
                conversionTypes = Enumerable.Empty<Type>();
11,990✔
251
            }
11,990✔
252

253
            foreach (var type in conversionTypes)
45,565✔
254
            {
755✔
255
                if (type.IsAbstract)
755✔
256
                {
5✔
257
                    Dbg.Err($"Found converter {type} which is abstract. This is not allowed.");
5✔
258
                    continue;
5✔
259
                }
260

261
                if (type.IsGenericType)
750✔
262
                {
30✔
263
                    var baseConverterType = type;
30✔
264
                    while (baseConverterType.BaseType != typeof(ConverterString) && baseConverterType.BaseType != typeof(ConverterRecord) && baseConverterType.BaseType != typeof(ConverterFactory))
60✔
265
                    {
30✔
266
                        baseConverterType = baseConverterType.BaseType;
30✔
267
                    }
30✔
268

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

273
                    if (!converterTarget.IsGenericType)
30✔
274
                    {
5✔
275
                        Dbg.Err($"Found generic converter {type} which is not referring to a generic constructed type.");
5✔
276
                        continue;
5✔
277
                    }
278

279
                    converterTarget = converterTarget.GetGenericTypeDefinition();
25✔
280
                    if (ConverterGenericPrototypes.ContainsKey(converterTarget))
25✔
281
                    {
×
282
                        Dbg.Err($"Found multiple converters for {converterTarget}: {ConverterGenericPrototypes[converterTarget]} and {type}");
×
283
                    }
×
284

285
                    ConverterGenericPrototypes[converterTarget] = type;
25✔
286
                    continue;
25✔
287
                }
288

289
                var converter = (Converter)type.CreateInstanceSafe("converter", null);
720✔
290
                if (converter != null && (converter is ConverterString || converter is ConverterRecord || converter is ConverterFactory))
720✔
291
                {
715✔
292
                    Type convertedType = converter.GetConvertedTypeHint();
715✔
293
                    if (ConverterObjects.ContainsKey(convertedType))
715✔
294
                    {
5✔
295
                        Dbg.Err($"Found multiple converters for {convertedType}: {ConverterObjects[convertedType]} and {type}");
5✔
296
                    }
5✔
297

298
                    ConverterObjects[convertedType] = converter;
715✔
299
                    continue;
715✔
300
                }
301
            }
5✔
302
        }
18,855✔
303

304
        internal static object GenerateResultFallback(object model, Type type)
305
        {
270✔
306
            if (model != null)
270✔
307
            {
5✔
308
                return model;
5✔
309
            }
310
            else if (type.IsValueType)
265✔
311
            {
165✔
312
                // We don't need Safe here because all value types are required to have a default constructor.
313
                return Activator.CreateInstance(type);
165✔
314
            }
315
            else
316
            {
100✔
317
                return null;
100✔
318
            }
319
        }
270✔
320

321
        internal enum ParseMode
322
        {
323
            Default,
324
            Replace,
325
            Patch,
326
            Append,
327

328
            // Dec-only
329
            Create,
330
            CreateOrReplace,
331
            CreateOrPatch,
332
            CreateOrIgnore,
333
            Delete,
334
            ReplaceIfExists,
335
            PatchIfExists,
336
            DeleteIfExists,
337
        }
338
        internal static ParseMode ParseModeFromString(InputContext context, string str)
339
        {
3,114,693✔
340
            if (str == null)
3,114,693✔
341
            {
3,108,183✔
342
                return ParseMode.Default;
3,108,183✔
343
            }
344
            else if (str == "replace")
6,510✔
345
            {
1,020✔
346
                return ParseMode.Replace;
1,020✔
347
            }
348
            else if (str == "patch")
5,490✔
349
            {
2,180✔
350
                return ParseMode.Patch;
2,180✔
351
            }
352
            else if (str == "append")
3,310✔
353
            {
950✔
354
                return ParseMode.Append;
950✔
355
            }
356
            else if (str == "create")
2,360✔
357
            {
320✔
358
                return ParseMode.Create;
320✔
359
            }
360
            else if (str == "createOrReplace")
2,040✔
361
            {
180✔
362
                return ParseMode.CreateOrReplace;
180✔
363
            }
364
            else if (str == "createOrPatch")
1,860✔
365
            {
200✔
366
                return ParseMode.CreateOrPatch;
200✔
367
            }
368
            else if (str == "createOrIgnore")
1,660✔
369
            {
100✔
370
                return ParseMode.CreateOrIgnore;
100✔
371
            }
372
            else if (str == "delete")
1,560✔
373
            {
240✔
374
                return ParseMode.Delete;
240✔
375
            }
376
            else if (str == "replaceIfExists")
1,320✔
377
            {
160✔
378
                return ParseMode.ReplaceIfExists;
160✔
379
            }
380
            else if (str == "patchIfExists")
1,160✔
381
            {
180✔
382
                return ParseMode.PatchIfExists;
180✔
383
            }
384
            else if (str == "deleteIfExists")
980✔
385
            {
140✔
386
                return ParseMode.DeleteIfExists;
140✔
387
            }
388
            else
389
            {
840✔
390
                Dbg.Err($"{context}: Invalid `{str}` mode!");
840✔
391

392
                return ParseMode.Default;
840✔
393
            }
394
        }
3,114,693✔
395

396
        internal enum ParseCommand
397
        {
398
            Replace,
399
            Patch,
400
            Append,
401
        }
402
        internal static List<(ParseCommand command, ReaderNodeParseable node)> CompileOrders(UtilType.ParseModeCategory modeCategory, List<ReaderNodeParseable> nodes)
403
        {
1,511,554✔
404
            var orders = new List<(ParseCommand command, ReaderNodeParseable payload)>();
1,511,554✔
405

406
            if (modeCategory == UtilType.ParseModeCategory.Dec)
1,511,554✔
407
            {
×
408
                Dbg.Err($"Internal error: CompileOrders called with Dec mode category, this should never happen! Please report it.");
×
409
                return orders;
×
410
            }
411

412
            foreach (var node in nodes)
7,557,770✔
413
            {
1,511,554✔
414
                var inputContext = node.GetInputContext();
1,511,554✔
415
                var s_parseMode = ParseModeFromString(inputContext, node.GetMetadata(ReaderNodeParseable.Metadata.Mode));
1,511,554✔
416

417
                ParseCommand s_parseCommand;
418

419
                switch (modeCategory)
1,511,554✔
420
                {
421
                    case UtilType.ParseModeCategory.Object:
422
                        switch (s_parseMode)
943,443✔
423
                        {
424
                            default:
425
                                Dbg.Err($"{inputContext}: Invalid mode {s_parseMode} provided for an Object-type parse, defaulting to Patch");
240✔
426
                                goto case ParseMode.Default;
240✔
427

428
                            case ParseMode.Default:
429
                            case ParseMode.Patch:
430
                                s_parseCommand = ParseCommand.Patch;
943,443✔
431
                                break;
943,443✔
432
                        }
433
                        break;
943,443✔
434
                    case UtilType.ParseModeCategory.OrderedContainer:
435
                        switch (s_parseMode)
131,030✔
436
                        {
437
                            default:
438
                                Dbg.Err($"{inputContext}: Invalid mode {s_parseMode} provided for an ordered-container-type parse, defaulting to Replace");
80✔
439
                                goto case ParseMode.Default;
80✔
440

441
                            case ParseMode.Default:
442
                            case ParseMode.Replace:
443
                                s_parseCommand = ParseCommand.Replace;
130,895✔
444
                                break;
130,895✔
445

446
                            case ParseMode.Append:
447
                                s_parseCommand = ParseCommand.Append;
135✔
448
                                break;
135✔
449
                        }
450
                        break;
131,030✔
451
                    case UtilType.ParseModeCategory.UnorderedContainer:
452
                        switch (s_parseMode)
35,665✔
453
                        {
454
                            default:
455
                                Dbg.Err($"{inputContext}: Invalid mode {s_parseMode} provided for an unordered-container-type parse, defaulting to Replace");
×
456
                                goto case ParseMode.Default;
×
457

458
                            case ParseMode.Default:
459
                            case ParseMode.Replace:
460
                                s_parseCommand = ParseCommand.Replace;
35,265✔
461
                                break;
35,265✔
462

463
                            case ParseMode.Patch:
464
                                s_parseCommand = ParseCommand.Patch;
240✔
465
                                break;
240✔
466

467
                            case ParseMode.Append:
468
                                s_parseCommand = ParseCommand.Append;
160✔
469
                                break;
160✔
470
                        }
471

472
                        break;
35,665✔
473
                    case UtilType.ParseModeCategory.Value:
474
                        switch (s_parseMode)
401,416✔
475
                        {
476
                            default:
477
                                Dbg.Err($"{inputContext}: Invalid mode {s_parseMode} provided for a value-type parse, defaulting to Replace");
80✔
478
                                goto case ParseMode.Default;
80✔
479

480
                            case ParseMode.Default:
481
                            case ParseMode.Replace:
482
                                s_parseCommand = ParseCommand.Replace;
401,416✔
483
                                break;
401,416✔
484
                        }
485
                        break;
401,416✔
486
                    default:
487
                        Dbg.Err($"{inputContext}: Internal error, unknown mode category {modeCategory}, please report");
×
488
                        s_parseCommand = ParseCommand.Patch;  // . . . I guess?
×
489
                        break;
×
490
                }
491

492
                if (s_parseCommand == ParseCommand.Replace)
1,511,554✔
493
                {
567,576✔
494
                    orders.Clear();
567,576✔
495
                }
567,576✔
496

497
                orders.Add((s_parseCommand, node));
1,511,554✔
498
            }
1,511,554✔
499

500
            return orders;
1,511,554✔
501
        }
1,511,554✔
502

503
        internal static List<ReaderFileDec.ReaderDec> CompileDecOrders(List<ReaderFileDec.ReaderDec> decs)
504
        {
45,050✔
505
            var orders = new List<ReaderFileDec.ReaderDec>();
45,050✔
506
            bool everExisted = false;
45,050✔
507
            foreach (var item in decs)
227,490✔
508
            {
46,170✔
509
                var s_parseMode = ParseModeFromString(item.inputContext, item.node.GetMetadata(ReaderNodeParseable.Metadata.Mode));
46,170✔
510

511
                switch (s_parseMode)
46,170✔
512
                {
513
                    default:
514
                        Dbg.Err($"{item.inputContext}: Invalid mode {s_parseMode} provided for a Dec-type parse, defaulting to Create");
20✔
515
                        goto case ParseMode.Default;
20✔
516

517
                    case ParseMode.Default:
518
                    case ParseMode.Create:
519
                        if (orders.Count != 0)
45,050✔
520
                        {
80✔
521
                            Dbg.Err($"{item.inputContext}: Create mode used when a Dec already exists, falling back to Patch");
80✔
522
                            goto case ParseMode.Patch;
80✔
523
                        }
524
                        orders.Add(item);
44,970✔
525
                        everExisted = true;
44,970✔
526
                        break;
44,970✔
527

528
                    case ParseMode.Replace:
529
                        if (orders.Count == 0)
100✔
530
                        {
40✔
531
                            Dbg.Err($"{item.inputContext}: Replace mode used when a Dec doesn't exist, falling back to Create");
40✔
532
                            goto case ParseMode.Create;
40✔
533
                        }
534
                        orders.Clear();
60✔
535
                        orders.Add(item);
60✔
536
                        break;
60✔
537

538
                    case ParseMode.Patch:
539
                        if (orders.Count == 0)
420✔
540
                        {
60✔
541
                            Dbg.Err($"{item.inputContext}: Patch mode used when a Dec doesn't exist, falling back to Create");
60✔
542
                            goto case ParseMode.Create;
60✔
543
                        }
544
                        orders.Add(item);
360✔
545
                        break;
360✔
546

547
                    case ParseMode.CreateOrReplace:
548
                        // doesn't matter if we have a thing or not
549
                        orders.Clear();
80✔
550
                        orders.Add(item);
80✔
551
                        everExisted = true;
80✔
552
                        break;
80✔
553

554
                    case ParseMode.CreateOrPatch:
555
                        // doesn't matter if we have a thing or not
556
                        orders.Add(item);
80✔
557
                        everExisted = true;
80✔
558
                        break;
80✔
559

560
                    case ParseMode.CreateOrIgnore:
561
                        if (orders.Count == 0)
80✔
562
                        {
20✔
563
                            orders.Add(item);
20✔
564
                            everExisted = true;
20✔
565
                        }
20✔
566
                        break;
80✔
567

568
                    case ParseMode.Delete:
569
                        if (!everExisted)
240✔
570
                        {
20✔
571
                            Dbg.Err($"{item.inputContext}: Delete mode used when a Dec doesn't exist; did you want deleteIfExists?");
20✔
572
                        }
20✔
573
                        orders.Clear();
240✔
574
                        break;
240✔
575

576
                    case ParseMode.ReplaceIfExists:
577
                        if (orders.Count != 0)
80✔
578
                        {
60✔
579
                            orders.Clear();
60✔
580
                            orders.Add(item);
60✔
581
                        }
60✔
582
                        break;
80✔
583

584
                    case ParseMode.PatchIfExists:
585
                        if (orders.Count != 0)
80✔
586
                        {
60✔
587
                            orders.Add(item);
60✔
588
                        }
60✔
589
                        break;
80✔
590

591
                    case ParseMode.DeleteIfExists:
592
                        orders.Clear();
140✔
593
                        break;
140✔
594
                }
595
            }
46,170✔
596

597
            return orders;
45,050✔
598
        }
45,050✔
599

600
        internal static object ParseElement(List<ReaderNodeParseable> nodes, Type type, object original, ReaderContext context, Recorder.Context recContext, FieldInfo fieldInfo = null, bool isRootDec = false, bool hasReferenceId = false, bool asThis = false, List<(ParseCommand command, ReaderNodeParseable node)> ordersOverride = null)
601
        {
1,555,484✔
602
            if (nodes == null || nodes.Count == 0)
1,555,484✔
603
            {
×
604
                Dbg.Err("Internal error, Dec failed to provide nodes to ParseElement. Please report this!");
×
605
                return original;
×
606
            }
607

608
            if (!context.allowReflection && nodes.Count > 1)
1,555,484✔
609
            {
×
610
                Dbg.Err("Internal error, multiple nodes provided for recorder-mode behavior. Please report this!");
×
611
            }
×
612

613
            // We keep the original around in case of error, but do all our manipulation on a result object.
614
            object result = original;
1,555,484✔
615

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

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

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

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

661
                var unrecognized = s_node.GetMetadataUnrecognized();
1,557,164✔
662
                if (unrecognized != null)
1,557,164✔
663
                {
65✔
664
                    Dbg.Err($"{s_node.GetInputContext()}: Has unknown attributes {unrecognized}");
65✔
665
                }
65✔
666
            }
1,557,164✔
667

668
            // Doesn't mean anything outside recorderMode, so we check it for validity just in case
669
            string refKey;
670
            ReaderNode refKeyNode = null; // stored entirely for error reporting
1,555,484✔
671
            if (!context.allowRefs)
1,555,484✔
672
            {
607,829✔
673
                refKey = null;
607,829✔
674
                foreach (var s_node in nodes)
3,042,505✔
675
                {
609,509✔
676
                    string nodeRefAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Ref);
609,509✔
677
                    if (nodeRefAttribute != null)
609,509✔
678
                    {
200✔
679
                        Dbg.Err($"{s_node.GetInputContext()}: Found a reference tag while not evaluating Recorder mode, ignoring it");
200✔
680
                    }
200✔
681
                }
609,509✔
682
            }
607,829✔
683
            else
684
            {
947,655✔
685
                (refKey, refKeyNode) = nodes.Select(node => (node.GetMetadata(ReaderNodeParseable.Metadata.Ref), node)).Where(anp => anp.Item1 != null).LastOrDefault();
2,842,965✔
686
            }
947,655✔
687

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

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

724
                    // update the class based on whatever this says
725
                    string localClassAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Class);
1,556,969✔
726
                    if (localClassAttribute != null)
1,556,969✔
727
                    {
207,911✔
728
                        classAttribute = localClassAttribute;
207,911✔
729
                        classAttributeNode = s_node;
207,911✔
730
                    }
207,911✔
731
                }
1,556,969✔
732

733
                if (classAttribute != null)
1,555,289✔
734
                {
207,911✔
735
                    var possibleType = (Type)ParseString(classAttribute, typeof(Type), null, classAttributeNode.GetInputContext());
207,911✔
736
                    if (!type.IsAssignableFrom(possibleType))
207,911✔
737
                    {
20✔
738
                        Dbg.Err($"{classAttributeNode.GetInputContext()}: Explicit type {classAttribute} cannot be assigned to expected type {type}");
20✔
739
                    }
20✔
740
                    else if (!replaced && result != null && result.GetType() != possibleType)
207,891✔
741
                    {
20✔
742
                        Dbg.Err($"{classAttributeNode.GetInputContext()}: Explicit type {classAttribute} does not match already-provided instance {type}");
20✔
743
                    }
20✔
744
                    else
745
                    {
207,871✔
746
                        type = possibleType;
207,871✔
747
                    }
207,871✔
748
                }
207,911✔
749
            }
1,555,289✔
750

751
            var converter = ConverterFor(type);
1,555,484✔
752

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

757
            // Gather info
758
            bool hasChildren = false;
1,555,484✔
759
            ReaderNode hasChildrenNode = null;
1,555,484✔
760
            bool hasText = false;
1,555,484✔
761
            ReaderNode hasTextNode = null;
1,555,484✔
762
            foreach (var (_, node) in orders)
7,780,780✔
763
            {
1,557,164✔
764
                if (!hasChildren && node.HasChildren())
1,557,164✔
765
                {
479,306✔
766
                    hasChildren = true;
479,306✔
767
                    hasChildrenNode = node;
479,306✔
768
                }
479,306✔
769
                if (!hasText && node.GetText() != null)
1,557,164✔
770
                {
356,811✔
771
                    hasText = true;
356,811✔
772
                    hasTextNode = node;
356,811✔
773
                }
356,811✔
774
            }
1,557,164✔
775

776
            // Actually handle our attributes
777
            if (refKey != null)
1,555,484✔
778
            {
357,160✔
779
                // Ref is the highest priority, largely because I think it's cool
780

781
                if (recContext.shared == Recorder.Context.Shared.Deny)
357,160✔
782
                {
55✔
783
                    Dbg.Err($"{refKeyNode.GetInputContext()}: 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✔
784
                }
55✔
785

786
                if (context.refs == null)
357,160✔
787
                {
40✔
788
                    Dbg.Err($"{refKeyNode.GetInputContext()}: Found a reference object {refKey} before refs are initialized (is this being used in a ConverterFactory<>.Create()?)");
40✔
789
                    return result;
40✔
790
                }
791

792
                if (!context.refs.ContainsKey(refKey))
357,120✔
793
                {
5✔
794
                    Dbg.Err($"{refKeyNode.GetInputContext()}: Found a reference object {refKey} without a valid reference mapping");
5✔
795
                    return result;
5✔
796
                }
797

798
                object refObject = context.refs[refKey];
357,115✔
799
                if (refObject == null && !type.IsValueType)
357,115✔
800
                {
80✔
801
                    // okay, good enough
802
                    return refObject;
80✔
803
                }
804

805
                if (!type.IsAssignableFrom(refObject.GetType()))
357,035✔
806
                {
25✔
807
                    Dbg.Err($"{refKeyNode.GetInputContext()}: Reference object {refKey} is of type {refObject.GetType()}, which cannot be converted to expected type {type}");
25✔
808
                    return result;
25✔
809
                }
810

811
                return refObject;
357,010✔
812
            }
813
            else if (isNull)
1,198,324✔
814
            {
249,602✔
815
                return null;
249,602✔
816

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

822
            // Basic early validation
823

824
            if (hasChildren && hasText)
948,722✔
825
            {
15✔
826
                Dbg.Err($"{hasChildrenNode.GetInputContext()} / {hasTextNode.GetInputContext()}: 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✔
827

828
                // we'll just fall through and try to parse anyway, though
829
            }
15✔
830

831
            if (typeof(Dec).IsAssignableFrom(type) && hasChildren && !isRootDec)
948,722✔
832
            {
×
833
                Dbg.Err($"{hasChildrenNode.GetInputContext()}: 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.");
×
834
                return null;
×
835
            }
836

837
            // Defer off to converters, whatever they feel like doing
838
            if (converter != null)
948,722✔
839
            {
605✔
840
                // string converter
841
                if (converter is ConverterString converterString)
605✔
842
                {
260✔
843
                    foreach (var (parseCommand, node) in orders)
1,300✔
844
                    {
260✔
845
                        switch (parseCommand)
260✔
846
                        {
847
                            case ParseCommand.Replace:
848
                                // easy, done
849
                                break;
260✔
850

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

856
                        if (hasChildren)
260✔
857
                        {
15✔
858
                            Dbg.Err($"{node.GetInputContext()}: String converter {converter.GetType()} called with child XML nodes, which will be ignored");
15✔
859
                        }
15✔
860

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

863
                        // context might be null; that's OK at the moment
864
                        try
865
                        {
260✔
866
                            result = converterString.ReadObj(node.GetText() ?? "", node.GetInputContext());
260✔
867
                        }
175✔
868
                        catch (Exception e)
85✔
869
                        {
85✔
870
                            Dbg.Ex(new ConverterReadException(node.GetInputContext(), converter, e));
85✔
871

872
                            result = GenerateResultFallback(result, type);
85✔
873
                        }
85✔
874
                    }
260✔
875
                }
260✔
876
                else if (converter is ConverterRecord converterRecord)
345✔
877
                {
235✔
878
                    foreach (var (parseCommand, node) in orders)
1,175✔
879
                    {
235✔
880
                        switch (parseCommand)
235✔
881
                        {
882
                            case ParseCommand.Patch:
883
                                // easy, done
884
                                break;
235✔
885

886
                            case ParseCommand.Replace:
887
                                result = null;
×
888
                                break;
×
889

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

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

897
                        if (result == null && !isNullable)
235✔
898
                        {
130✔
899
                            result = type.CreateInstanceSafe("converterrecord", node);
130✔
900
                        }
130✔
901

902
                        // context might be null; that's OK at the moment
903
                        if (result != null || isNullable)
235✔
904
                        {
235✔
905
                            var recorderReader = new RecorderReader(node, context, trackUsage: true);
235✔
906
                            try
907
                            {
235✔
908
                                object returnedResult = converterRecord.RecordObj(result, recorderReader);
235✔
909

910
                                if (!type.IsValueType && result != returnedResult)
220✔
911
                                {
×
912
                                    Dbg.Err($"{node.GetInputContext()}: Converter {converterRecord.GetType()} changed object instance, this is disallowed");
×
913
                                }
×
914
                                else
915
                                {
220✔
916
                                    // for value types, this is fine
917
                                    result = returnedResult;
220✔
918
                                }
220✔
919

920
                                recorderReader.ReportUnusedFields();
220✔
921
                            }
220✔
922
                            catch (Exception e)
15✔
923
                            {
15✔
924
                                Dbg.Ex(new ConverterReadException(node.GetInputContext(), converter, e));
15✔
925

926
                                // no fallback needed, we already have a result
927
                            }
15✔
928
                        }
235✔
929
                    }
235✔
930
                }
235✔
931
                else if (converter is ConverterFactory converterFactory)
110✔
932
                {
110✔
933
                    foreach (var (parseCommand, node) in orders)
550✔
934
                    {
110✔
935
                        switch (parseCommand)
110✔
936
                        {
937
                            case ParseCommand.Patch:
938
                                // easy, done
939
                                break;
110✔
940

941
                            case ParseCommand.Replace:
942
                                result = null;
×
943
                                break;
×
944

945
                            default:
946
                                Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
947
                                break;
×
948
                        }
949

950
                        var recorderReader = new RecorderReader(node, context, disallowShared: true, trackUsage: true);
110✔
951
                        if (result == null)
110✔
952
                        {
105✔
953
                            try
954
                            {
105✔
955
                                result = converterFactory.CreateObj(recorderReader);
105✔
956
                            }
90✔
957
                            catch (Exception e)
15✔
958
                            {
15✔
959
                                Dbg.Ex(new ConverterReadException(node.GetInputContext(), converter, e));
15✔
960
                            }
15✔
961
                        }
105✔
962

963
                        // context might be null; that's OK at the moment
964
                        if (result != null)
110✔
965
                        {
95✔
966
                            recorderReader.AllowShared(context);
95✔
967
                            try
968
                            {
95✔
969
                                result = converterFactory.ReadObj(result, recorderReader);
95✔
970
                                recorderReader.ReportUnusedFields();
80✔
971
                            }
80✔
972
                            catch (Exception e)
15✔
973
                            {
15✔
974
                                Dbg.Ex(new ConverterReadException(node.GetInputContext(), converter, e));
15✔
975

976
                                // no fallback needed, we already have a result
977
                            }
15✔
978
                        }
95✔
979
                    }
110✔
980
                }
110✔
981
                else
982
                {
×
983
                    Dbg.Err($"Somehow ended up with an unsupported converter {converter.GetType()}");
×
984
                }
×
985

986
                return result;
605✔
987
            }
988

989
            // All our standard text-using options
990
            // Placed before IRecordable just in case we have a Dec that is IRecordable
991
            if ((typeof(Dec).IsAssignableFrom(type) && !isRootDec) ||
948,117✔
992
                    type == typeof(Type) ||
948,117✔
993
                    type == typeof(string) ||
948,117✔
994
                    type.IsPrimitive ||
948,117✔
995
                    (TypeDescriptor.GetConverter(type)?.CanConvertFrom(typeof(string)) ?? false)   // this is last because it's slow
948,117✔
996
                )
948,117✔
997
            {
397,386✔
998
                foreach (var (parseCommand, node) in orders)
1,986,930✔
999
                {
397,386✔
1000
                    switch (parseCommand)
397,386✔
1001
                    {
1002
                        case ParseCommand.Replace:
1003
                            // easy, done
1004
                            break;
397,386✔
1005

1006
                        default:
1007
                            Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1008
                            break;
×
1009
                    }
1010

1011
                    if (hasChildren)
397,386✔
1012
                    {
20✔
1013
                        Dbg.Err($"{node.GetInputContext()}: Child nodes are not valid when parsing {type}");
20✔
1014
                    }
20✔
1015

1016
                    result = ParseString(node.GetText(), type, result, node.GetInputContext());
397,386✔
1017
                }
397,386✔
1018

1019
                return result;
397,386✔
1020
            }
1021

1022
            // Special case: IRecordables
1023
            IRecordable recordableBuffered = null;
550,731✔
1024
            if (typeof(IRecordable).IsAssignableFrom(type))
550,731✔
1025
            {
388,640✔
1026
                // we're going to need to make one anyway so let's just go ahead and do that
1027
                IRecordable recordable = null;
388,640✔
1028

1029
                if (result != null)
388,640✔
1030
                {
206,705✔
1031
                    recordable = (IRecordable)result;
206,705✔
1032
                }
206,705✔
1033
                else if (recContext.factories == null)
181,935✔
1034
                {
180,905✔
1035
                    recordable = (IRecordable)type.CreateInstanceSafe("recordable", orders[0].node);
180,905✔
1036
                }
180,905✔
1037
                else
1038
                {
1,030✔
1039
                    recordable = recContext.CreateRecordableFromFactory(type, "recordable", orders[0].node);
1,030✔
1040
                }
1,030✔
1041

1042
                // we hold on to this so that, *if* we end up not using this object, we can optionally reuse it later for reflection
1043
                // 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
1044
                recordableBuffered = recordable;
388,640✔
1045

1046
                var conditionalRecordable = recordable as IConditionalRecordable;
388,640✔
1047
                if (conditionalRecordable == null || conditionalRecordable.ShouldRecord(nodes[0].UserSettings))
388,640✔
1048
                {
388,625✔
1049
                    foreach (var (parseCommand, node) in orders)
1,943,125✔
1050
                    {
388,625✔
1051
                        switch (parseCommand)
388,625✔
1052
                        {
1053
                            case ParseCommand.Patch:
1054
                                // easy, done
1055
                                break;
388,625✔
1056

1057
                            default:
1058
                                Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1059
                                break;
×
1060
                        }
1061

1062
                        if (recordable != null)
388,625✔
1063
                        {
388,605✔
1064
                            var recorderReader = new RecorderReader(node, context, trackUsage: true);
388,605✔
1065
                            recordable.Record(recorderReader);
388,605✔
1066
                            recorderReader.ReportUnusedFields();
388,605✔
1067

1068
                            // TODO: support indices if this is within the Dec system?
1069
                        }
388,605✔
1070
                    }
388,625✔
1071

1072
                    result = recordable;
388,625✔
1073
                    return result;
388,625✔
1074
                }
1075

1076
                // otherwise we just fall through
1077
            }
15✔
1078

1079
            // Nothing past this point even supports text, so let's just get angry and break stuff.
1080
            if (hasText)
162,106✔
1081
            {
80✔
1082
                Dbg.Err($"{hasTextNode.GetInputContext()}: Text detected in a situation where it is invalid; will be ignored");
80✔
1083
                return result;
80✔
1084
            }
1085

1086
            // Special case: Lists
1087
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
162,026✔
1088
            {
46,120✔
1089
                foreach (var (parseCommand, node) in orders)
230,600✔
1090
                {
46,120✔
1091
                    switch (parseCommand)
46,120✔
1092
                    {
1093
                        case ParseCommand.Replace:
1094
                            // 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.
1095
                            // 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.
1096
                            // 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.
1097
                            if (result != null)
46,040✔
1098
                            {
400✔
1099
                                ((IList)result).Clear();
400✔
1100
                            }
400✔
1101
                            break;
46,040✔
1102

1103
                        case ParseCommand.Append:
1104
                            // we're good
1105
                            break;
80✔
1106

1107
                        default:
1108
                            Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1109
                            break;
×
1110
                    }
1111

1112
                    // List<> handling
1113
                    Type referencedType = type.GetGenericArguments()[0];
46,120✔
1114

1115
                    var list = (IList)(result ?? Activator.CreateInstance(type));
46,120✔
1116

1117
                    node.ParseList(list, referencedType, context, recContext);
46,120✔
1118

1119
                    result = list;
46,120✔
1120
                }
46,120✔
1121

1122
                return result;
46,120✔
1123
            }
1124

1125
            // Special case: Arrays
1126
            if (type.IsArray)
115,906✔
1127
            {
2,485✔
1128
                Type referencedType = type.GetElementType();
2,485✔
1129

1130
                foreach (var (parseCommand, node) in orders)
12,425✔
1131
                {
2,485✔
1132
                    Array array = null;
2,485✔
1133
                    int startOffset = 0;
2,485✔
1134

1135
                    // 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.
1136
                    switch (parseCommand)
2,485✔
1137
                    {
1138
                        case ParseCommand.Replace:
1139
                        {
2,450✔
1140
                            // This is a full override, so we're going to create it here.
1141
                            // It is actually vitally important that we fall back on the model when possible, because the Recorder Ref system requires it.
1142
                            bool match = result != null && result.GetType() == type;
2,450✔
1143
                            var arrayDimensions = node.GetArrayDimensions(type.GetArrayRank());
2,450✔
1144
                            if (match)
2,450✔
1145
                            {
410✔
1146
                                array = (Array)result;
410✔
1147
                                if (array.Rank != type.GetArrayRank())
410✔
1148
                                {
×
1149
                                    match = false;
×
1150
                                }
×
1151
                                else
1152
                                {
410✔
1153
                                    for (int i = 0; i < array.Rank; i++)
1,040✔
1154
                                    {
420✔
1155
                                        if (array.GetLength(i) != arrayDimensions[i])
420✔
1156
                                        {
310✔
1157
                                            match = false;
310✔
1158
                                            break;
310✔
1159
                                        }
1160
                                    }
110✔
1161
                                }
410✔
1162
                            }
410✔
1163

1164
                            if (!match)
2,450✔
1165
                            {
2,350✔
1166
                                // Otherwise just make a new one, no harm done.
1167
                                array = Array.CreateInstance(referencedType, arrayDimensions);
2,350✔
1168
                            }
2,350✔
1169

1170
                            break;
2,450✔
1171
                        }
1172

1173
                        case ParseCommand.Append:
1174
                        {
55✔
1175
                            if (result == null)
55✔
1176
                            {
20✔
1177
                                goto case ParseCommand.Replace;
20✔
1178
                            }
1179

1180
                            // This is jankier; we create it here with the intended final length, then copy the elements over, all because arrays can't be resized
1181
                            // (yes, I know, that's the point of arrays, I'm not complaining, just . . . grumbling a little)
1182
                            var oldArray = (Array)result;
35✔
1183
                            startOffset = oldArray.Length;
35✔
1184
                            var arrayDimensions = node.GetArrayDimensions(type.GetArrayRank());
35✔
1185
                            arrayDimensions[0] += startOffset;
35✔
1186
                            array = Array.CreateInstance(referencedType, arrayDimensions);
35✔
1187
                            if (arrayDimensions.Length == 1)
35✔
1188
                            {
20✔
1189
                                oldArray.CopyTo(array, 0);
20✔
1190
                            }
20✔
1191
                            else
1192
                            {
15✔
1193
                                // oy
1194
                                void CopyArray(Array source, Array destination, int[] indices, int rank = 0)
1195
                                {
60✔
1196
                                    if (rank < source.Rank)
60✔
1197
                                    {
45✔
1198
                                        for (int i = 0; i < source.GetLength(rank); i++)
180✔
1199
                                        {
45✔
1200
                                            indices[rank] = i;
45✔
1201
                                            CopyArray(source, destination, indices, rank + 1);
45✔
1202
                                        }
45✔
1203
                                    }
45✔
1204
                                    else
1205
                                    {
15✔
1206
                                        destination.SetValue(source.GetValue(indices), indices);
15✔
1207
                                    }
15✔
1208
                                }
60✔
1209

1210
                                {
15✔
1211
                                    int[] indices = new int[arrayDimensions.Length];
15✔
1212
                                    CopyArray(oldArray, array, indices, 0);
15✔
1213
                                }
15✔
1214
                            }
15✔
1215

1216
                            break;
35✔
1217
                        }
1218

1219
                        default:
1220
                            Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1221
                            array = null; // just to break the unassigned-local-variable
×
1222
                            break;
×
1223
                    }
1224

1225
                    node.ParseArray(array, referencedType, context, recContext, startOffset);
2,485✔
1226

1227
                    result = array;
2,485✔
1228
                }
2,485✔
1229

1230
                return result;
2,485✔
1231
            }
1232

1233
            // Special case: Dictionaries
1234
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
113,421✔
1235
            {
23,380✔
1236
                foreach (var (parseCommand, node) in orders)
116,900✔
1237
                {
23,380✔
1238
                    bool permitPatch = false;
23,380✔
1239
                    switch (parseCommand)
23,380✔
1240
                    {
1241
                        case ParseCommand.Replace:
1242
                            // 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.
1243
                            // 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.
1244
                            // 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.
1245
                            if (result != null)
23,140✔
1246
                            {
485✔
1247
                                ((IDictionary)result).Clear();
485✔
1248
                            }
485✔
1249
                            break;
23,140✔
1250

1251
                        case ParseCommand.Patch:
1252
                            if (original != null)
160✔
1253
                            {
140✔
1254
                                permitPatch = true;
140✔
1255
                            }
140✔
1256
                            break;
160✔
1257

1258
                        case ParseCommand.Append:
1259
                            // nothing needs to be done, our existing dupe checking will solve it
1260
                            break;
80✔
1261

1262
                        default:
1263
                            Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1264
                            break;
×
1265
                    }
1266

1267
                    // Dictionary<> handling
1268
                    Type keyType = type.GetGenericArguments()[0];
23,380✔
1269
                    Type valueType = type.GetGenericArguments()[1];
23,380✔
1270

1271
                    var dict = (IDictionary)(result ?? Activator.CreateInstance(type));
23,380✔
1272

1273
                    node.ParseDictionary(dict, keyType, valueType, context, recContext, permitPatch);
23,380✔
1274

1275
                    result = dict;
23,380✔
1276
                }
23,380✔
1277

1278
                return result;
23,380✔
1279
            }
1280

1281
            // Special case: HashSet
1282
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(HashSet<>))
90,041✔
1283
            {
985✔
1284
                foreach (var (parseCommand, node) in orders)
4,925✔
1285
                {
985✔
1286
                    bool permitPatch = false;
985✔
1287
                    switch (parseCommand)
985✔
1288
                    {
1289
                        case ParseCommand.Replace:
1290
                            // 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.
1291
                            // 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.
1292
                            // 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.
1293
                            if (result != null)
825✔
1294
                            {
450✔
1295
                                // Did you know there's no non-generic interface that HashSet<> supports that includes a Clear function?
1296
                                // Fun fact:
1297
                                // That thing I just wrote!
1298
                                var clearFunction = result.GetType().GetMethod("Clear");
450✔
1299
                                clearFunction.Invoke(result, null);
450✔
1300
                            }
450✔
1301
                            break;
825✔
1302

1303
                        case ParseCommand.Patch:
1304
                            if (original != null)
80✔
1305
                            {
60✔
1306
                                permitPatch = true;
60✔
1307
                            }
60✔
1308
                            break;
80✔
1309

1310
                        case ParseCommand.Append:
1311
                            // nothing needs to be done, our existing dupe checking will solve it
1312
                            break;
80✔
1313

1314
                        default:
1315
                            Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1316
                            break;
×
1317
                    }
1318

1319
                    Type keyType = type.GetGenericArguments()[0];
985✔
1320

1321
                    var set = result ?? Activator.CreateInstance(type);
985✔
1322

1323
                    node.ParseHashset(set, keyType, context, recContext, permitPatch);
985✔
1324

1325
                    result = set;
985✔
1326
                }
985✔
1327

1328
                return result;
985✔
1329
            }
1330

1331
            // Special case: Stack
1332
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Stack<>))
89,056✔
1333
            {
55✔
1334
                // Stack<> handling
1335
                // Again, no sensible non-generic interface to use, so we're stuck with reflection
1336

1337
                foreach (var (parseCommand, node) in orders)
275✔
1338
                {
55✔
1339
                    switch (parseCommand)
55✔
1340
                    {
1341
                        case ParseCommand.Replace:
1342
                            // 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.
1343
                            // 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.
1344
                            // 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.
1345
                            if (result != null)
55✔
1346
                            {
5✔
1347
                                var clearFunction = result.GetType().GetMethod("Clear");
5✔
1348
                                clearFunction.Invoke(result, null);
5✔
1349
                            }
5✔
1350
                            break;
55✔
1351

1352
                        case ParseCommand.Append:
1353
                            break;
×
1354

1355
                        // There definitely starts being an argument for prepend.
1356

1357
                        default:
1358
                            Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1359
                            break;
×
1360
                    }
1361

1362
                    Type keyType = type.GetGenericArguments()[0];
55✔
1363

1364
                    var set = result ?? Activator.CreateInstance(type);
55✔
1365

1366
                    node.ParseStack(set, keyType, context, recContext);
55✔
1367

1368
                    result = set;
55✔
1369
                }
55✔
1370

1371
                return result;
55✔
1372
            }
1373

1374
            // Special case: Queue
1375
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Queue<>))
89,001✔
1376
            {
55✔
1377
                // Queue<> handling
1378
                // Again, no sensible non-generic interface to use, so we're stuck with reflection
1379

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

1395
                        case ParseCommand.Append:
1396
                            break;
×
1397

1398
                        // There definitely starts being an argument for prepend.
1399

1400
                        default:
1401
                            Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1402
                            break;
×
1403
                    }
1404

1405
                    Type keyType = type.GetGenericArguments()[0];
55✔
1406

1407
                    var set = result ?? Activator.CreateInstance(type);
55✔
1408

1409
                    node.ParseQueue(set, keyType, context, recContext);
55✔
1410

1411
                    result = set;
55✔
1412
                }
55✔
1413

1414
                return result;
55✔
1415
            }
1416

1417
            // Special case: A bucket of tuples
1418
            // These are all basically identical, but AFAIK there's no good way to test them all in a better way.
1419
            if (type.IsGenericType && (
88,946✔
1420
                    type.GetGenericTypeDefinition() == typeof(Tuple<>) ||
88,946✔
1421
                    type.GetGenericTypeDefinition() == typeof(Tuple<,>) ||
88,946✔
1422
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,>) ||
88,946✔
1423
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,>) ||
88,946✔
1424
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,>) ||
88,946✔
1425
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,>) ||
88,946✔
1426
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,>) ||
88,946✔
1427
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,,>) ||
88,946✔
1428
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<>) ||
88,946✔
1429
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,>) ||
88,946✔
1430
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,>) ||
88,946✔
1431
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,>) ||
88,946✔
1432
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,>) ||
88,946✔
1433
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,>) ||
88,946✔
1434
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,>) ||
88,946✔
1435
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,,>)
88,946✔
1436
                    ))
88,946✔
1437
            {
1,065✔
1438
                foreach (var (parseCommand, node) in orders)
5,325✔
1439
                {
1,065✔
1440
                    switch (parseCommand)
1,065✔
1441
                    {
1442
                        case ParseCommand.Replace:
1443
                            // easy, done
1444
                            break;
1,065✔
1445

1446
                        default:
1447
                            Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1448
                            break;
×
1449
                    }
1450

1451
                    int expectedCount = type.GenericTypeArguments.Length;
1,065✔
1452
                    object[] parameters = new object[expectedCount];
1,065✔
1453

1454
                    node.ParseTuple(parameters, type, fieldInfo?.GetCustomAttribute<System.Runtime.CompilerServices.TupleElementNamesAttribute>()?.TransformNames, context, recContext);
1,065✔
1455

1456
                    // construct!
1457
                    result = Activator.CreateInstance(type, parameters);
1,065✔
1458
                }
1,065✔
1459

1460
                return result;
1,065✔
1461
            }
1462

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

1465
            // If we have refs, something has gone wrong; we should never be doing reflection inside a Record system.
1466
            // This is a really ad-hoc way of testing this and should be fixed.
1467
            // 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.
1468
            // I'm less OK with security vulnerabilities in save files. Nobody expects a savefile can compromise their system.
1469
            // And the full reflection system is probably impossible to secure, whereas the Record system should be secureable.
1470
            if (!context.allowReflection)
87,881✔
1471
            {
40✔
1472
                // just pick the first node to get something to go on
1473
                Dbg.Err($"{orders[0].node.GetInputContext()}: 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✔
1474
                return result;
40✔
1475
            }
1476

1477
            foreach (var (parseCommand, node) in orders)
442,485✔
1478
            {
89,521✔
1479
                if (!isRootDec)
89,521✔
1480
                {
44,021✔
1481
                    switch (parseCommand)
44,021✔
1482
                    {
1483
                        case ParseCommand.Patch:
1484
                            // easy, done
1485
                            break;
44,021✔
1486

1487
                        default:
1488
                            Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1489
                            break;
×
1490
                    }
1491
                }
44,021✔
1492
                else
1493
                {
45,500✔
1494
                    if (parseCommand != ParseCommand.Patch)
45,500✔
1495
                    {
×
1496
                        Dbg.Err($"{node.GetInputContext()}: Mode provided for root Dec; this is currently not supported in any form");
×
1497
                    }
×
1498
                }
45,500✔
1499

1500
                // If we haven't been given a generic class from our parent, go ahead and init to defaults
1501
                if (result == null && recordableBuffered != null)
89,521✔
1502
                {
15✔
1503
                    result = recordableBuffered;
15✔
1504
                }
15✔
1505

1506
                if (result == null)
89,521✔
1507
                {
17,640✔
1508
                    // okay fine
1509
                    result = type.CreateInstanceSafe("object", node);
17,640✔
1510

1511
                    if (result == null)
17,640✔
1512
                    {
80✔
1513
                        // error already reported
1514
                        return result;
80✔
1515
                    }
1516
                }
17,560✔
1517

1518
                node.ParseReflection(result, context, recContext);
89,441✔
1519
            }
89,441✔
1520

1521
            // Set up our index fields; this has to happen last in case we're a struct
1522
            Index.Register(ref result);
87,761✔
1523

1524
            return result;
87,761✔
1525
        }
1,555,484✔
1526

1527
        internal static object ParseString(string text, Type type, object original, InputContext context)
1528
        {
814,092✔
1529
            // Special case: Converter override
1530
            // This is redundant if we're being called from ParseElement, but we aren't always.
1531
            if (ConverterFor(type) is Converter converter)
814,092✔
1532
            {
45✔
1533
                object result = original;
45✔
1534

1535
                try
1536
                {
45✔
1537
                    // string converter
1538
                    if (converter is ConverterString converterString)
45✔
1539
                    {
45✔
1540
                        // context might be null; that's OK at the moment
1541
                        try
1542
                        {
45✔
1543
                            result = converterString.ReadObj(text, context);
45✔
1544
                        }
45✔
1545
                        catch (Exception e)
×
1546
                        {
×
1547
                            Dbg.Ex(new ConverterReadException(context, converter, e));
×
1548

1549
                            result = GenerateResultFallback(result, type);
×
1550
                        }
×
1551
                    }
45✔
1552
                    else if (converter is ConverterRecord converterRecord)
×
1553
                    {
×
1554
                        // string parsing really doesn't apply here, we can't get a full Recorder context out anymore
1555
                        // in theory this could be done with RecordAsThis() but I'm just going to skip it for now
1556
                        Dbg.Err($"{context}: Attempt to string-parse with a ConverterRecord, this is currently not supported, contact developers if you need this feature");
×
1557
                    }
×
1558
                    else if (converter is ConverterFactory converterFactory)
×
1559
                    {
×
1560
                        // string parsing really doesn't apply here, we can't get a full Recorder context out anymore
1561
                        // in theory this could be done with RecordAsThis() but I'm just going to skip it for now
1562
                        Dbg.Err($"{context}: Attempt to string-parse with a ConverterFactory, this is currently not supported, contact developers if you need this feature");
×
1563
                    }
×
1564
                    else
1565
                    {
×
1566
                        Dbg.Err($"Somehow ended up with an unsupported converter {converter.GetType()}");
×
1567
                    }
×
1568
                }
45✔
1569
                catch (Exception e)
×
1570
                {
×
1571
                    Dbg.Ex(e);
×
1572
                }
×
1573

1574
                return result;
45✔
1575
            }
1576

1577
            // Special case: decs
1578
            if (typeof(Dec).IsAssignableFrom(type))
814,047✔
1579
            {
48,285✔
1580
                if (text == "" || text == null)
48,285✔
1581
                {
40,695✔
1582
                    // you reference nothing, you get the null (even if this isn't a specified type; null is null, after all)
1583
                    return null;
40,695✔
1584
                }
1585
                else
1586
                {
7,590✔
1587
                    if (type.GetDecRootType() == null)
7,590✔
1588
                    {
80✔
1589
                        Dbg.Err($"{context}: Non-hierarchy decs cannot be used as references");
80✔
1590
                        return null;
80✔
1591
                    }
1592

1593
                    Dec result = Database.Get(type, text);
7,510✔
1594
                    if (result == null)
7,510✔
1595
                    {
85✔
1596
                        if (UtilMisc.ValidateDecName(text, context))
85✔
1597
                        {
85✔
1598
                            Dbg.Err($"{context}: Couldn't find {type} named `{text}`");
85✔
1599
                        }
85✔
1600

1601
                        // If we're an invalid name, we already spat out the error
1602
                    }
85✔
1603
                    return result;
7,510✔
1604
                }
1605
            }
1606

1607
            // Special case: types
1608
            if (type == typeof(Type))
765,762✔
1609
            {
415,056✔
1610
                if (text == "")
415,056✔
1611
                {
×
1612
                    return null;
×
1613
                }
1614

1615
                return UtilType.ParseDecFormatted(text, context);
415,056✔
1616
            }
1617

1618
            // Various non-composite-type special-cases
1619
            if (text != "")
350,706✔
1620
            {
350,706✔
1621
                // If we've got text, treat us as an object of appropriate type
1622
                try
1623
                {
350,706✔
1624
                    if (type == typeof(float))
350,706✔
1625
                    {
28,250✔
1626
                        // first check the various strings, case-insensitive
1627
                        if (String.Compare(text, "nan", true) == 0)
28,250✔
1628
                        {
90✔
1629
                            return float.NaN;
90✔
1630
                        }
1631

1632
                        if (String.Compare(text, "infinity", true) == 0)
28,160✔
1633
                        {
40✔
1634
                            return float.PositiveInfinity;
40✔
1635
                        }
1636

1637
                        if (String.Compare(text, "-infinity", true) == 0)
28,120✔
1638
                        {
40✔
1639
                            return float.NegativeInfinity;
40✔
1640
                        }
1641

1642
                        if (text.StartsWith("nanbox", StringComparison.CurrentCultureIgnoreCase))
28,080✔
1643
                        {
85✔
1644
                            const int expectedFloatSize = 6 + 8;
1645

1646
                            if (type == typeof(float) && text.Length != expectedFloatSize)
85✔
1647
                            {
×
1648
                                Dbg.Err($"{context}: Found nanboxed value without the expected number of characters, expected {expectedFloatSize} but got {text.Length}");
×
1649
                                return float.NaN;
×
1650
                            }
1651

1652
                            int number = Convert.ToInt32(text.Substring(6), 16);
85✔
1653
                            return BitConverter.Int32BitsToSingle(number);
85✔
1654
                        }
1655
                    }
27,995✔
1656

1657
                    if (type == typeof(double))
350,451✔
1658
                    {
2,840✔
1659
                        // first check the various strings, case-insensitive
1660
                        if (String.Compare(text, "nan", true) == 0)
2,840✔
1661
                        {
1,815✔
1662
                            return double.NaN;
1,815✔
1663
                        }
1664

1665
                        if (String.Compare(text, "infinity", true) == 0)
1,025✔
1666
                        {
40✔
1667
                            return double.PositiveInfinity;
40✔
1668
                        }
1669

1670
                        if (String.Compare(text, "-infinity", true) == 0)
985✔
1671
                        {
40✔
1672
                            return double.NegativeInfinity;
40✔
1673
                        }
1674

1675
                        if (text.StartsWith("nanbox", StringComparison.CurrentCultureIgnoreCase))
945✔
1676
                        {
75✔
1677
                            const int expectedDoubleSize = 6 + 16;
1678

1679
                            if (type == typeof(double) && text.Length != expectedDoubleSize)
75✔
1680
                            {
×
1681
                                Dbg.Err($"{context}: Found nanboxed value without the expected number of characters, expected {expectedDoubleSize} but got {text.Length}");
×
1682
                                return double.NaN;
×
1683
                            }
1684

1685
                            long number = Convert.ToInt64(text.Substring(6), 16);
75✔
1686
                            return BitConverter.Int64BitsToDouble(number);
75✔
1687
                        }
1688
                    }
870✔
1689

1690
                    return TypeDescriptor.GetConverter(type).ConvertFromString(text);
348,481✔
1691
                }
1692
                catch (System.Exception e)  // I would normally not catch System.Exception, but TypeConverter is wrapping FormatException in an Exception for some reason
180✔
1693
                {
180✔
1694
                    Dbg.Err($"{context}: {e.ToString()}");
180✔
1695
                    return original;
180✔
1696
                }
1697
            }
1698
            else if (type == typeof(string))
×
1699
            {
×
1700
                // If we don't have text, and we're a string, return ""
1701
                return "";
×
1702
            }
1703
            else
1704
            {
×
1705
                // If we don't have text, and we've fallen down to this point, that's an error (and return original value I guess)
1706
                Dbg.Err($"{context}: Empty field provided for type {type}");
×
1707
                return original;
×
1708
            }
1709
        }
814,092✔
1710

1711
        internal static Type TypeSystemRuntimeType = Type.GetType("System.RuntimeType");
10✔
1712
        internal static void ComposeElement(WriterNode node, object value, Type fieldType, FieldInfo fieldInfo = null, bool isRootDec = false, bool asThis = false)
1713
        {
1,459,744✔
1714
            // Verify our Shared flags as the *very* first step to ensure nothing gets past us.
1715
            // 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
1716
            bool canBeShared = fieldType.CanBeShared();
1,459,744✔
1717
            if (node.RecorderContext.shared == Recorder.Context.Shared.Allow && !asThis)
1,459,744✔
1718
            {
801,740✔
1719
                // 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.
1720
                if (!canBeShared)
801,740✔
1721
                {
120✔
1722
                    // If shared, make sure our type is appropriate for sharing
1723
                    // this really needs the recorder name and the field name too
1724
                    Dbg.Wrn($"Value type `{fieldType}` tagged as Shared in recorder, this is meaningless but harmless");
120✔
1725
                }
120✔
1726
            }
801,740✔
1727

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

1737
                var rootType = value.GetType().GetDecRootType();
3,870✔
1738
                if (!rootType.IsAssignableFrom(fieldType))
3,870✔
1739
                {
30✔
1740
                    // The user has a Dec.Dec or similar, and it has a Dec assigned to it.
1741
                    // 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.
1742
                    // But we're OK with that, honestly. We just do that.
1743
                    // If you're saving something like this you don't get to rename Dec classes later on, but, hey, deal with it.
1744
                    // 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.
1745
                    node.TagClass(rootType);
30✔
1746
                }
30✔
1747

1748
                node.WriteDec(value as Dec);
3,870✔
1749

1750
                return;
3,870✔
1751
            }
1752

1753
            // Everything represents "null" with an explicit XML tag, so let's just do that
1754
            // Maybe at some point we want to special-case this for the empty Dec link
1755
            if (value == null)
1,455,874✔
1756
            {
366,392✔
1757
                if (typeof(Dec).IsAssignableFrom(fieldType))
366,392✔
1758
                {
33,950✔
1759
                    node.WriteDec(null);
33,950✔
1760
                }
33,950✔
1761
                else
1762
                {
332,442✔
1763
                    node.WriteExplicitNull();
332,442✔
1764
                }
332,442✔
1765

1766
                return;
366,392✔
1767
            }
1768

1769
            var valType = value.GetType();
1,089,482✔
1770

1771
            // This is our value's type, but we may need a little bit of tinkering to make it useful.
1772
            // The current case I know of is System.RuntimeType, which appears if we call .GetType() on a Type.
1773
            // I assume there is a complicated internal reason for this; good news, we can ignore it and just pretend it's a System.Type.
1774
            // Bad news: it's actually really hard to detect this case because System.RuntimeType is private.
1775
            // That's why we have the annoying `static` up above.
1776
            if (valType == TypeSystemRuntimeType)
1,089,482✔
1777
            {
255✔
1778
                valType = typeof(Type);
255✔
1779
            }
255✔
1780

1781
            // Do all our unreferencables first
1782
            bool unreferenceableComplete = false;
1,089,482✔
1783

1784
            if (valType.IsPrimitive)
1,089,482✔
1785
            {
249,581✔
1786
                node.WritePrimitive(value);
249,581✔
1787

1788
                unreferenceableComplete = true;
249,581✔
1789
            }
249,581✔
1790
            else if (value is System.Enum)
839,901✔
1791
            {
305✔
1792
                node.WriteEnum(value);
305✔
1793

1794
                unreferenceableComplete = true;
305✔
1795
            }
305✔
1796
            else if (value is string)
839,596✔
1797
            {
27,520✔
1798
                node.WriteString(value as string);
27,520✔
1799

1800
                unreferenceableComplete = true;
27,520✔
1801
            }
27,520✔
1802
            else if (value is Type)
812,076✔
1803
            {
255✔
1804
                node.WriteType(value as Type);
255✔
1805

1806
                unreferenceableComplete = true;
255✔
1807
            }
255✔
1808

1809
            // Check to see if we should make this into a ref (yes, even if we're not tagged as Shared)
1810
            // Do this *before* we do the class tagging, otherwise we may add ref/class tags to a single node, which is invalid.
1811
            // 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.
1812
            if (Util.CanBeShared(valType) && !asThis)
1,089,482✔
1813
            {
782,601✔
1814
                if (node.WriteReference(value))
782,601✔
1815
                {
150,755✔
1816
                    // The ref system has set up the appropriate tagging, so we're done!
1817
                    return;
150,755✔
1818
                }
1819

1820
                // If we support references, then this object has not previously shown up in the reference system; keep going so we finish serializing it.
1821
                // If we don't support references at all then obviously we *really* need to finish serializing it.
1822
            }
631,846✔
1823

1824
            // 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`.
1825
            if (valType != fieldType)
938,727✔
1826
            {
1,196✔
1827
                if (asThis)
1,196✔
1828
                {
20✔
1829
                    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✔
1830
                    // . . . I guess we just keep going?
1831
                }
20✔
1832
                else
1833
                {
1,176✔
1834
                    node.TagClass(valType);
1,176✔
1835
                }
1,176✔
1836
            }
1,196✔
1837

1838
            // Did we actually write our node type? Alright, we're done.
1839
            if (unreferenceableComplete)
938,727✔
1840
            {
277,661✔
1841
                return;
277,661✔
1842
            }
1843

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

1846
            if (node.AllowCloning && valType.GetCustomAttribute<CloneWithAssignmentAttribute>() != null)
661,066✔
1847
            {
20✔
1848
                node.WriteCloneCopy(value);
20✔
1849

1850
                return;
20✔
1851
            }
1852

1853
            if (valType.IsArray)
661,046✔
1854
            {
2,630✔
1855
                node.WriteArray(value as Array);
2,630✔
1856

1857
                return;
2,630✔
1858
            }
1859

1860
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(List<>))
658,416✔
1861
            {
26,485✔
1862
                node.WriteList(value as IList);
26,485✔
1863

1864
                return;
26,485✔
1865
            }
1866

1867
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
631,931✔
1868
            {
11,795✔
1869
                node.WriteDictionary(value as IDictionary);
11,795✔
1870

1871
                return;
11,795✔
1872
            }
1873

1874
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(HashSet<>))
620,136✔
1875
            {
590✔
1876
                node.WriteHashSet(value as IEnumerable);
590✔
1877

1878
                return;
590✔
1879
            }
1880

1881
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(Queue<>))
619,546✔
1882
            {
50✔
1883
                node.WriteQueue(value as IEnumerable);
50✔
1884

1885
                return;
50✔
1886
            }
1887

1888
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(Stack<>))
619,496✔
1889
            {
50✔
1890
                node.WriteStack(value as IEnumerable);
50✔
1891

1892
                return;
50✔
1893
            }
1894

1895
            if (valType.IsGenericType && (
619,446✔
1896
                    valType.GetGenericTypeDefinition() == typeof(Tuple<>) ||
619,446✔
1897
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,>) ||
619,446✔
1898
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,>) ||
619,446✔
1899
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,>) ||
619,446✔
1900
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,>) ||
619,446✔
1901
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,,>) ||
619,446✔
1902
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,>) ||
619,446✔
1903
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,,>)
619,446✔
1904
                ))
619,446✔
1905
            {
215✔
1906
                node.WriteTuple(value, fieldInfo?.GetCustomAttribute<System.Runtime.CompilerServices.TupleElementNamesAttribute>());
215✔
1907

1908
                return;
215✔
1909
            }
1910

1911
            if (valType.IsGenericType && (
619,231✔
1912
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<>) ||
619,231✔
1913
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,>) ||
619,231✔
1914
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,>) ||
619,231✔
1915
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,>) ||
619,231✔
1916
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,>) ||
619,231✔
1917
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,>) ||
619,231✔
1918
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,>) ||
619,231✔
1919
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,,>)
619,231✔
1920
                ))
619,231✔
1921
            {
385✔
1922
                node.WriteValueTuple(value, fieldInfo?.GetCustomAttribute<System.Runtime.CompilerServices.TupleElementNamesAttribute>());
385✔
1923

1924
                return;
385✔
1925
            }
1926

1927
            if (value is IRecordable
618,846✔
1928
                && (!(value is IConditionalRecordable) || (value as IConditionalRecordable).ShouldRecord(node.UserSettings)))
618,846✔
1929
            {
562,800✔
1930
                node.WriteRecord(value as IRecordable);
562,800✔
1931

1932
                return;
562,800✔
1933
            }
1934

1935
            {
56,046✔
1936
                // Look for a converter; that's the only way to handle this before we fall back to reflection
1937
                var converter = Serialization.ConverterFor(valType);
56,046✔
1938
                if (converter != null)
56,046✔
1939
                {
595✔
1940
                    node.WriteConvertible(converter, value);
595✔
1941
                    return;
595✔
1942
                }
1943
            }
55,451✔
1944

1945
            if (!node.AllowReflection)
55,451✔
1946
            {
45✔
1947
                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✔
1948
                node.WriteError();
45✔
1949
                return;
45✔
1950
            }
1951

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

1954
            foreach (var field in valType.GetSerializableFieldsFromHierarchy())
758,576✔
1955
            {
296,179✔
1956
                ComposeElement(node.CreateReflectionChild(field, node.RecorderContext), field.GetValue(value), field.FieldType, fieldInfo: field);
296,179✔
1957
            }
296,179✔
1958

1959
            return;
55,406✔
1960
        }
1,459,744✔
1961

1962
        internal static void Clear()
1963
        {
26,225✔
1964
            ConverterInitialized = false;
26,225✔
1965
            ConverterObjects = new System.Collections.Concurrent.ConcurrentDictionary<Type, Converter>();
26,225✔
1966
            ConverterGenericPrototypes = new System.Collections.Concurrent.ConcurrentDictionary<Type, Type>();
26,225✔
1967
        }
26,225✔
1968
    }
1969
}
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