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

zorbathut / dec / 10467590168

20 Aug 2024 07:57AM UTC coverage: 91.08%. Remained the same
10467590168

push

github

zorbathut
Fix: Decs that are also IRecordable cause errors when referenced in other Decs.

4411 of 4843 relevant lines covered (91.08%)

198122.79 hits per line

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

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

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

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

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

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

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

58
        internal static Converter ConverterFor(Type inputType)
59
        {
5,333,171✔
60
            if (ConverterObjects.TryGetValue(inputType, out var converter))
5,333,171✔
61
            {
5,288,779✔
62
                return converter;
5,288,779✔
63
            }
64

65
            if (inputType.IsConstructedGenericType)
44,392✔
66
            {
8,087✔
67
                var genericType = inputType.GetGenericTypeDefinition();
8,087✔
68
                if (ConverterGenericPrototypes.TryGetValue(genericType, out var converterType))
8,087✔
69
                {
50✔
70
                    // construct `prototype` with the same generic arguments that `type` has
71
                    var concreteConverterType = converterType.MakeGenericType(inputType.GenericTypeArguments);
50✔
72
                    converter = (Converter)concreteConverterType.CreateInstanceSafe("converter", null);
50✔
73

74
                    // yes, do this even if it's null
75
                    ConverterObjects[inputType] = converter;
50✔
76

77
                    return converter;
50✔
78
                }
79
                else
80
                {
8,037✔
81
                    // stub it out so we can do the fast path next time
82
                    ConverterObjects[inputType] = null;
8,037✔
83
                }
8,037✔
84
            }
8,037✔
85

86
            var factoriedConverter = Config.ConverterFactory?.Invoke(inputType);
44,342✔
87
            ConverterObjects[inputType] = factoriedConverter;   // cache this so we don't generate a million of them
44,342✔
88
            return factoriedConverter;
44,342✔
89
        }
5,333,171✔
90

91

92
        internal static void Initialize()
93
        {
18,695✔
94
            if (ConverterInitialized)
18,695✔
95
            {
4,140✔
96
                return;
4,140✔
97
            }
98

99
            // this is here just so we don't keep thrashing if something breaks
100
            ConverterInitialized = true;
14,555✔
101

102
            ConverterObjects = new System.Collections.Concurrent.ConcurrentDictionary<Type, Converter>();
14,555✔
103

104
            IEnumerable<Type> conversionTypes;
105
            if (Config.TestParameters == null)
14,555✔
106
            {
5✔
107
                conversionTypes = UtilReflection.GetAllUserTypes().Where(t => t.IsSubclassOf(typeof(Converter)));
7,110✔
108
            }
5✔
109
            else if (Config.TestParameters.explicitConverters != null)
14,550✔
110
            {
2,590✔
111
                conversionTypes = Config.TestParameters.explicitConverters;
2,590✔
112
            }
2,590✔
113
            else
114
            {
11,960✔
115
                conversionTypes = Enumerable.Empty<Type>();
11,960✔
116
            }
11,960✔
117

118
            foreach (var type in conversionTypes)
45,010✔
119
            {
680✔
120
                if (type.IsAbstract)
680✔
121
                {
5✔
122
                    Dbg.Err($"Found converter {type} which is abstract. This is not allowed.");
5✔
123
                    continue;
5✔
124
                }
125

126
                if (type.IsGenericType)
675✔
127
                {
30✔
128
                    var baseConverterType = type;
30✔
129
                    while (baseConverterType.BaseType != typeof(ConverterString) && baseConverterType.BaseType != typeof(ConverterRecord) && baseConverterType.BaseType != typeof(ConverterFactory))
60✔
130
                    {
30✔
131
                        baseConverterType = baseConverterType.BaseType;
30✔
132
                    }
30✔
133

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

138
                    if (!converterTarget.IsGenericType)
30✔
139
                    {
5✔
140
                        Dbg.Err($"Found generic converter {type} which is not referring to a generic constructed type.");
5✔
141
                        continue;
5✔
142
                    }
143

144
                    converterTarget = converterTarget.GetGenericTypeDefinition();
25✔
145
                    if (ConverterGenericPrototypes.ContainsKey(converterTarget))
25✔
146
                    {
×
147
                        Dbg.Err($"Found multiple converters for {converterTarget}: {ConverterGenericPrototypes[converterTarget]} and {type}");
×
148
                    }
×
149

150
                    ConverterGenericPrototypes[converterTarget] = type;
25✔
151
                    continue;
25✔
152
                }
153

154
                var converter = (Converter)type.CreateInstanceSafe("converter", null);
645✔
155
                if (converter != null && (converter is ConverterString || converter is ConverterRecord || converter is ConverterFactory))
645✔
156
                {
640✔
157
                    Type convertedType = converter.GetConvertedTypeHint();
640✔
158
                    if (ConverterObjects.ContainsKey(convertedType))
640✔
159
                    {
5✔
160
                        Dbg.Err($"Found multiple converters for {convertedType}: {ConverterObjects[convertedType]} and {type}");
5✔
161
                    }
5✔
162

163
                    ConverterObjects[convertedType] = converter;
640✔
164
                    continue;
640✔
165
                }
166
            }
5✔
167
        }
18,695✔
168

169
        internal static object GenerateResultFallback(object model, Type type)
170
        {
270✔
171
            if (model != null)
270✔
172
            {
5✔
173
                return model;
5✔
174
            }
175
            else if (type.IsValueType)
265✔
176
            {
165✔
177
                // We don't need Safe here because all value types are required to have a default constructor.
178
                return Activator.CreateInstance(type);
165✔
179
            }
180
            else
181
            {
100✔
182
                return null;
100✔
183
            }
184
        }
270✔
185

186
        internal enum ParseMode
187
        {
188
            Default,
189
            Replace,
190
            Patch,
191
            Append,
192

193
            // Dec-only
194
            Create,
195
            CreateOrReplace,
196
            CreateOrPatch,
197
            CreateOrIgnore,
198
            Delete,
199
            ReplaceIfExists,
200
            PatchIfExists,
201
            DeleteIfExists,
202
        }
203
        internal static ParseMode ParseModeFromString(InputContext context, string str)
204
        {
3,113,313✔
205
            if (str == null)
3,113,313✔
206
            {
3,106,803✔
207
                return ParseMode.Default;
3,106,803✔
208
            }
209
            else if (str == "replace")
6,510✔
210
            {
1,020✔
211
                return ParseMode.Replace;
1,020✔
212
            }
213
            else if (str == "patch")
5,490✔
214
            {
2,180✔
215
                return ParseMode.Patch;
2,180✔
216
            }
217
            else if (str == "append")
3,310✔
218
            {
950✔
219
                return ParseMode.Append;
950✔
220
            }
221
            else if (str == "create")
2,360✔
222
            {
320✔
223
                return ParseMode.Create;
320✔
224
            }
225
            else if (str == "createOrReplace")
2,040✔
226
            {
180✔
227
                return ParseMode.CreateOrReplace;
180✔
228
            }
229
            else if (str == "createOrPatch")
1,860✔
230
            {
200✔
231
                return ParseMode.CreateOrPatch;
200✔
232
            }
233
            else if (str == "createOrIgnore")
1,660✔
234
            {
100✔
235
                return ParseMode.CreateOrIgnore;
100✔
236
            }
237
            else if (str == "delete")
1,560✔
238
            {
240✔
239
                return ParseMode.Delete;
240✔
240
            }
241
            else if (str == "replaceIfExists")
1,320✔
242
            {
160✔
243
                return ParseMode.ReplaceIfExists;
160✔
244
            }
245
            else if (str == "patchIfExists")
1,160✔
246
            {
180✔
247
                return ParseMode.PatchIfExists;
180✔
248
            }
249
            else if (str == "deleteIfExists")
980✔
250
            {
140✔
251
                return ParseMode.DeleteIfExists;
140✔
252
            }
253
            else
254
            {
840✔
255
                Dbg.Err($"{context}: Invalid `{str}` mode!");
840✔
256

257
                return ParseMode.Default;
840✔
258
            }
259
        }
3,113,313✔
260

261
        internal enum ParseCommand
262
        {
263
            Replace,
264
            Patch,
265
            Append,
266
        }
267
        internal static List<(ParseCommand command, ReaderNodeParseable node)> CompileOrders(UtilType.ParseModeCategory modeCategory, List<ReaderNodeParseable> nodes)
268
        {
1,510,969✔
269
            var orders = new List<(ParseCommand command, ReaderNodeParseable payload)>();
1,510,969✔
270

271
            if (modeCategory == UtilType.ParseModeCategory.Dec)
1,510,969✔
272
            {
×
273
                Dbg.Err($"Internal error: CompileOrders called with Dec mode category, this should never happen! Please report it.");
×
274
                return orders;
×
275
            }
276

277
            foreach (var node in nodes)
7,554,845✔
278
            {
1,510,969✔
279
                var inputContext = node.GetInputContext();
1,510,969✔
280
                var s_parseMode = ParseModeFromString(inputContext, node.GetMetadata(ReaderNodeParseable.Metadata.Mode));
1,510,969✔
281

282
                ParseCommand s_parseCommand;
283

284
                switch (modeCategory)
1,510,969✔
285
                {
286
                    case UtilType.ParseModeCategory.Object:
287
                        switch (s_parseMode)
943,263✔
288
                        {
289
                            default:
290
                                Dbg.Err($"{inputContext}: Invalid mode {s_parseMode} provided for an Object-type parse, defaulting to Patch");
240✔
291
                                goto case ParseMode.Default;
240✔
292

293
                            case ParseMode.Default:
294
                            case ParseMode.Patch:
295
                                s_parseCommand = ParseCommand.Patch;
943,263✔
296
                                break;
943,263✔
297
                        }
298
                        break;
943,263✔
299
                    case UtilType.ParseModeCategory.OrderedContainer:
300
                        switch (s_parseMode)
131,030✔
301
                        {
302
                            default:
303
                                Dbg.Err($"{inputContext}: Invalid mode {s_parseMode} provided for an ordered-container-type parse, defaulting to Replace");
80✔
304
                                goto case ParseMode.Default;
80✔
305

306
                            case ParseMode.Default:
307
                            case ParseMode.Replace:
308
                                s_parseCommand = ParseCommand.Replace;
130,895✔
309
                                break;
130,895✔
310

311
                            case ParseMode.Append:
312
                                s_parseCommand = ParseCommand.Append;
135✔
313
                                break;
135✔
314
                        }
315
                        break;
131,030✔
316
                    case UtilType.ParseModeCategory.UnorderedContainer:
317
                        switch (s_parseMode)
35,605✔
318
                        {
319
                            default:
320
                                Dbg.Err($"{inputContext}: Invalid mode {s_parseMode} provided for an unordered-container-type parse, defaulting to Replace");
×
321
                                goto case ParseMode.Default;
×
322

323
                            case ParseMode.Default:
324
                            case ParseMode.Replace:
325
                                s_parseCommand = ParseCommand.Replace;
35,205✔
326
                                break;
35,205✔
327

328
                            case ParseMode.Patch:
329
                                s_parseCommand = ParseCommand.Patch;
240✔
330
                                break;
240✔
331

332
                            case ParseMode.Append:
333
                                s_parseCommand = ParseCommand.Append;
160✔
334
                                break;
160✔
335
                        }
336

337
                        break;
35,605✔
338
                    case UtilType.ParseModeCategory.Value:
339
                        switch (s_parseMode)
401,071✔
340
                        {
341
                            default:
342
                                Dbg.Err($"{inputContext}: Invalid mode {s_parseMode} provided for a value-type parse, defaulting to Replace");
80✔
343
                                goto case ParseMode.Default;
80✔
344

345
                            case ParseMode.Default:
346
                            case ParseMode.Replace:
347
                                s_parseCommand = ParseCommand.Replace;
401,071✔
348
                                break;
401,071✔
349
                        }
350
                        break;
401,071✔
351
                    default:
352
                        Dbg.Err($"{inputContext}: Internal error, unknown mode category {modeCategory}, please report");
×
353
                        s_parseCommand = ParseCommand.Patch;  // . . . I guess?
×
354
                        break;
×
355
                }
356

357
                if (s_parseCommand == ParseCommand.Replace)
1,510,969✔
358
                {
567,171✔
359
                    orders.Clear();
567,171✔
360
                }
567,171✔
361

362
                orders.Add((s_parseCommand, node));
1,510,969✔
363
            }
1,510,969✔
364

365
            return orders;
1,510,969✔
366
        }
1,510,969✔
367

368
        internal static List<ReaderFileDec.ReaderDec> CompileDecOrders(List<ReaderFileDec.ReaderDec> decs)
369
        {
44,945✔
370
            var orders = new List<ReaderFileDec.ReaderDec>();
44,945✔
371
            bool everExisted = false;
44,945✔
372
            foreach (var item in decs)
226,965✔
373
            {
46,065✔
374
                var s_parseMode = ParseModeFromString(item.inputContext, item.node.GetMetadata(ReaderNodeParseable.Metadata.Mode));
46,065✔
375

376
                switch (s_parseMode)
46,065✔
377
                {
378
                    default:
379
                        Dbg.Err($"{item.inputContext}: Invalid mode {s_parseMode} provided for a Dec-type parse, defaulting to Create");
20✔
380
                        goto case ParseMode.Default;
20✔
381

382
                    case ParseMode.Default:
383
                    case ParseMode.Create:
384
                        if (orders.Count != 0)
44,945✔
385
                        {
80✔
386
                            Dbg.Err($"{item.inputContext}: Create mode used when a Dec already exists, falling back to Patch");
80✔
387
                            goto case ParseMode.Patch;
80✔
388
                        }
389
                        orders.Add(item);
44,865✔
390
                        everExisted = true;
44,865✔
391
                        break;
44,865✔
392

393
                    case ParseMode.Replace:
394
                        if (orders.Count == 0)
100✔
395
                        {
40✔
396
                            Dbg.Err($"{item.inputContext}: Replace mode used when a Dec doesn't exist, falling back to Create");
40✔
397
                            goto case ParseMode.Create;
40✔
398
                        }
399
                        orders.Clear();
60✔
400
                        orders.Add(item);
60✔
401
                        break;
60✔
402

403
                    case ParseMode.Patch:
404
                        if (orders.Count == 0)
420✔
405
                        {
60✔
406
                            Dbg.Err($"{item.inputContext}: Patch mode used when a Dec doesn't exist, falling back to Create");
60✔
407
                            goto case ParseMode.Create;
60✔
408
                        }
409
                        orders.Add(item);
360✔
410
                        break;
360✔
411

412
                    case ParseMode.CreateOrReplace:
413
                        // doesn't matter if we have a thing or not
414
                        orders.Clear();
80✔
415
                        orders.Add(item);
80✔
416
                        everExisted = true;
80✔
417
                        break;
80✔
418

419
                    case ParseMode.CreateOrPatch:
420
                        // doesn't matter if we have a thing or not
421
                        orders.Add(item);
80✔
422
                        everExisted = true;
80✔
423
                        break;
80✔
424

425
                    case ParseMode.CreateOrIgnore:
426
                        if (orders.Count == 0)
80✔
427
                        {
20✔
428
                            orders.Add(item);
20✔
429
                            everExisted = true;
20✔
430
                        }
20✔
431
                        break;
80✔
432

433
                    case ParseMode.Delete:
434
                        if (!everExisted)
240✔
435
                        {
20✔
436
                            Dbg.Err($"{item.inputContext}: Delete mode used when a Dec doesn't exist; did you want deleteIfExists?");
20✔
437
                        }
20✔
438
                        orders.Clear();
240✔
439
                        break;
240✔
440

441
                    case ParseMode.ReplaceIfExists:
442
                        if (orders.Count != 0)
80✔
443
                        {
60✔
444
                            orders.Clear();
60✔
445
                            orders.Add(item);
60✔
446
                        }
60✔
447
                        break;
80✔
448

449
                    case ParseMode.PatchIfExists:
450
                        if (orders.Count != 0)
80✔
451
                        {
60✔
452
                            orders.Add(item);
60✔
453
                        }
60✔
454
                        break;
80✔
455

456
                    case ParseMode.DeleteIfExists:
457
                        orders.Clear();
140✔
458
                        break;
140✔
459
                }
460
            }
46,065✔
461

462
            return orders;
44,945✔
463
        }
44,945✔
464

465
        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)
466
        {
1,554,794✔
467
            if (nodes == null || nodes.Count == 0)
1,554,794✔
468
            {
×
469
                Dbg.Err("Internal error, Dec failed to provide nodes to ParseElement. Please report this!");
×
470
                return original;
×
471
            }
472

473
            if (!context.allowReflection && nodes.Count > 1)
1,554,794✔
474
            {
×
475
                Dbg.Err("Internal error, multiple nodes provided for recorder-mode behavior. Please report this!");
×
476
            }
×
477

478
            // We keep the original around in case of error, but do all our manipulation on a result object.
479
            object result = original;
1,554,794✔
480

481
            // Verify our Shared flags as the *very* first step to ensure nothing gets past us.
482
            // 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
483
            if (recContext.shared == Recorder.Context.Shared.Allow)
1,554,794✔
484
            {
808,095✔
485
                if (!type.CanBeShared())
808,095✔
486
                {
100✔
487
                    // If shared, make sure our input is null and our type is appropriate for sharing
488
                    Dbg.Wrn($"{nodes[0].GetInputContext()}: Value type `{type}` tagged as Shared in recorder, this is meaningless but harmless");
100✔
489
                }
100✔
490
                else if (original != null && !hasReferenceId)
807,995✔
491
                {
25✔
492
                    // We need to create objects without context if it's shared, so we kind of panic in this case
493
                    Dbg.Err($"{nodes[0].GetInputContext()}: Shared `{type}` provided with non-null default object, this may result in unexpected behavior");
25✔
494
                }
25✔
495
            }
808,095✔
496

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

499
            // Validate all combinations here
500
            // This could definitely be more efficient and skip at least one traversal pass
501
            foreach (var s_node in nodes)
7,777,330✔
502
            {
1,556,474✔
503
                string nullAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Null);
1,556,474✔
504
                string refAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Ref);
1,556,474✔
505
                string classAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Class);
1,556,474✔
506
                string modeAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Mode);
1,556,474✔
507

508
                // Some of these are redundant and that's OK
509
                if (nullAttribute != null && (refAttribute != null || classAttribute != null || modeAttribute != null))
1,556,474✔
510
                {
180✔
511
                    Dbg.Err($"{s_node.GetInputContext()}: Null element may not have ref, class, or mode specified; guessing wildly at intentions");
180✔
512
                }
180✔
513
                else if (refAttribute != null && (nullAttribute != null || classAttribute != null || modeAttribute != null))
1,556,294✔
514
                {
75✔
515
                    Dbg.Err($"{s_node.GetInputContext()}: Ref element may not have null, class, or mode specified; guessing wildly at intentions");
75✔
516
                }
75✔
517
                else if (classAttribute != null && (nullAttribute != null || refAttribute != null))
1,556,219✔
518
                {
×
519
                    Dbg.Err($"{s_node.GetInputContext()}: Class-specified element may not have null or ref specified; guessing wildly at intentions");
×
520
                }
×
521
                else if (modeAttribute != null && (nullAttribute != null || refAttribute != null))
1,556,219✔
522
                {
×
523
                    Dbg.Err($"{s_node.GetInputContext()}: Mode-specified element may not have null or ref specified; guessing wildly at intentions");
×
524
                }
×
525

526
                var unrecognized = s_node.GetMetadataUnrecognized();
1,556,474✔
527
                if (unrecognized != null)
1,556,474✔
528
                {
65✔
529
                    Dbg.Err($"{s_node.GetInputContext()}: Has unknown attributes {unrecognized}");
65✔
530
                }
65✔
531
            }
1,556,474✔
532

533
            // Doesn't mean anything outside recorderMode, so we check it for validity just in case
534
            string refKey;
535
            ReaderNode refKeyNode = null; // stored entirely for error reporting
1,554,794✔
536
            if (!context.allowRefs)
1,554,794✔
537
            {
607,364✔
538
                refKey = null;
607,364✔
539
                foreach (var s_node in nodes)
3,040,180✔
540
                {
609,044✔
541
                    string nodeRefAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Ref);
609,044✔
542
                    if (nodeRefAttribute != null)
609,044✔
543
                    {
200✔
544
                        Dbg.Err($"{s_node.GetInputContext()}: Found a reference tag while not evaluating Recorder mode, ignoring it");
200✔
545
                    }
200✔
546
                }
609,044✔
547
            }
607,364✔
548
            else
549
            {
947,430✔
550
                (refKey, refKeyNode) = nodes.Select(node => (node.GetMetadata(ReaderNodeParseable.Metadata.Ref), node)).Where(anp => anp.Item1 != null).LastOrDefault();
2,842,290✔
551
            }
947,430✔
552

553
            // First figure out type. We actually need type to be set before we can properly analyze and validate the mode flags.
554
            // If we're in an asThis block, it refers to the outer item, not the inner item; just skip this entirely
555
            bool isNull = false;
1,554,794✔
556
            if (!asThis)
1,554,794✔
557
            {
1,554,599✔
558
                string classAttribute = null;
1,554,599✔
559
                ReaderNode classAttributeNode = null; // stored entirely for error reporting
1,554,599✔
560
                bool replaced = false;
1,554,599✔
561
                foreach (var s_node in nodes)
7,776,355✔
562
                {
1,556,279✔
563
                    // However, we do need to watch for Replace, because that means we should nuke the class attribute and start over.
564
                    string modeAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Mode);
1,556,279✔
565
                    ParseMode s_parseMode = ParseModeFromString(s_node.GetInputContext(), modeAttribute);
1,556,279✔
566
                    if (s_parseMode == ParseMode.Replace)
1,556,279✔
567
                    {
520✔
568
                        // we also should maybe be doing this if we're a list, map, or set?
569
                        classAttribute = null;
520✔
570
                        replaced = true;
520✔
571
                    }
520✔
572

573
                    // if we get nulled, we kill the class tag and basically treat it like a delete
574
                    // but we also reset the null tag on every entry
575
                    isNull = false;
1,556,279✔
576
                    string nullAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Null);
1,556,279✔
577
                    if (nullAttribute != null)
1,556,279✔
578
                    {
249,562✔
579
                        if (!bool.TryParse(nullAttribute, out bool nullValue))
249,562✔
580
                        {
×
581
                            Dbg.Err($"{s_node.GetInputContext()}: Invalid `null` attribute");
×
582
                        }
×
583
                        else if (nullValue)
249,562✔
584
                        {
249,522✔
585
                            isNull = true;
249,522✔
586
                        }
249,522✔
587
                    }
249,562✔
588

589
                    // update the class based on whatever this says
590
                    string localClassAttribute = s_node.GetMetadata(ReaderNodeParseable.Metadata.Class);
1,556,279✔
591
                    if (localClassAttribute != null)
1,556,279✔
592
                    {
207,791✔
593
                        classAttribute = localClassAttribute;
207,791✔
594
                        classAttributeNode = s_node;
207,791✔
595
                    }
207,791✔
596
                }
1,556,279✔
597

598
                if (classAttribute != null)
1,554,599✔
599
                {
207,791✔
600
                    var possibleType = (Type)ParseString(classAttribute, typeof(Type), null, classAttributeNode.GetInputContext());
207,791✔
601
                    if (!type.IsAssignableFrom(possibleType))
207,791✔
602
                    {
20✔
603
                        Dbg.Err($"{classAttributeNode.GetInputContext()}: Explicit type {classAttribute} cannot be assigned to expected type {type}");
20✔
604
                    }
20✔
605
                    else if (!replaced && result != null && result.GetType() != possibleType)
207,771✔
606
                    {
20✔
607
                        Dbg.Err($"{classAttributeNode.GetInputContext()}: Explicit type {classAttribute} does not match already-provided instance {type}");
20✔
608
                    }
20✔
609
                    else
610
                    {
207,751✔
611
                        type = possibleType;
207,751✔
612
                    }
207,751✔
613
                }
207,791✔
614
            }
1,554,599✔
615

616
            var converter = ConverterFor(type);
1,554,794✔
617

618
            // Now we traverse the Mode attributes as prep for our final parse pass.
619
            // ordersOverride makes `nodes` admittedly a little unnecessary.
620
            List<(ParseCommand command, ReaderNodeParseable node)> orders = ordersOverride ?? CompileOrders(type.CalculateSerializationModeCategory(converter, isRootDec), nodes);
1,554,794✔
621

622
            // Gather info
623
            bool hasChildren = false;
1,554,794✔
624
            ReaderNode hasChildrenNode = null;
1,554,794✔
625
            bool hasText = false;
1,554,794✔
626
            ReaderNode hasTextNode = null;
1,554,794✔
627
            foreach (var (_, node) in orders)
7,777,330✔
628
            {
1,556,474✔
629
                if (!hasChildren && node.HasChildren())
1,556,474✔
630
                {
478,986✔
631
                    hasChildren = true;
478,986✔
632
                    hasChildrenNode = node;
478,986✔
633
                }
478,986✔
634
                if (!hasText && node.GetText() != null)
1,556,474✔
635
                {
356,551✔
636
                    hasText = true;
356,551✔
637
                    hasTextNode = node;
356,551✔
638
                }
356,551✔
639
            }
1,556,474✔
640

641
            // Actually handle our attributes
642
            if (refKey != null)
1,554,794✔
643
            {
357,155✔
644
                // Ref is the highest priority, largely because I think it's cool
645

646
                if (recContext.shared == Recorder.Context.Shared.Deny)
357,155✔
647
                {
55✔
648
                    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✔
649
                }
55✔
650

651
                if (context.refs == null)
357,155✔
652
                {
40✔
653
                    Dbg.Err($"{refKeyNode.GetInputContext()}: Found a reference object {refKey} before refs are initialized (is this being used in a ConverterFactory<>.Create()?)");
40✔
654
                    return result;
40✔
655
                }
656

657
                if (!context.refs.ContainsKey(refKey))
357,115✔
658
                {
5✔
659
                    Dbg.Err($"{refKeyNode.GetInputContext()}: Found a reference object {refKey} without a valid reference mapping");
5✔
660
                    return result;
5✔
661
                }
662

663
                object refObject = context.refs[refKey];
357,110✔
664
                if (refObject == null && !type.IsValueType)
357,110✔
665
                {
80✔
666
                    // okay, good enough
667
                    return refObject;
80✔
668
                }
669

670
                if (!type.IsAssignableFrom(refObject.GetType()))
357,030✔
671
                {
25✔
672
                    Dbg.Err($"{refKeyNode.GetInputContext()}: Reference object {refKey} is of type {refObject.GetType()}, which cannot be converted to expected type {type}");
25✔
673
                    return result;
25✔
674
                }
675

676
                return refObject;
357,005✔
677
            }
678
            else if (isNull)
1,197,639✔
679
            {
249,497✔
680
                return null;
249,497✔
681

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

687
            // Basic early validation
688

689
            if (hasChildren && hasText)
948,142✔
690
            {
15✔
691
                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✔
692

693
                // we'll just fall through and try to parse anyway, though
694
            }
15✔
695

696
            if (typeof(Dec).IsAssignableFrom(type) && hasChildren && !isRootDec)
948,142✔
697
            {
×
698
                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.");
×
699
                return null;
×
700
            }
701

702
            // Defer off to converters, whatever they feel like doing
703
            if (converter != null)
948,142✔
704
            {
530✔
705
                // string converter
706
                if (converter is ConverterString converterString)
530✔
707
                {
235✔
708
                    foreach (var (parseCommand, node) in orders)
1,175✔
709
                    {
235✔
710
                        switch (parseCommand)
235✔
711
                        {
712
                            case ParseCommand.Replace:
713
                                // easy, done
714
                                break;
235✔
715

716
                            default:
717
                                Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
718
                                break;
×
719
                        }
720

721
                        if (hasChildren)
235✔
722
                        {
15✔
723
                            Dbg.Err($"{node.GetInputContext()}: String converter {converter.GetType()} called with child XML nodes, which will be ignored");
15✔
724
                        }
15✔
725

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

728
                        // context might be null; that's OK at the moment
729
                        try
730
                        {
235✔
731
                            result = converterString.ReadObj(node.GetText() ?? "", node.GetInputContext());
235✔
732
                        }
150✔
733
                        catch (Exception e)
85✔
734
                        {
85✔
735
                            Dbg.Ex(new ConverterReadException(node.GetInputContext(), converter, e));
85✔
736

737
                            result = GenerateResultFallback(result, type);
85✔
738
                        }
85✔
739
                    }
235✔
740
                }
235✔
741
                else if (converter is ConverterRecord converterRecord)
295✔
742
                {
210✔
743
                    foreach (var (parseCommand, node) in orders)
1,050✔
744
                    {
210✔
745
                        switch (parseCommand)
210✔
746
                        {
747
                            case ParseCommand.Patch:
748
                                // easy, done
749
                                break;
210✔
750

751
                            case ParseCommand.Replace:
752
                                result = null;
×
753
                                break;
×
754

755
                            default:
756
                                Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
757
                                break;
×
758
                        }
759

760
                        if (result == null)
210✔
761
                        {
120✔
762
                            result = type.CreateInstanceSafe("converterrecord", node);
120✔
763
                        }
120✔
764

765
                        // context might be null; that's OK at the moment
766
                        if (result != null)
210✔
767
                        {
210✔
768
                            var recorderReader = new RecorderReader(node, context, trackUsage: true);
210✔
769
                            try
770
                            {
210✔
771
                                object returnedResult = converterRecord.RecordObj(result, recorderReader);
210✔
772

773
                                if (!type.IsValueType && result != returnedResult)
195✔
774
                                {
×
775
                                    Dbg.Err($"{node.GetInputContext()}: Converter {converterRecord.GetType()} changed object instance, this is disallowed");
×
776
                                }
×
777
                                else
778
                                {
195✔
779
                                    // for value types, this is fine
780
                                    result = returnedResult;
195✔
781
                                }
195✔
782

783
                                recorderReader.ReportUnusedFields();
195✔
784
                            }
195✔
785
                            catch (Exception e)
15✔
786
                            {
15✔
787
                                Dbg.Ex(new ConverterReadException(node.GetInputContext(), converter, e));
15✔
788

789
                                // no fallback needed, we already have a result
790
                            }
15✔
791
                        }
210✔
792
                    }
210✔
793
                }
210✔
794
                else if (converter is ConverterFactory converterFactory)
85✔
795
                {
85✔
796
                    foreach (var (parseCommand, node) in orders)
425✔
797
                    {
85✔
798
                        switch (parseCommand)
85✔
799
                        {
800
                            case ParseCommand.Patch:
801
                                // easy, done
802
                                break;
85✔
803

804
                            case ParseCommand.Replace:
805
                                result = null;
×
806
                                break;
×
807

808
                            default:
809
                                Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
810
                                break;
×
811
                        }
812

813
                        var recorderReader = new RecorderReader(node, context, disallowShared: true, trackUsage: true);
85✔
814
                        if (result == null)
85✔
815
                        {
80✔
816
                            try
817
                            {
80✔
818
                                result = converterFactory.CreateObj(recorderReader);
80✔
819
                            }
65✔
820
                            catch (Exception e)
15✔
821
                            {
15✔
822
                                Dbg.Ex(new ConverterReadException(node.GetInputContext(), converter, e));
15✔
823
                            }
15✔
824
                        }
80✔
825

826
                        // context might be null; that's OK at the moment
827
                        if (result != null)
85✔
828
                        {
70✔
829
                            recorderReader.AllowShared(context);
70✔
830
                            try
831
                            {
70✔
832
                                result = converterFactory.ReadObj(result, recorderReader);
70✔
833
                                recorderReader.ReportUnusedFields();
55✔
834
                            }
55✔
835
                            catch (Exception e)
15✔
836
                            {
15✔
837
                                Dbg.Ex(new ConverterReadException(node.GetInputContext(), converter, e));
15✔
838

839
                                // no fallback needed, we already have a result
840
                            }
15✔
841
                        }
70✔
842
                    }
85✔
843
                }
85✔
844
                else
845
                {
×
846
                    Dbg.Err($"Somehow ended up with an unsupported converter {converter.GetType()}");
×
847
                }
×
848

849
                return result;
530✔
850
            }
851

852
            // All our standard text-using options
853
            // Placed before IRecordable just in case we have a Dec that is IRecordable
854
            if ((typeof(Dec).IsAssignableFrom(type) && !isRootDec) ||
947,612✔
855
                    type == typeof(Type) ||
947,612✔
856
                    type == typeof(string) ||
947,612✔
857
                    type.IsPrimitive ||
947,612✔
858
                    (TypeDescriptor.GetConverter(type)?.CanConvertFrom(typeof(string)) ?? false)   // this is last because it's slow
947,612✔
859
                )
947,612✔
860
            {
397,151✔
861
                foreach (var (parseCommand, node) in orders)
1,985,755✔
862
                {
397,151✔
863
                    switch (parseCommand)
397,151✔
864
                    {
865
                        case ParseCommand.Replace:
866
                            // easy, done
867
                            break;
397,151✔
868

869
                        default:
870
                            Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
871
                            break;
×
872
                    }
873

874
                    if (hasChildren)
397,151✔
875
                    {
20✔
876
                        Dbg.Err($"{node.GetInputContext()}: Child nodes are not valid when parsing {type}");
20✔
877
                    }
20✔
878

879
                    result = ParseString(node.GetText(), type, result, node.GetInputContext());
397,151✔
880
                }
397,151✔
881

882
                return result;
397,151✔
883
            }
884

885
            // Special case: IRecordables
886
            IRecordable recordableBuffered = null;
550,461✔
887
            if (typeof(IRecordable).IsAssignableFrom(type))
550,461✔
888
            {
388,535✔
889
                // we're going to need to make one anyway so let's just go ahead and do that
890
                IRecordable recordable = null;
388,535✔
891

892
                if (result != null)
388,535✔
893
                {
206,620✔
894
                    recordable = (IRecordable)result;
206,620✔
895
                }
206,620✔
896
                else if (recContext.factories == null)
181,915✔
897
                {
180,885✔
898
                    recordable = (IRecordable)type.CreateInstanceSafe("recordable", orders[0].node);
180,885✔
899
                }
180,885✔
900
                else
901
                {
1,030✔
902
                    recordable = recContext.CreateRecordableFromFactory(type, "recordable", orders[0].node);
1,030✔
903
                }
1,030✔
904

905
                // we hold on to this so that, *if* we end up not using this object, we can optionally reuse it later for reflection
906
                // 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
907
                recordableBuffered = recordable;
388,535✔
908

909
                var conditionalRecordable = recordable as IConditionalRecordable;
388,535✔
910
                if (conditionalRecordable == null || conditionalRecordable.ShouldRecord(nodes[0].UserSettings))
388,535✔
911
                {
388,520✔
912
                    foreach (var (parseCommand, node) in orders)
1,942,600✔
913
                    {
388,520✔
914
                        switch (parseCommand)
388,520✔
915
                        {
916
                            case ParseCommand.Patch:
917
                                // easy, done
918
                                break;
388,520✔
919

920
                            default:
921
                                Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
922
                                break;
×
923
                        }
924

925
                        if (recordable != null)
388,520✔
926
                        {
388,500✔
927
                            var recorderReader = new RecorderReader(node, context, trackUsage: true);
388,500✔
928
                            recordable.Record(recorderReader);
388,500✔
929
                            recorderReader.ReportUnusedFields();
388,500✔
930

931
                            // TODO: support indices if this is within the Dec system?
932
                        }
388,500✔
933
                    }
388,520✔
934

935
                    result = recordable;
388,520✔
936
                    return result;
388,520✔
937
                }
938

939
                // otherwise we just fall through
940
            }
15✔
941

942
            // Nothing past this point even supports text, so let's just get angry and break stuff.
943
            if (hasText)
161,941✔
944
            {
80✔
945
                Dbg.Err($"{hasTextNode.GetInputContext()}: Text detected in a situation where it is invalid; will be ignored");
80✔
946
                return result;
80✔
947
            }
948

949
            // Special case: Lists
950
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
161,861✔
951
            {
46,120✔
952
                foreach (var (parseCommand, node) in orders)
230,600✔
953
                {
46,120✔
954
                    switch (parseCommand)
46,120✔
955
                    {
956
                        case ParseCommand.Replace:
957
                            // 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.
958
                            // 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.
959
                            // 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.
960
                            if (result != null)
46,040✔
961
                            {
400✔
962
                                ((IList)result).Clear();
400✔
963
                            }
400✔
964
                            break;
46,040✔
965

966
                        case ParseCommand.Append:
967
                            // we're good
968
                            break;
80✔
969

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

975
                    // List<> handling
976
                    Type referencedType = type.GetGenericArguments()[0];
46,120✔
977

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

980
                    node.ParseList(list, referencedType, context, recContext);
46,120✔
981

982
                    result = list;
46,120✔
983
                }
46,120✔
984

985
                return result;
46,120✔
986
            }
987

988
            // Special case: Arrays
989
            if (type.IsArray)
115,741✔
990
            {
2,485✔
991
                Type referencedType = type.GetElementType();
2,485✔
992

993
                foreach (var (parseCommand, node) in orders)
12,425✔
994
                {
2,485✔
995
                    Array array = null;
2,485✔
996
                    int startOffset = 0;
2,485✔
997

998
                    // 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.
999
                    switch (parseCommand)
2,485✔
1000
                    {
1001
                        case ParseCommand.Replace:
1002
                        {
2,450✔
1003
                            // This is a full override, so we're going to create it here.
1004
                            // It is actually vitally important that we fall back on the model when possible, because the Recorder Ref system requires it.
1005
                            bool match = result != null && result.GetType() == type;
2,450✔
1006
                            var arrayDimensions = node.GetArrayDimensions(type.GetArrayRank());
2,450✔
1007
                            if (match)
2,450✔
1008
                            {
410✔
1009
                                array = (Array)result;
410✔
1010
                                if (array.Rank != type.GetArrayRank())
410✔
1011
                                {
×
1012
                                    match = false;
×
1013
                                }
×
1014
                                else
1015
                                {
410✔
1016
                                    for (int i = 0; i < array.Rank; i++)
1,040✔
1017
                                    {
420✔
1018
                                        if (array.GetLength(i) != arrayDimensions[i])
420✔
1019
                                        {
310✔
1020
                                            match = false;
310✔
1021
                                            break;
310✔
1022
                                        }
1023
                                    }
110✔
1024
                                }
410✔
1025
                            }
410✔
1026

1027
                            if (!match)
2,450✔
1028
                            {
2,350✔
1029
                                // Otherwise just make a new one, no harm done.
1030
                                array = Array.CreateInstance(referencedType, arrayDimensions);
2,350✔
1031
                            }
2,350✔
1032

1033
                            break;
2,450✔
1034
                        }
1035

1036
                        case ParseCommand.Append:
1037
                        {
55✔
1038
                            if (result == null)
55✔
1039
                            {
20✔
1040
                                goto case ParseCommand.Replace;
20✔
1041
                            }
1042

1043
                            // This is jankier; we create it here with the intended final length, then copy the elements over, all because arrays can't be resized
1044
                            // (yes, I know, that's the point of arrays, I'm not complaining, just . . . grumbling a little)
1045
                            var oldArray = (Array)result;
35✔
1046
                            startOffset = oldArray.Length;
35✔
1047
                            var arrayDimensions = node.GetArrayDimensions(type.GetArrayRank());
35✔
1048
                            arrayDimensions[0] += startOffset;
35✔
1049
                            array = Array.CreateInstance(referencedType, arrayDimensions);
35✔
1050
                            if (arrayDimensions.Length == 1)
35✔
1051
                            {
20✔
1052
                                oldArray.CopyTo(array, 0);
20✔
1053
                            }
20✔
1054
                            else
1055
                            {
15✔
1056
                                // oy
1057
                                void CopyArray(Array source, Array destination, int[] indices, int rank = 0)
1058
                                {
60✔
1059
                                    if (rank < source.Rank)
60✔
1060
                                    {
45✔
1061
                                        for (int i = 0; i < source.GetLength(rank); i++)
180✔
1062
                                        {
45✔
1063
                                            indices[rank] = i;
45✔
1064
                                            CopyArray(source, destination, indices, rank + 1);
45✔
1065
                                        }
45✔
1066
                                    }
45✔
1067
                                    else
1068
                                    {
15✔
1069
                                        destination.SetValue(source.GetValue(indices), indices);
15✔
1070
                                    }
15✔
1071
                                }
60✔
1072

1073
                                {
15✔
1074
                                    int[] indices = new int[arrayDimensions.Length];
15✔
1075
                                    CopyArray(oldArray, array, indices, 0);
15✔
1076
                                }
15✔
1077
                            }
15✔
1078

1079
                            break;
35✔
1080
                        }
1081

1082
                        default:
1083
                            Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1084
                            array = null; // just to break the unassigned-local-variable
×
1085
                            break;
×
1086
                    }
1087

1088
                    node.ParseArray(array, referencedType, context, recContext, startOffset);
2,485✔
1089

1090
                    result = array;
2,485✔
1091
                }
2,485✔
1092

1093
                return result;
2,485✔
1094
            }
1095

1096
            // Special case: Dictionaries
1097
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
113,256✔
1098
            {
23,320✔
1099
                foreach (var (parseCommand, node) in orders)
116,600✔
1100
                {
23,320✔
1101
                    bool permitPatch = false;
23,320✔
1102
                    switch (parseCommand)
23,320✔
1103
                    {
1104
                        case ParseCommand.Replace:
1105
                            // 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.
1106
                            // 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.
1107
                            // 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.
1108
                            if (result != null)
23,080✔
1109
                            {
485✔
1110
                                ((IDictionary)result).Clear();
485✔
1111
                            }
485✔
1112
                            break;
23,080✔
1113

1114
                        case ParseCommand.Patch:
1115
                            if (original != null)
160✔
1116
                            {
140✔
1117
                                permitPatch = true;
140✔
1118
                            }
140✔
1119
                            break;
160✔
1120

1121
                        case ParseCommand.Append:
1122
                            // nothing needs to be done, our existing dupe checking will solve it
1123
                            break;
80✔
1124

1125
                        default:
1126
                            Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1127
                            break;
×
1128
                    }
1129

1130
                    // Dictionary<> handling
1131
                    Type keyType = type.GetGenericArguments()[0];
23,320✔
1132
                    Type valueType = type.GetGenericArguments()[1];
23,320✔
1133

1134
                    var dict = (IDictionary)(result ?? Activator.CreateInstance(type));
23,320✔
1135

1136
                    node.ParseDictionary(dict, keyType, valueType, context, recContext, permitPatch);
23,320✔
1137

1138
                    result = dict;
23,320✔
1139
                }
23,320✔
1140

1141
                return result;
23,320✔
1142
            }
1143

1144
            // Special case: HashSet
1145
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(HashSet<>))
89,936✔
1146
            {
985✔
1147
                foreach (var (parseCommand, node) in orders)
4,925✔
1148
                {
985✔
1149
                    bool permitPatch = false;
985✔
1150
                    switch (parseCommand)
985✔
1151
                    {
1152
                        case ParseCommand.Replace:
1153
                            // 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.
1154
                            // 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.
1155
                            // 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.
1156
                            if (result != null)
825✔
1157
                            {
450✔
1158
                                // Did you know there's no non-generic interface that HashSet<> supports that includes a Clear function?
1159
                                // Fun fact:
1160
                                // That thing I just wrote!
1161
                                var clearFunction = result.GetType().GetMethod("Clear");
450✔
1162
                                clearFunction.Invoke(result, null);
450✔
1163
                            }
450✔
1164
                            break;
825✔
1165

1166
                        case ParseCommand.Patch:
1167
                            if (original != null)
80✔
1168
                            {
60✔
1169
                                permitPatch = true;
60✔
1170
                            }
60✔
1171
                            break;
80✔
1172

1173
                        case ParseCommand.Append:
1174
                            // nothing needs to be done, our existing dupe checking will solve it
1175
                            break;
80✔
1176

1177
                        default:
1178
                            Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1179
                            break;
×
1180
                    }
1181

1182
                    Type keyType = type.GetGenericArguments()[0];
985✔
1183

1184
                    var set = result ?? Activator.CreateInstance(type);
985✔
1185

1186
                    node.ParseHashset(set, keyType, context, recContext, permitPatch);
985✔
1187

1188
                    result = set;
985✔
1189
                }
985✔
1190

1191
                return result;
985✔
1192
            }
1193

1194
            // Special case: Stack
1195
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Stack<>))
88,951✔
1196
            {
55✔
1197
                // Stack<> handling
1198
                // Again, no sensible non-generic interface to use, so we're stuck with reflection
1199

1200
                foreach (var (parseCommand, node) in orders)
275✔
1201
                {
55✔
1202
                    switch (parseCommand)
55✔
1203
                    {
1204
                        case ParseCommand.Replace:
1205
                            // 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.
1206
                            // 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.
1207
                            // 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.
1208
                            if (result != null)
55✔
1209
                            {
5✔
1210
                                var clearFunction = result.GetType().GetMethod("Clear");
5✔
1211
                                clearFunction.Invoke(result, null);
5✔
1212
                            }
5✔
1213
                            break;
55✔
1214

1215
                        case ParseCommand.Append:
1216
                            break;
×
1217

1218
                        // There definitely starts being an argument for prepend.
1219

1220
                        default:
1221
                            Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1222
                            break;
×
1223
                    }
1224

1225
                    Type keyType = type.GetGenericArguments()[0];
55✔
1226

1227
                    var set = result ?? Activator.CreateInstance(type);
55✔
1228

1229
                    node.ParseStack(set, keyType, context, recContext);
55✔
1230

1231
                    result = set;
55✔
1232
                }
55✔
1233

1234
                return result;
55✔
1235
            }
1236

1237
            // Special case: Queue
1238
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Queue<>))
88,896✔
1239
            {
55✔
1240
                // Queue<> handling
1241
                // Again, no sensible non-generic interface to use, so we're stuck with reflection
1242

1243
                foreach (var (parseCommand, node) in orders)
275✔
1244
                {
55✔
1245
                    switch (parseCommand)
55✔
1246
                    {
1247
                        case ParseCommand.Replace:
1248
                            // 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.
1249
                            // 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.
1250
                            // 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.
1251
                            if (result != null)
55✔
1252
                            {
5✔
1253
                                var clearFunction = result.GetType().GetMethod("Clear");
5✔
1254
                                clearFunction.Invoke(result, null);
5✔
1255
                            }
5✔
1256
                            break;
55✔
1257

1258
                        case ParseCommand.Append:
1259
                            break;
×
1260

1261
                        // There definitely starts being an argument for prepend.
1262

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

1268
                    Type keyType = type.GetGenericArguments()[0];
55✔
1269

1270
                    var set = result ?? Activator.CreateInstance(type);
55✔
1271

1272
                    node.ParseQueue(set, keyType, context, recContext);
55✔
1273

1274
                    result = set;
55✔
1275
                }
55✔
1276

1277
                return result;
55✔
1278
            }
1279

1280
            // Special case: A bucket of tuples
1281
            // These are all basically identical, but AFAIK there's no good way to test them all in a better way.
1282
            if (type.IsGenericType && (
88,841✔
1283
                    type.GetGenericTypeDefinition() == typeof(Tuple<>) ||
88,841✔
1284
                    type.GetGenericTypeDefinition() == typeof(Tuple<,>) ||
88,841✔
1285
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,>) ||
88,841✔
1286
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,>) ||
88,841✔
1287
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,>) ||
88,841✔
1288
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,>) ||
88,841✔
1289
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,>) ||
88,841✔
1290
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,,>) ||
88,841✔
1291
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<>) ||
88,841✔
1292
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,>) ||
88,841✔
1293
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,>) ||
88,841✔
1294
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,>) ||
88,841✔
1295
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,>) ||
88,841✔
1296
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,>) ||
88,841✔
1297
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,>) ||
88,841✔
1298
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,,>)
88,841✔
1299
                    ))
88,841✔
1300
            {
1,065✔
1301
                foreach (var (parseCommand, node) in orders)
5,325✔
1302
                {
1,065✔
1303
                    switch (parseCommand)
1,065✔
1304
                    {
1305
                        case ParseCommand.Replace:
1306
                            // easy, done
1307
                            break;
1,065✔
1308

1309
                        default:
1310
                            Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1311
                            break;
×
1312
                    }
1313

1314
                    int expectedCount = type.GenericTypeArguments.Length;
1,065✔
1315
                    object[] parameters = new object[expectedCount];
1,065✔
1316

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

1319
                    // construct!
1320
                    result = Activator.CreateInstance(type, parameters);
1,065✔
1321
                }
1,065✔
1322

1323
                return result;
1,065✔
1324
            }
1325

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

1328
            // If we have refs, something has gone wrong; we should never be doing reflection inside a Record system.
1329
            // This is a really ad-hoc way of testing this and should be fixed.
1330
            // 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.
1331
            // I'm less OK with security vulnerabilities in save files. Nobody expects a savefile can compromise their system.
1332
            // And the full reflection system is probably impossible to secure, whereas the Record system should be secureable.
1333
            if (!context.allowReflection)
87,776✔
1334
            {
40✔
1335
                // just pick the first node to get something to go on
1336
                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✔
1337
                return result;
40✔
1338
            }
1339

1340
            foreach (var (parseCommand, node) in orders)
441,960✔
1341
            {
89,416✔
1342
                if (!isRootDec)
89,416✔
1343
                {
43,991✔
1344
                    switch (parseCommand)
43,991✔
1345
                    {
1346
                        case ParseCommand.Patch:
1347
                            // easy, done
1348
                            break;
43,991✔
1349

1350
                        default:
1351
                            Dbg.Err($"{node.GetInputContext()}: Internal error, got invalid mode {parseCommand}");
×
1352
                            break;
×
1353
                    }
1354
                }
43,991✔
1355
                else
1356
                {
45,425✔
1357
                    if (parseCommand != ParseCommand.Patch)
45,425✔
1358
                    {
×
1359
                        Dbg.Err($"{node.GetInputContext()}: Mode provided for root Dec; this is currently not supported in any form");
×
1360
                    }
×
1361
                }
45,425✔
1362

1363
                // If we haven't been given a generic class from our parent, go ahead and init to defaults
1364
                if (result == null && recordableBuffered != null)
89,416✔
1365
                {
15✔
1366
                    result = recordableBuffered;
15✔
1367
                }
15✔
1368

1369
                if (result == null)
89,416✔
1370
                {
17,610✔
1371
                    // okay fine
1372
                    result = type.CreateInstanceSafe("object", node);
17,610✔
1373

1374
                    if (result == null)
17,610✔
1375
                    {
80✔
1376
                        // error already reported
1377
                        return result;
80✔
1378
                    }
1379
                }
17,530✔
1380

1381
                node.ParseReflection(result, context, recContext);
89,336✔
1382
            }
89,336✔
1383

1384
            // Set up our index fields; this has to happen last in case we're a struct
1385
            Index.Register(ref result);
87,656✔
1386

1387
            return result;
87,656✔
1388
        }
1,554,794✔
1389

1390
        internal static object ParseString(string text, Type type, object original, InputContext context)
1391
        {
813,692✔
1392
            // Special case: Converter override
1393
            // This is redundant if we're being called from ParseElement, but we aren't always.
1394
            if (ConverterFor(type) is Converter converter)
813,692✔
1395
            {
45✔
1396
                object result = original;
45✔
1397

1398
                try
1399
                {
45✔
1400
                    // string converter
1401
                    if (converter is ConverterString converterString)
45✔
1402
                    {
45✔
1403
                        // context might be null; that's OK at the moment
1404
                        try
1405
                        {
45✔
1406
                            result = converterString.ReadObj(text, context);
45✔
1407
                        }
45✔
1408
                        catch (Exception e)
×
1409
                        {
×
1410
                            Dbg.Ex(new ConverterReadException(context, converter, e));
×
1411

1412
                            result = GenerateResultFallback(result, type);
×
1413
                        }
×
1414
                    }
45✔
1415
                    else if (converter is ConverterRecord converterRecord)
×
1416
                    {
×
1417
                        // string parsing really doesn't apply here, we can't get a full Recorder context out anymore
1418
                        // in theory this could be done with RecordAsThis() but I'm just going to skip it for now
1419
                        Dbg.Err($"{context}: Attempt to string-parse with a ConverterRecord, this is currently not supported, contact developers if you need this feature");
×
1420
                    }
×
1421
                    else if (converter is ConverterFactory converterFactory)
×
1422
                    {
×
1423
                        // string parsing really doesn't apply here, we can't get a full Recorder context out anymore
1424
                        // in theory this could be done with RecordAsThis() but I'm just going to skip it for now
1425
                        Dbg.Err($"{context}: Attempt to string-parse with a ConverterFactory, this is currently not supported, contact developers if you need this feature");
×
1426
                    }
×
1427
                    else
1428
                    {
×
1429
                        Dbg.Err($"Somehow ended up with an unsupported converter {converter.GetType()}");
×
1430
                    }
×
1431
                }
45✔
1432
                catch (Exception e)
×
1433
                {
×
1434
                    Dbg.Ex(e);
×
1435
                }
×
1436

1437
                return result;
45✔
1438
            }
1439

1440
            // Special case: decs
1441
            if (typeof(Dec).IsAssignableFrom(type))
813,647✔
1442
            {
48,285✔
1443
                if (text == "" || text == null)
48,285✔
1444
                {
40,695✔
1445
                    // you reference nothing, you get the null (even if this isn't a specified type; null is null, after all)
1446
                    return null;
40,695✔
1447
                }
1448
                else
1449
                {
7,590✔
1450
                    if (type.GetDecRootType() == null)
7,590✔
1451
                    {
80✔
1452
                        Dbg.Err($"{context}: Non-hierarchy decs cannot be used as references");
80✔
1453
                        return null;
80✔
1454
                    }
1455

1456
                    Dec result = Database.Get(type, text);
7,510✔
1457
                    if (result == null)
7,510✔
1458
                    {
85✔
1459
                        if (UtilMisc.ValidateDecName(text, context))
85✔
1460
                        {
85✔
1461
                            Dbg.Err($"{context}: Couldn't find {type} named `{text}`");
85✔
1462
                        }
85✔
1463

1464
                        // If we're an invalid name, we already spat out the error
1465
                    }
85✔
1466
                    return result;
7,510✔
1467
                }
1468
            }
1469

1470
            // Special case: types
1471
            if (type == typeof(Type))
765,362✔
1472
            {
414,901✔
1473
                if (text == "")
414,901✔
1474
                {
×
1475
                    return null;
×
1476
                }
1477

1478
                return UtilType.ParseDecFormatted(text, context);
414,901✔
1479
            }
1480

1481
            // Various non-composite-type special-cases
1482
            if (text != "")
350,461✔
1483
            {
350,461✔
1484
                // If we've got text, treat us as an object of appropriate type
1485
                try
1486
                {
350,461✔
1487
                    if (type == typeof(float))
350,461✔
1488
                    {
28,225✔
1489
                        // first check the various strings, case-insensitive
1490
                        if (String.Compare(text, "nan", true) == 0)
28,225✔
1491
                        {
90✔
1492
                            return float.NaN;
90✔
1493
                        }
1494

1495
                        if (String.Compare(text, "infinity", true) == 0)
28,135✔
1496
                        {
40✔
1497
                            return float.PositiveInfinity;
40✔
1498
                        }
1499

1500
                        if (String.Compare(text, "-infinity", true) == 0)
28,095✔
1501
                        {
40✔
1502
                            return float.NegativeInfinity;
40✔
1503
                        }
1504

1505
                        if (text.StartsWith("nanbox", StringComparison.CurrentCultureIgnoreCase))
28,055✔
1506
                        {
85✔
1507
                            const int expectedFloatSize = 6 + 8;
1508

1509
                            if (type == typeof(float) && text.Length != expectedFloatSize)
85✔
1510
                            {
×
1511
                                Dbg.Err($"{context}: Found nanboxed value without the expected number of characters, expected {expectedFloatSize} but got {text.Length}");
×
1512
                                return float.NaN;
×
1513
                            }
1514

1515
                            int number = Convert.ToInt32(text.Substring(6), 16);
85✔
1516
                            return BitConverter.Int32BitsToSingle(number);
85✔
1517
                        }
1518
                    }
27,970✔
1519

1520
                    if (type == typeof(double))
350,206✔
1521
                    {
2,840✔
1522
                        // first check the various strings, case-insensitive
1523
                        if (String.Compare(text, "nan", true) == 0)
2,840✔
1524
                        {
1,815✔
1525
                            return double.NaN;
1,815✔
1526
                        }
1527

1528
                        if (String.Compare(text, "infinity", true) == 0)
1,025✔
1529
                        {
40✔
1530
                            return double.PositiveInfinity;
40✔
1531
                        }
1532

1533
                        if (String.Compare(text, "-infinity", true) == 0)
985✔
1534
                        {
40✔
1535
                            return double.NegativeInfinity;
40✔
1536
                        }
1537

1538
                        if (text.StartsWith("nanbox", StringComparison.CurrentCultureIgnoreCase))
945✔
1539
                        {
75✔
1540
                            const int expectedDoubleSize = 6 + 16;
1541

1542
                            if (type == typeof(double) && text.Length != expectedDoubleSize)
75✔
1543
                            {
×
1544
                                Dbg.Err($"{context}: Found nanboxed value without the expected number of characters, expected {expectedDoubleSize} but got {text.Length}");
×
1545
                                return double.NaN;
×
1546
                            }
1547

1548
                            long number = Convert.ToInt64(text.Substring(6), 16);
75✔
1549
                            return BitConverter.Int64BitsToDouble(number);
75✔
1550
                        }
1551
                    }
870✔
1552

1553
                    return TypeDescriptor.GetConverter(type).ConvertFromString(text);
348,236✔
1554
                }
1555
                catch (System.Exception e)  // I would normally not catch System.Exception, but TypeConverter is wrapping FormatException in an Exception for some reason
180✔
1556
                {
180✔
1557
                    Dbg.Err($"{context}: {e.ToString()}");
180✔
1558
                    return original;
180✔
1559
                }
1560
            }
1561
            else if (type == typeof(string))
×
1562
            {
×
1563
                // If we don't have text, and we're a string, return ""
1564
                return "";
×
1565
            }
1566
            else
1567
            {
×
1568
                // If we don't have text, and we've fallen down to this point, that's an error (and return original value I guess)
1569
                Dbg.Err($"{context}: Empty field provided for type {type}");
×
1570
                return original;
×
1571
            }
1572
        }
813,692✔
1573

1574
        internal static Type TypeSystemRuntimeType = Type.GetType("System.RuntimeType");
10✔
1575
        internal static void ComposeElement(WriterNode node, object value, Type fieldType, FieldInfo fieldInfo = null, bool isRootDec = false, bool asThis = false)
1576
        {
1,459,129✔
1577
            // Verify our Shared flags as the *very* first step to ensure nothing gets past us.
1578
            // 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
1579
            bool canBeShared = fieldType.CanBeShared();
1,459,129✔
1580
            if (node.RecorderContext.shared == Recorder.Context.Shared.Allow && !asThis)
1,459,129✔
1581
            {
801,740✔
1582
                // 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.
1583
                if (!canBeShared)
801,740✔
1584
                {
120✔
1585
                    // If shared, make sure our type is appropriate for sharing
1586
                    // this really needs the recorder name and the field name too
1587
                    Dbg.Wrn($"Value type `{fieldType}` tagged as Shared in recorder, this is meaningless but harmless");
120✔
1588
                }
120✔
1589
            }
801,740✔
1590

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

1600
                var rootType = value.GetType().GetDecRootType();
3,870✔
1601
                if (!rootType.IsAssignableFrom(fieldType))
3,870✔
1602
                {
30✔
1603
                    // The user has a Dec.Dec or similar, and it has a Dec assigned to it.
1604
                    // 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.
1605
                    // But we're OK with that, honestly. We just do that.
1606
                    // If you're saving something like this you don't get to rename Dec classes later on, but, hey, deal with it.
1607
                    // 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.
1608
                    node.TagClass(rootType);
30✔
1609
                }
30✔
1610

1611
                node.WriteDec(value as Dec);
3,870✔
1612

1613
                return;
3,870✔
1614
            }
1615

1616
            // Everything represents "null" with an explicit XML tag, so let's just do that
1617
            // Maybe at some point we want to special-case this for the empty Dec link
1618
            if (value == null)
1,455,259✔
1619
            {
366,257✔
1620
                if (typeof(Dec).IsAssignableFrom(fieldType))
366,257✔
1621
                {
33,950✔
1622
                    node.WriteDec(null);
33,950✔
1623
                }
33,950✔
1624
                else
1625
                {
332,307✔
1626
                    node.WriteExplicitNull();
332,307✔
1627
                }
332,307✔
1628

1629
                return;
366,257✔
1630
            }
1631

1632
            var valType = value.GetType();
1,089,002✔
1633

1634
            // This is our value's type, but we may need a little bit of tinkering to make it useful.
1635
            // The current case I know of is System.RuntimeType, which appears if we call .GetType() on a Type.
1636
            // I assume there is a complicated internal reason for this; good news, we can ignore it and just pretend it's a System.Type.
1637
            // Bad news: it's actually really hard to detect this case because System.RuntimeType is private.
1638
            // That's why we have the annoying `static` up above.
1639
            if (valType == TypeSystemRuntimeType)
1,089,002✔
1640
            {
240✔
1641
                valType = typeof(Type);
240✔
1642
            }
240✔
1643

1644
            // Do all our unreferencables first
1645
            bool unreferenceableComplete = false;
1,089,002✔
1646

1647
            if (valType.IsPrimitive)
1,089,002✔
1648
            {
249,356✔
1649
                node.WritePrimitive(value);
249,356✔
1650

1651
                unreferenceableComplete = true;
249,356✔
1652
            }
249,356✔
1653
            else if (value is System.Enum)
839,646✔
1654
            {
305✔
1655
                node.WriteEnum(value);
305✔
1656

1657
                unreferenceableComplete = true;
305✔
1658
            }
305✔
1659
            else if (value is string)
839,341✔
1660
            {
27,505✔
1661
                node.WriteString(value as string);
27,505✔
1662

1663
                unreferenceableComplete = true;
27,505✔
1664
            }
27,505✔
1665
            else if (value is Type)
811,836✔
1666
            {
240✔
1667
                node.WriteType(value as Type);
240✔
1668

1669
                unreferenceableComplete = true;
240✔
1670
            }
240✔
1671

1672
            // Check to see if we should make this into a ref (yes, even if we're not tagged as Shared)
1673
            // Do this *before* we do the class tagging, otherwise we may add ref/class tags to a single node, which is invalid.
1674
            // 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.
1675
            if (Util.CanBeShared(valType) && !asThis)
1,089,002✔
1676
            {
782,521✔
1677
                if (node.WriteReference(value))
782,521✔
1678
                {
150,755✔
1679
                    // The ref system has set up the appropriate tagging, so we're done!
1680
                    return;
150,755✔
1681
                }
1682

1683
                // If we support references, then this object has not previously shown up in the reference system; keep going so we finish serializing it.
1684
                // If we don't support references at all then obviously we *really* need to finish serializing it.
1685
            }
631,766✔
1686

1687
            // 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`.
1688
            if (valType != fieldType)
938,247✔
1689
            {
1,046✔
1690
                if (asThis)
1,046✔
1691
                {
20✔
1692
                    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✔
1693
                    // . . . I guess we just keep going?
1694
                }
20✔
1695
                else
1696
                {
1,026✔
1697
                    node.TagClass(valType);
1,026✔
1698
                }
1,026✔
1699
            }
1,046✔
1700

1701
            // Did we actually write our node type? Alright, we're done.
1702
            if (unreferenceableComplete)
938,247✔
1703
            {
277,406✔
1704
                return;
277,406✔
1705
            }
1706

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

1709
            if (node.AllowCloning && valType.GetCustomAttribute<CloneWithAssignmentAttribute>() != null)
660,841✔
1710
            {
20✔
1711
                node.WriteCloneCopy(value);
20✔
1712

1713
                return;
20✔
1714
            }
1715

1716
            if (valType.IsArray)
660,821✔
1717
            {
2,630✔
1718
                node.WriteArray(value as Array);
2,630✔
1719

1720
                return;
2,630✔
1721
            }
1722

1723
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(List<>))
658,191✔
1724
            {
26,485✔
1725
                node.WriteList(value as IList);
26,485✔
1726

1727
                return;
26,485✔
1728
            }
1729

1730
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
631,706✔
1731
            {
11,765✔
1732
                node.WriteDictionary(value as IDictionary);
11,765✔
1733

1734
                return;
11,765✔
1735
            }
1736

1737
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(HashSet<>))
619,941✔
1738
            {
590✔
1739
                node.WriteHashSet(value as IEnumerable);
590✔
1740

1741
                return;
590✔
1742
            }
1743

1744
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(Queue<>))
619,351✔
1745
            {
50✔
1746
                node.WriteQueue(value as IEnumerable);
50✔
1747

1748
                return;
50✔
1749
            }
1750

1751
            if (valType.IsGenericType && valType.GetGenericTypeDefinition() == typeof(Stack<>))
619,301✔
1752
            {
50✔
1753
                node.WriteStack(value as IEnumerable);
50✔
1754

1755
                return;
50✔
1756
            }
1757

1758
            if (valType.IsGenericType && (
619,251✔
1759
                    valType.GetGenericTypeDefinition() == typeof(Tuple<>) ||
619,251✔
1760
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,>) ||
619,251✔
1761
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,>) ||
619,251✔
1762
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,>) ||
619,251✔
1763
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,>) ||
619,251✔
1764
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,,>) ||
619,251✔
1765
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,>) ||
619,251✔
1766
                    valType.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,,>)
619,251✔
1767
                ))
619,251✔
1768
            {
215✔
1769
                node.WriteTuple(value, fieldInfo?.GetCustomAttribute<System.Runtime.CompilerServices.TupleElementNamesAttribute>());
215✔
1770

1771
                return;
215✔
1772
            }
1773

1774
            if (valType.IsGenericType && (
619,036✔
1775
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<>) ||
619,036✔
1776
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,>) ||
619,036✔
1777
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,>) ||
619,036✔
1778
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,>) ||
619,036✔
1779
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,>) ||
619,036✔
1780
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,>) ||
619,036✔
1781
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,>) ||
619,036✔
1782
                    valType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,,>)
619,036✔
1783
                ))
619,036✔
1784
            {
385✔
1785
                node.WriteValueTuple(value, fieldInfo?.GetCustomAttribute<System.Runtime.CompilerServices.TupleElementNamesAttribute>());
385✔
1786

1787
                return;
385✔
1788
            }
1789

1790
            if (value is IRecordable
618,651✔
1791
                && (!(value is IConditionalRecordable) || (value as IConditionalRecordable).ShouldRecord(node.UserSettings)))
618,651✔
1792
            {
562,680✔
1793
                node.WriteRecord(value as IRecordable);
562,680✔
1794

1795
                return;
562,680✔
1796
            }
1797

1798
            {
55,971✔
1799
                // Look for a converter; that's the only way to handle this before we fall back to reflection
1800
                var converter = Serialization.ConverterFor(valType);
55,971✔
1801
                if (converter != null)
55,971✔
1802
                {
565✔
1803
                    node.WriteConvertible(converter, value);
565✔
1804
                    return;
565✔
1805
                }
1806
            }
55,406✔
1807

1808
            if (!node.AllowReflection)
55,406✔
1809
            {
45✔
1810
                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✔
1811
                node.WriteError();
45✔
1812
                return;
45✔
1813
            }
1814

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

1817
            foreach (var field in valType.GetSerializableFieldsFromHierarchy())
758,261✔
1818
            {
296,089✔
1819
                ComposeElement(node.CreateReflectionChild(field, node.RecorderContext), field.GetValue(value), field.FieldType, fieldInfo: field);
296,089✔
1820
            }
296,089✔
1821

1822
            return;
55,361✔
1823
        }
1,459,129✔
1824

1825
        internal static void Clear()
1826
        {
25,995✔
1827
            ConverterInitialized = false;
25,995✔
1828
            ConverterObjects = new System.Collections.Concurrent.ConcurrentDictionary<Type, Converter>();
25,995✔
1829
            ConverterGenericPrototypes = new System.Collections.Concurrent.ConcurrentDictionary<Type, Type>();
25,995✔
1830
        }
25,995✔
1831
    }
1832
}
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