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

zorbathut / dec / 11670896818

04 Nov 2024 07:02PM UTC coverage: 90.556%. Remained the same
11670896818

push

github

zorbathut
Removed redundant namespaces

4564 of 5040 relevant lines covered (90.56%)

191694.1 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

158
        internal static Converter ConverterFor(Type inputType)
159
        {
5,335,426✔
160
            if (ConverterObjects.TryGetValue(inputType, out var converter))
5,335,426✔
161
            {
5,290,179✔
162
                return converter;
5,290,179✔
163
            }
164

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

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

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

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

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

220
            var factoriedConverter = Config.ConverterFactory?.Invoke(inputType);
45,122✔
221
            ConverterObjects[inputType] = factoriedConverter;   // cache this so we don't generate a million of them
45,122✔
222
            return factoriedConverter;
45,122✔
223
        }
5,335,426✔
224

225

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

391
                return ParseMode.Default;
840✔
392
            }
393
        }
3,114,893✔
394

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

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

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

416
                ParseCommand s_parseCommand;
417

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

596
            return orders;
45,150✔
597
        }
45,150✔
598

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

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

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

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

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

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

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

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

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

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

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

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

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

750
            var converter = ConverterFor(type);
1,555,584✔
751

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

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

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

780
                if (recContext.shared == Recorder.Context.Shared.Deny)
357,160✔
781
                {
55✔
782
                    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✔
783
                }
55✔
784

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

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

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

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

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

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

821
            // Basic early validation
822

823
            if (hasChildren && hasText)
948,822✔
824
            {
15✔
825
                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✔
826

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

830
            if (typeof(Dec).IsAssignableFrom(type) && hasChildren && !isRootDec)
948,822✔
831
            {
×
832
                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.");
×
833
                return null;
×
834
            }
835

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

985
                return result;
605✔
986
            }
987

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

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

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

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

1018
                return result;
397,386✔
1019
            }
1020

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1121
                return result;
46,120✔
1122
            }
1123

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

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

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

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

1169
                            break;
2,450✔
1170
                        }
1171

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

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

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

1215
                            break;
35✔
1216
                        }
1217

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

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

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

1229
                return result;
2,485✔
1230
            }
1231

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

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

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

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

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

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

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

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

1277
                return result;
23,380✔
1278
            }
1279

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

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

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

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

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

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

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

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

1327
                return result;
985✔
1328
            }
1329

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

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

1351
                        case ParseCommand.Append:
1352
                            break;
×
1353

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

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

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

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

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

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

1370
                return result;
55✔
1371
            }
1372

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

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

1394
                        case ParseCommand.Append:
1395
                            break;
×
1396

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

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

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

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

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

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

1413
                return result;
55✔
1414
            }
1415

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

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

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

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

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

1459
                return result;
1,065✔
1460
            }
1461

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

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

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

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

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

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

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

1517
                node.ParseReflection(result, context, recContext);
89,541✔
1518
            }
89,541✔
1519

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

1523
            return result;
87,861✔
1524
        }
1,555,584✔
1525

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

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

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

1573
                return result;
45✔
1574
            }
1575

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1749
                return;
3,870✔
1750
            }
1751

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

1765
                return;
366,392✔
1766
            }
1767

1768
            var valType = value.GetType();
1,089,497✔
1769

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

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

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

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

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

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

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

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

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

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

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

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

1845
            if (node.AllowCloning && UtilType.CanBeCloneCopied(valType))
661,091✔
1846
            {
20✔
1847
                node.WriteCloneCopy(value);
20✔
1848

1849
                return;
20✔
1850
            }
1851

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

1856
                return;
2,630✔
1857
            }
1858

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

1863
                return;
26,485✔
1864
            }
1865

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

1870
                return;
11,810✔
1871
            }
1872

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

1877
                return;
590✔
1878
            }
1879

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

1884
                return;
50✔
1885
            }
1886

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

1891
                return;
50✔
1892
            }
1893

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

1907
                return;
215✔
1908
            }
1909

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

1923
                return;
385✔
1924
            }
1925

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

1931
                return;
562,810✔
1932
            }
1933

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

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

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

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

1958
            return;
55,406✔
1959
        }
1,459,759✔
1960

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