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

zorbathut / dec / 11674610626

04 Nov 2024 11:37PM UTC coverage: 90.088% (-0.5%) from 90.556%
11674610626

push

github

zorbathut
Rig up the first attempt at usable path data.

4617 of 5125 relevant lines covered (90.09%)

191115.08 hits per line

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

87.97
/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 element;
20
        internal Path path;
21

22
        internal InputContext(string filename = null, System.Xml.Linq.XElement element = null, Path path = null)
23
        {
3,994,185✔
24
            this.filename = filename;
3,994,185✔
25
            this.element = element;
3,994,185✔
26
            this.path = path;
3,994,185✔
27
        }
3,994,185✔
28

29
        public override string ToString()
30
        {
5,335✔
31
            if (this.element != null)
5,335✔
32
            {
5,135✔
33
                return $"{filename}:{element.LineNumber()}";
5,135✔
34
            }
35
            else if (filename != null)
200✔
36
            {
200✔
37
                return filename;
200✔
38
            }
39
            else if (path != null)
×
40
            {
×
41
                return path.ToString();
×
42
            }
43
            else
44
            {
×
45
                // shrug
46
                return "";
×
47
            }
48
        }
5,335✔
49
    }
50

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

62
        internal class ConverterNullableString<T> : ConverterString<T?> where T : struct
63
        {
64
            private ConverterString<T> child;
65

66
            public ConverterNullableString(ConverterString<T> child)
25✔
67
            {
25✔
68
                this.child = child;
25✔
69
            }
25✔
70

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

79
                return child.Write(input.Value);
×
80
            }
×
81

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

89
        internal class ConverterNullableRecord<T> : ConverterRecord<T?> where T : struct
90
        {
91
            private ConverterRecord<T> child;
92

93
            public ConverterNullableRecord(ConverterRecord<T> child)
25✔
94
            {
25✔
95
                this.child = child;
25✔
96
            }
25✔
97

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

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

124
        internal class ConverterNullableFactory<T> : ConverterFactory<T?> where T : struct
125
        {
126
            private ConverterFactory<T> child;
127

128
            public ConverterNullableFactory(ConverterFactory<T> child)
25✔
129
            {
25✔
130
                this.child = child;
25✔
131
            }
25✔
132

133
            public override T? Create(Recorder recorder)
134
            {
15✔
135
                return child.Create(recorder);
15✔
136
            }
15✔
137

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

146
                var value = input.Value;
15✔
147
                child.Read(ref value, recorder);
15✔
148
                input = value;
15✔
149
            }
15✔
150

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

159
                child.Write(input.Value, recorder);
×
160
            }
×
161
        }
162

163
        internal static Converter ConverterFor(Type inputType)
164
        {
5,335,426✔
165
            if (ConverterObjects.TryGetValue(inputType, out var converter))
5,335,426✔
166
            {
5,290,179✔
167
                return converter;
5,290,179✔
168
            }
169

170
            // check for Nullable
171
            if (inputType.IsConstructedGenericType && inputType.GetGenericTypeDefinition() == typeof(Nullable<>))
45,247✔
172
            {
200✔
173
                var nullableType = inputType.GenericTypeArguments[0];
200✔
174

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

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

213
                    // yes, do this even if it's null
214
                    ConverterObjects[inputType] = converter;
50✔
215

216
                    return converter;
50✔
217
                }
218
                else
219
                {
8,197✔
220
                    // stub it out so we can do the fast path next time
221
                    ConverterObjects[inputType] = null;
8,197✔
222
                }
8,197✔
223
            }
8,197✔
224

225
            var factoriedConverter = Config.ConverterFactory?.Invoke(inputType);
45,122✔
226
            ConverterObjects[inputType] = factoriedConverter;   // cache this so we don't generate a million of them
45,122✔
227
            return factoriedConverter;
45,122✔
228
        }
5,335,426✔
229

230

231
        internal static void Initialize()
232
        {
18,905✔
233
            if (ConverterInitialized)
18,905✔
234
            {
4,165✔
235
                return;
4,165✔
236
            }
237

238
            // this is here just so we don't keep thrashing if something breaks
239
            ConverterInitialized = true;
14,740✔
240

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

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

257
            foreach (var type in conversionTypes)
45,715✔
258
            {
755✔
259
                if (type.IsAbstract)
755✔
260
                {
5✔
261
                    Dbg.Err($"Found converter {type} which is abstract. This is not allowed.");
5✔
262
                    continue;
5✔
263
                }
264

265
                if (type.IsGenericType)
750✔
266
                {
30✔
267
                    var baseConverterType = type;
30✔
268
                    while (baseConverterType.BaseType != typeof(ConverterString) && baseConverterType.BaseType != typeof(ConverterRecord) && baseConverterType.BaseType != typeof(ConverterFactory))
60✔
269
                    {
30✔
270
                        baseConverterType = baseConverterType.BaseType;
30✔
271
                    }
30✔
272

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

277
                    if (!converterTarget.IsGenericType)
30✔
278
                    {
5✔
279
                        Dbg.Err($"Found generic converter {type} which is not referring to a generic constructed type.");
5✔
280
                        continue;
5✔
281
                    }
282

283
                    converterTarget = converterTarget.GetGenericTypeDefinition();
25✔
284
                    if (ConverterGenericPrototypes.ContainsKey(converterTarget))
25✔
285
                    {
×
286
                        Dbg.Err($"Found multiple converters for {converterTarget}: {ConverterGenericPrototypes[converterTarget]} and {type}");
×
287
                    }
×
288

289
                    ConverterGenericPrototypes[converterTarget] = type;
25✔
290
                    continue;
25✔
291
                }
292

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

302
                    ConverterObjects[convertedType] = converter;
715✔
303
                    continue;
715✔
304
                }
305
            }
5✔
306
        }
18,905✔
307

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

325
        internal enum ParseMode
326
        {
327
            Default,
328
            Replace,
329
            Patch,
330
            Append,
331

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

396
                return ParseMode.Default;
840✔
397
            }
398
        }
3,114,893✔
399

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

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

416
            foreach (var node in nodes)
7,557,770✔
417
            {
1,511,554✔
418
                var inputContext = node.GetInputContext();
1,511,554✔
419
                var s_parseMode = ParseModeFromString(inputContext, node.GetMetadata(ReaderNodeParseable.Metadata.Mode));
1,511,554✔
420

421
                ParseCommand s_parseCommand;
422

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

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

445
                            case ParseMode.Default:
446
                            case ParseMode.Replace:
447
                                s_parseCommand = ParseCommand.Replace;
130,895✔
448
                                break;
130,895✔
449

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

462
                            case ParseMode.Default:
463
                            case ParseMode.Replace:
464
                                s_parseCommand = ParseCommand.Replace;
35,265✔
465
                                break;
35,265✔
466

467
                            case ParseMode.Patch:
468
                                s_parseCommand = ParseCommand.Patch;
240✔
469
                                break;
240✔
470

471
                            case ParseMode.Append:
472
                                s_parseCommand = ParseCommand.Append;
160✔
473
                                break;
160✔
474
                        }
475

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

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

496
                if (s_parseCommand == ParseCommand.Replace)
1,511,554✔
497
                {
567,576✔
498
                    orders.Clear();
567,576✔
499
                }
567,576✔
500

501
                orders.Add((s_parseCommand, node));
1,511,554✔
502
            }
1,511,554✔
503

504
            return orders;
1,511,554✔
505
        }
1,511,554✔
506

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

515
                switch (s_parseMode)
46,270✔
516
                {
517
                    default:
518
                        Dbg.Err($"{item.inputContext}: Invalid mode {s_parseMode} provided for a Dec-type parse, defaulting to Create");
20✔
519
                        goto case ParseMode.Default;
20✔
520

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

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

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

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

558
                    case ParseMode.CreateOrPatch:
559
                        // doesn't matter if we have a thing or not
560
                        orders.Add(item);
80✔
561
                        everExisted = true;
80✔
562
                        break;
80✔
563

564
                    case ParseMode.CreateOrIgnore:
565
                        if (orders.Count == 0)
80✔
566
                        {
20✔
567
                            orders.Add(item);
20✔
568
                            everExisted = true;
20✔
569
                        }
20✔
570
                        break;
80✔
571

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

580
                    case ParseMode.ReplaceIfExists:
581
                        if (orders.Count != 0)
80✔
582
                        {
60✔
583
                            orders.Clear();
60✔
584
                            orders.Add(item);
60✔
585
                        }
60✔
586
                        break;
80✔
587

588
                    case ParseMode.PatchIfExists:
589
                        if (orders.Count != 0)
80✔
590
                        {
60✔
591
                            orders.Add(item);
60✔
592
                        }
60✔
593
                        break;
80✔
594

595
                    case ParseMode.DeleteIfExists:
596
                        orders.Clear();
140✔
597
                        break;
140✔
598
                }
599
            }
46,270✔
600

601
            return orders;
45,150✔
602
        }
45,150✔
603

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

612
            if (!context.allowReflection && nodes.Count > 1)
1,555,584✔
613
            {
×
614
                Dbg.Err("Internal error, multiple nodes provided for recorder-mode behavior. Please report this!");
×
615
            }
×
616

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

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

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

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

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

665
                var unrecognized = s_node.GetMetadataUnrecognized();
1,557,264✔
666
                if (unrecognized != null)
1,557,264✔
667
                {
65✔
668
                    Dbg.Err($"{s_node.GetInputContext()}: Has unknown attributes {unrecognized}");
65✔
669
                }
65✔
670
            }
1,557,264✔
671

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

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

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

728
                    // update the class based on whatever this says
729
                    string localClassAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Class);
1,557,069✔
730
                    if (localClassAttribute != null)
1,557,069✔
731
                    {
207,911✔
732
                        classAttribute = localClassAttribute;
207,911✔
733
                        classAttributeNode = s_node;
207,911✔
734
                    }
207,911✔
735
                }
1,557,069✔
736

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

755
            var converter = ConverterFor(type);
1,555,584✔
756

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

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

780
            // Actually handle our attributes
781
            if (refKey != null)
1,555,584✔
782
            {
357,160✔
783
                // Ref is the highest priority, largely because I think it's cool
784

785
                if (recContext.shared == Recorder.Context.Shared.Deny)
357,160✔
786
                {
55✔
787
                    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✔
788
                }
55✔
789

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

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

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

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

815
                return refObject;
357,010✔
816
            }
817
            else if (isNull)
1,198,424✔
818
            {
249,602✔
819
                return null;
249,602✔
820

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

826
            // Basic early validation
827

828
            if (hasChildren && hasText)
948,822✔
829
            {
15✔
830
                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✔
831

832
                // we'll just fall through and try to parse anyway, though
833
            }
15✔
834

835
            if (typeof(Dec).IsAssignableFrom(type) && hasChildren && !isRootDec)
948,822✔
836
            {
×
837
                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.");
×
838
                return null;
×
839
            }
840

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

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

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

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

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

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

890
                            case ParseCommand.Replace:
891
                                result = null;
×
892
                                break;
×
893

894
                            default:
895
                                Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
896
                                break;
×
897
                        }
898

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

901
                        if (result == null && !isNullable)
235✔
902
                        {
130✔
903
                            result = type.CreateInstanceSafe("converterrecord", node);
130✔
904
                        }
130✔
905

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

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

924
                                recorderReader.ReportUnusedFields();
220✔
925
                            }
220✔
926
                            catch (Exception e)
15✔
927
                            {
15✔
928
                                Dbg.Ex(new ConverterReadException(node.GetInputContext(), converter, e));
15✔
929

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

945
                            case ParseCommand.Replace:
946
                                result = null;
×
947
                                break;
×
948

949
                            default:
950
                                Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
951
                                break;
×
952
                        }
953

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

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

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

990
                return result;
605✔
991
            }
992

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

1010
                        default:
1011
                            Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1012
                            break;
×
1013
                    }
1014

1015
                    if (hasChildren)
397,386✔
1016
                    {
20✔
1017
                        Dbg.Err($"{node.GetInputContext()}: Child nodes are not valid when parsing {type}");
20✔
1018
                    }
20✔
1019

1020
                    result = ParseString(node.GetText(), type, result, node.GetInputContext());
397,386✔
1021
                }
397,386✔
1022

1023
                return result;
397,386✔
1024
            }
1025

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

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

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

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

1061
                            default:
1062
                                Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1063
                                break;
×
1064
                        }
1065

1066
                        if (recordable != null)
388,625✔
1067
                        {
388,605✔
1068
                            var recorderReader = new RecorderReader(node, context, trackUsage: true);
388,605✔
1069
                            recordable.Record(recorderReader);
388,605✔
1070
                            recorderReader.ReportUnusedFields();
388,605✔
1071

1072
                            // TODO: support indices if this is within the Dec system?
1073
                        }
388,605✔
1074
                    }
388,625✔
1075

1076
                    result = recordable;
388,625✔
1077
                    return result;
388,625✔
1078
                }
1079

1080
                // otherwise we just fall through
1081
            }
15✔
1082

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

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

1107
                        case ParseCommand.Append:
1108
                            // we're good
1109
                            break;
80✔
1110

1111
                        default:
1112
                            Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1113
                            break;
×
1114
                    }
1115

1116
                    // List<> handling
1117
                    Type referencedType = type.GetGenericArguments()[0];
46,120✔
1118

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

1121
                    node.ParseList(list, referencedType, context, recContext);
46,120✔
1122

1123
                    result = list;
46,120✔
1124
                }
46,120✔
1125

1126
                return result;
46,120✔
1127
            }
1128

1129
            // Special case: Arrays
1130
            if (type.IsArray)
116,006✔
1131
            {
2,485✔
1132
                Type referencedType = type.GetElementType();
2,485✔
1133

1134
                foreach (var (parseCommand, node) in orders)
12,425✔
1135
                {
2,485✔
1136
                    Array array = null;
2,485✔
1137
                    int startOffset = 0;
2,485✔
1138

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

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

1174
                            break;
2,450✔
1175
                        }
1176

1177
                        case ParseCommand.Append:
1178
                        {
55✔
1179
                            if (result == null)
55✔
1180
                            {
20✔
1181
                                goto case ParseCommand.Replace;
20✔
1182
                            }
1183

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

1214
                                {
15✔
1215
                                    int[] indices = new int[arrayDimensions.Length];
15✔
1216
                                    CopyArray(oldArray, array, indices, 0);
15✔
1217
                                }
15✔
1218
                            }
15✔
1219

1220
                            break;
35✔
1221
                        }
1222

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

1229
                    node.ParseArray(array, referencedType, context, recContext, startOffset);
2,485✔
1230

1231
                    result = array;
2,485✔
1232
                }
2,485✔
1233

1234
                return result;
2,485✔
1235
            }
1236

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

1255
                        case ParseCommand.Patch:
1256
                            if (original != null)
160✔
1257
                            {
140✔
1258
                                permitPatch = true;
140✔
1259
                            }
140✔
1260
                            break;
160✔
1261

1262
                        case ParseCommand.Append:
1263
                            // nothing needs to be done, our existing dupe checking will solve it
1264
                            break;
80✔
1265

1266
                        default:
1267
                            Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1268
                            break;
×
1269
                    }
1270

1271
                    // Dictionary<> handling
1272
                    Type keyType = type.GetGenericArguments()[0];
23,380✔
1273
                    Type valueType = type.GetGenericArguments()[1];
23,380✔
1274

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

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

1279
                    result = dict;
23,380✔
1280
                }
23,380✔
1281

1282
                return result;
23,380✔
1283
            }
1284

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

1307
                        case ParseCommand.Patch:
1308
                            if (original != null)
80✔
1309
                            {
60✔
1310
                                permitPatch = true;
60✔
1311
                            }
60✔
1312
                            break;
80✔
1313

1314
                        case ParseCommand.Append:
1315
                            // nothing needs to be done, our existing dupe checking will solve it
1316
                            break;
80✔
1317

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

1323
                    Type keyType = type.GetGenericArguments()[0];
985✔
1324

1325
                    var set = result ?? Activator.CreateInstance(type);
985✔
1326

1327
                    node.ParseHashset(set, keyType, context, recContext, permitPatch);
985✔
1328

1329
                    result = set;
985✔
1330
                }
985✔
1331

1332
                return result;
985✔
1333
            }
1334

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

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

1356
                        case ParseCommand.Append:
1357
                            break;
×
1358

1359
                        // There definitely starts being an argument for prepend.
1360

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

1366
                    Type keyType = type.GetGenericArguments()[0];
55✔
1367

1368
                    var set = result ?? Activator.CreateInstance(type);
55✔
1369

1370
                    node.ParseStack(set, keyType, context, recContext);
55✔
1371

1372
                    result = set;
55✔
1373
                }
55✔
1374

1375
                return result;
55✔
1376
            }
1377

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

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

1399
                        case ParseCommand.Append:
1400
                            break;
×
1401

1402
                        // There definitely starts being an argument for prepend.
1403

1404
                        default:
1405
                            Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1406
                            break;
×
1407
                    }
1408

1409
                    Type keyType = type.GetGenericArguments()[0];
55✔
1410

1411
                    var set = result ?? Activator.CreateInstance(type);
55✔
1412

1413
                    node.ParseQueue(set, keyType, context, recContext);
55✔
1414

1415
                    result = set;
55✔
1416
                }
55✔
1417

1418
                return result;
55✔
1419
            }
1420

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

1450
                        default:
1451
                            Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1452
                            break;
×
1453
                    }
1454

1455
                    int expectedCount = type.GenericTypeArguments.Length;
1,065✔
1456
                    object[] parameters = new object[expectedCount];
1,065✔
1457

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

1460
                    // construct!
1461
                    result = Activator.CreateInstance(type, parameters);
1,065✔
1462
                }
1,065✔
1463

1464
                return result;
1,065✔
1465
            }
1466

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

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

1481
            foreach (var (parseCommand, node) in orders)
442,985✔
1482
            {
89,621✔
1483
                if (!isRootDec)
89,621✔
1484
                {
44,021✔
1485
                    switch (parseCommand)
44,021✔
1486
                    {
1487
                        case ParseCommand.Patch:
1488
                            // easy, done
1489
                            break;
44,021✔
1490

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

1504
                // If we haven't been given a generic class from our parent, go ahead and init to defaults
1505
                if (result == null && recordableBuffered != null)
89,621✔
1506
                {
15✔
1507
                    result = recordableBuffered;
15✔
1508
                }
15✔
1509

1510
                if (result == null)
89,621✔
1511
                {
17,640✔
1512
                    // okay fine
1513
                    result = type.CreateInstanceSafe("object", node);
17,640✔
1514

1515
                    if (result == null)
17,640✔
1516
                    {
80✔
1517
                        // error already reported
1518
                        return result;
80✔
1519
                    }
1520
                }
17,560✔
1521

1522
                node.ParseReflection(result, context, recContext);
89,541✔
1523
            }
89,541✔
1524

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

1528
            return result;
87,861✔
1529
        }
1,555,584✔
1530

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

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

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

1578
                return result;
45✔
1579
            }
1580

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

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

1605
                        // If we're an invalid name, we already spat out the error
1606
                    }
85✔
1607
                    return result;
7,510✔
1608
                }
1609
            }
1610

1611
            // Special case: types
1612
            if (type == typeof(Type))
765,762✔
1613
            {
415,056✔
1614
                if (text == "")
415,056✔
1615
                {
×
1616
                    return null;
×
1617
                }
1618

1619
                return UtilType.ParseDecFormatted(text, context);
415,056✔
1620
            }
1621

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

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

1641
                        if (String.Compare(text, "-infinity", true) == 0)
28,120✔
1642
                        {
40✔
1643
                            return float.NegativeInfinity;
40✔
1644
                        }
1645

1646
                        if (text.StartsWith("nanbox", StringComparison.CurrentCultureIgnoreCase))
28,080✔
1647
                        {
85✔
1648
                            const int expectedFloatSize = 6 + 8;
1649

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

1656
                            int number = Convert.ToInt32(text.Substring(6), 16);
85✔
1657
                            return BitConverter.Int32BitsToSingle(number);
85✔
1658
                        }
1659
                    }
27,995✔
1660

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

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

1674
                        if (String.Compare(text, "-infinity", true) == 0)
985✔
1675
                        {
40✔
1676
                            return double.NegativeInfinity;
40✔
1677
                        }
1678

1679
                        if (text.StartsWith("nanbox", StringComparison.CurrentCultureIgnoreCase))
945✔
1680
                        {
75✔
1681
                            const int expectedDoubleSize = 6 + 16;
1682

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

1689
                            long number = Convert.ToInt64(text.Substring(6), 16);
75✔
1690
                            return BitConverter.Int64BitsToDouble(number);
75✔
1691
                        }
1692
                    }
870✔
1693

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

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

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

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

1752
                node.WriteDec(value as Dec);
3,870✔
1753

1754
                return;
3,870✔
1755
            }
1756

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

1770
                return;
366,392✔
1771
            }
1772

1773
            var valType = value.GetType();
1,089,497✔
1774

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

1785
            // Do all our unreferencables first
1786
            bool unreferenceableComplete = false;
1,089,497✔
1787

1788
            if (valType.IsPrimitive)
1,089,497✔
1789
            {
249,581✔
1790
                node.WritePrimitive(value);
249,581✔
1791

1792
                unreferenceableComplete = true;
249,581✔
1793
            }
249,581✔
1794
            else if (value is System.Enum)
839,916✔
1795
            {
305✔
1796
                node.WriteEnum(value);
305✔
1797

1798
                unreferenceableComplete = true;
305✔
1799
            }
305✔
1800
            else if (value is string)
839,611✔
1801
            {
27,510✔
1802
                node.WriteString(value as string);
27,510✔
1803

1804
                unreferenceableComplete = true;
27,510✔
1805
            }
27,510✔
1806
            else if (value is Type)
812,101✔
1807
            {
255✔
1808
                node.WriteType(value as Type);
255✔
1809

1810
                unreferenceableComplete = true;
255✔
1811
            }
255✔
1812

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

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

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

1842
            // Did we actually write our node type? Alright, we're done.
1843
            if (unreferenceableComplete)
938,742✔
1844
            {
277,651✔
1845
                return;
277,651✔
1846
            }
1847

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

1850
            if (node.AllowCloning && UtilType.CanBeCloneCopied(valType))
661,091✔
1851
            {
20✔
1852
                node.WriteCloneCopy(value);
20✔
1853

1854
                return;
20✔
1855
            }
1856

1857
            if (valType.IsArray)
661,071✔
1858
            {
2,630✔
1859
                node.WriteArray(value as Array);
2,630✔
1860

1861
                return;
2,630✔
1862
            }
1863

1864
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(List<>))
658,441✔
1865
            {
26,485✔
1866
                node.WriteList(value as IList);
26,485✔
1867

1868
                return;
26,485✔
1869
            }
1870

1871
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
631,956✔
1872
            {
11,810✔
1873
                node.WriteDictionary(value as IDictionary);
11,810✔
1874

1875
                return;
11,810✔
1876
            }
1877

1878
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(HashSet<>))
620,146✔
1879
            {
590✔
1880
                node.WriteHashSet(value as IEnumerable);
590✔
1881

1882
                return;
590✔
1883
            }
1884

1885
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(Queue<>))
619,556✔
1886
            {
50✔
1887
                node.WriteQueue(value as IEnumerable);
50✔
1888

1889
                return;
50✔
1890
            }
1891

1892
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(Stack<>))
619,506✔
1893
            {
50✔
1894
                node.WriteStack(value as IEnumerable);
50✔
1895

1896
                return;
50✔
1897
            }
1898

1899
            if (valType.IsGenericType && (
619,456✔
1900
                    valType.GetGenericTypeDefinition() == typeof(Tuple<>) ||
619,456✔
1901
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,>) ||
619,456✔
1902
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,>) ||
619,456✔
1903
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,>) ||
619,456✔
1904
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,>) ||
619,456✔
1905
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,,>) ||
619,456✔
1906
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,>) ||
619,456✔
1907
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,,>)
619,456✔
1908
                ))
619,456✔
1909
            {
215✔
1910
                node.WriteTuple(value, fieldInfo?.GetCustomAttribute<System.Runtime.CompilerServices.TupleElementNamesAttribute>());
215✔
1911

1912
                return;
215✔
1913
            }
1914

1915
            if (valType.IsGenericType && (
619,241✔
1916
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<>) ||
619,241✔
1917
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,>) ||
619,241✔
1918
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,>) ||
619,241✔
1919
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,>) ||
619,241✔
1920
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,>) ||
619,241✔
1921
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,>) ||
619,241✔
1922
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,>) ||
619,241✔
1923
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,,>)
619,241✔
1924
                ))
619,241✔
1925
            {
385✔
1926
                node.WriteValueTuple(value, fieldInfo?.GetCustomAttribute<System.Runtime.CompilerServices.TupleElementNamesAttribute>());
385✔
1927

1928
                return;
385✔
1929
            }
1930

1931
            if (value is IRecordable
618,856✔
1932
                && (!(value is IConditionalRecordable) || (value as IConditionalRecordable).ShouldRecord(node.UserSettings)))
618,856✔
1933
            {
562,810✔
1934
                node.WriteRecord(value as IRecordable);
562,810✔
1935

1936
                return;
562,810✔
1937
            }
1938

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

1949
            if (!node.AllowReflection)
55,451✔
1950
            {
45✔
1951
                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✔
1952
                node.WriteError();
45✔
1953
                return;
45✔
1954
            }
1955

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

1958
            foreach (var field in valType.GetSerializableFieldsFromHierarchy())
758,576✔
1959
            {
296,179✔
1960
                ComposeElement(node.CreateReflectionChild(field, node.RecorderContext), field.GetValue(value), field.FieldType, fieldInfo: field);
296,179✔
1961
            }
296,179✔
1962

1963
            return;
55,406✔
1964
        }
1,459,759✔
1965

1966
        internal static void Clear()
1967
        {
26,325✔
1968
            ConverterInitialized = false;
26,325✔
1969
            ConverterObjects = new System.Collections.Concurrent.ConcurrentDictionary<Type, Converter>();
26,325✔
1970
            ConverterGenericPrototypes = new System.Collections.Concurrent.ConcurrentDictionary<Type, Type>();
26,325✔
1971
        }
26,325✔
1972
    }
1973
}
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