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

zorbathut / dec / 15122828327

19 May 2025 08:44PM UTC coverage: 90.096% (-0.2%) from 90.272%
15122828327

push

github

zorbathut
Added Intent property to Recorder to indicate Serialization/Cloning/Checksum purpose.

5076 of 5634 relevant lines covered (90.1%)

221085.88 hits per line

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

89.21
/src/ReaderXml.cs
1
using System;
2
using System.Collections;
3
using System.Collections.Generic;
4
using System.Linq;
5
using System.Reflection;
6
using System.Xml.Linq;
7

8
namespace Dec
9
{
10
    internal class ReaderNodeXml : ReaderNodeParseable
11
    {
12
        public override Recorder.IUserSettings UserSettings { get; }
1,571,592✔
13
        public override Recorder.Purpose Intent { get => Recorder.Purpose.Serialization; }
×
14

15
        private XElement xml;
16
        private string fileIdentifier;
17
        private Path path;
18

19
        public ReaderNodeXml(XElement xml, string fileIdentifier, Path path, Recorder.IUserSettings userSettings)
1,988,892✔
20
        {
1,988,892✔
21
            this.UserSettings = userSettings;
1,988,892✔
22
            this.xml = xml;
1,988,892✔
23
            this.fileIdentifier = fileIdentifier;
1,988,892✔
24
            this.path = path;
1,988,892✔
25
        }
1,988,892✔
26

27
        public override Context GetContext()
28
        {
4,563,468✔
29
            return new Context(fileIdentifier, xml, path);
4,563,468✔
30
        }
4,563,468✔
31

32
        public override ReaderNode GetChildNamed(string name)
33
        {
927,210✔
34
            var child = xml.ElementNamed(name);
927,210✔
35
            return child == null ? null : new ReaderNodeXml(child, fileIdentifier, new PathMember(path, name), UserSettings);
927,210✔
36
        }
927,210✔
37
        public override string[] GetAllChildren()
38
        {
471,306✔
39
            return xml.Elements().Select(e => e.Name.LocalName).ToArray();
1,398,090✔
40
        }
471,306✔
41

42
        public override string GetText()
43
        {
2,355,836✔
44
            return xml.GetText();
2,355,836✔
45
        }
2,355,836✔
46

47
        public override string GetMetadata(Metadata metadata)
48
        {
16,901,394✔
49
            return xml.Attribute(metadata.ToLowerString())?.Value;
16,901,394✔
50
        }
16,901,394✔
51

52
        private readonly HashSet<string> metadataNames = UtilMisc.GetEnumValues<Metadata>().Select(metadata => metadata.ToLowerString()).ToHashSet();
9,944,460✔
53
        public override string GetMetadataUnrecognized()
54
        {
1,878,084✔
55
            if (!xml.HasAttributes)
1,878,084✔
56
            {
895,556✔
57
                return null;
895,556✔
58
            }
59

60
            var unrecognized = string.Join(", ", xml.Attributes().Select(attr => attr.Name.LocalName).Where(name => !metadataNames.Contains(name)));
2,948,628✔
61
            return unrecognized == string.Empty ? null : unrecognized;
982,528✔
62
        }
1,878,084✔
63

64
        public override bool HasChildren()
65
        {
1,876,338✔
66
            return xml.Elements().Any();
1,876,338✔
67
        }
1,876,338✔
68

69
        public override int[] GetArrayDimensions(int rank)
70
        {
3,882✔
71
            // The actual processing will be handled by ParseArray, so we're not doing much validation here right now
72
            int[] results = new int[rank];
3,882✔
73
            var tier = xml;
3,882✔
74
            for (int i = 0; i < rank; ++i)
16,704✔
75
            {
4,554✔
76
                results[i] = tier.Elements().Count();
4,554✔
77

78
                tier = tier.Elements().FirstOrDefault();
4,554✔
79
                if (tier == null)
4,554✔
80
                {
84✔
81
                    // ran out of elements; stop now, we'll leave them full of 0's
82
                    break;
84✔
83
                }
84
            }
4,470✔
85

86
            return results;
3,882✔
87
        }
3,882✔
88

89
        public override void ParseList(IList list, Type referencedType, ReaderGlobals readerGlobals, Recorder.Settings recorderSettings)
90
        {
55,878✔
91
            var recorderChildContext = recorderSettings.CreateChild();
55,878✔
92

93
            foreach (var fieldElement in xml.Elements())
429,186✔
94
            {
130,776✔
95
                if (fieldElement.Name.LocalName != "li")
130,776✔
96
                {
48✔
97
                    var elementContext = new Context(fileIdentifier, fieldElement, path);
48✔
98
                    Dbg.Err($"{elementContext}: Tag should be <li>, is <{fieldElement.Name.LocalName}>");
48✔
99
                }
48✔
100

101
                list.Add(Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(fieldElement, fileIdentifier, new PathIndex(path, list.Count), UserSettings) }, referencedType, null, readerGlobals, recorderChildContext));
130,776✔
102
            }
130,776✔
103

104
            list.GetType().GetField("_version", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(list, Util.CollectionDeserializationVersion);
55,878✔
105
        }
55,878✔
106

107
        private void ParseArrayRank(ReaderNodeXml node, ReaderGlobals readerGlobals, Recorder.Settings recorderSettings, Array value, Type referencedType, int rank, int[] indices, int startAt)
108
        {
4,848✔
109
            if (rank == indices.Length)
4,848✔
110
            {
2,790✔
111
                value.SetValue(Serialization.ParseElement(new List<ReaderNodeParseable>() { node }, referencedType, null, readerGlobals, recorderSettings), indices);
2,790✔
112
            }
2,790✔
113
            else
114
            {
2,058✔
115
                // this is kind of unnecessary but it's also an irrelevant perf hit
116
                var recorderChildContext = recorderSettings.CreateChild();
2,058✔
117

118
                int elementCount = node.xml.Elements().Count();
2,058✔
119
                int rankLength = value.GetLength(rank);
2,058✔
120
                if (elementCount > rankLength)
2,058✔
121
                {
18✔
122
                    Dbg.Err($"{node.GetContext()}: Array dimension {rank} expects {rankLength} elements but got {elementCount}; truncating");
18✔
123
                }
18✔
124
                else if (elementCount < rankLength)
2,040✔
125
                {
36✔
126
                    Dbg.Err($"{node.GetContext()}: Array dimension {rank} expects {rankLength} elements but got {elementCount}; padding with default values");
36✔
127
                }
36✔
128

129
                int i = 0;
2,058✔
130
                foreach (var fieldElement in node.xml.Elements())
14,796✔
131
                {
4,320✔
132
                    if (i >= rankLength)
4,320✔
133
                    {
18✔
134
                        // truncate, we're done here
135
                        break;
18✔
136
                    }
137

138
                    indices[rank] = startAt + i++;
4,302✔
139
                    var newPath = new PathIndexMultidim(path, indices.ToArray());
4,302✔
140

141
                    if (fieldElement.Name.LocalName != "li")
4,302✔
142
                    {
×
143
                        var elementContext = new Context(fileIdentifier, fieldElement, newPath);
×
144
                        Dbg.Err($"{elementContext}: Tag should be <li>, is <{fieldElement.Name.LocalName}>");
×
145
                    }
×
146

147
                    // the pathIndexMultidim is kind of slow and I should solve this at some point
148
                    ParseArrayRank(new ReaderNodeXml(fieldElement, fileIdentifier, newPath, UserSettings), readerGlobals, recorderChildContext, value, referencedType, rank + 1, indices, 0);
4,302✔
149
                }
4,302✔
150
            }
2,058✔
151
        }
4,848✔
152

153
        public override void ParseArray(Array array, Type referencedType, ReaderGlobals readerGlobals, Recorder.Settings recorderSettings, int startOffset)
154
        {
3,846✔
155
            var recorderChildContext = recorderSettings.CreateChild();
3,846✔
156

157
            if (array.Rank == 1)
3,846✔
158
            {
3,300✔
159
                // fast path
160
                int index = 0;
3,300✔
161
                foreach (var fieldElement in xml.Elements())
27,024✔
162
                {
8,562✔
163
                    var newPath = new PathIndex(path, startOffset + index);
8,562✔
164

165
                    if (fieldElement.Name.LocalName != "li")
8,562✔
166
                    {
48✔
167
                        var elementContext = new Context(fileIdentifier, fieldElement, newPath);
48✔
168
                        Dbg.Err($"{elementContext}: Tag should be <li>, is <{fieldElement.Name.LocalName}>");
48✔
169
                    }
48✔
170

171
                    array.SetValue(Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(fieldElement, fileIdentifier, newPath, UserSettings) }, referencedType, null, readerGlobals, recorderChildContext), startOffset + index);
8,562✔
172
                    ++index;
8,562✔
173
                }
8,562✔
174
            }
3,300✔
175
            else
176
            {
546✔
177
                // slow path
178
                var indices = new int[array.Rank];
546✔
179
                ParseArrayRank(this, readerGlobals, recorderChildContext, array, referencedType, 0, indices, startOffset);
546✔
180
            }
546✔
181
        }
3,846✔
182

183
        public override void ParseDictionary(IDictionary dict, Type referencedKeyType, Type referencedValueType, ReaderGlobals readerGlobals, Recorder.Settings recorderSettings, bool permitPatch)
184
        {
28,518✔
185
            var recorderChildContext = recorderSettings.CreateChild();
28,518✔
186

187
            // avoid the heap allocation if we can
188
            var writtenFields = permitPatch ? new HashSet<object>() : null;
28,518✔
189

190
            foreach (var fieldElement in xml.Elements())
202,026✔
191
            {
58,236✔
192
                var elementContext = new Context(fileIdentifier, fieldElement);
58,236✔
193

194
                if (fieldElement.Name.LocalName == "li")
58,236✔
195
                {
55,344✔
196
                    // Treat this like a key/value pair
197
                    var keyNode = fieldElement.ElementNamedWithFallback("key", elementContext, "Dictionary includes li tag without a `key`");
55,344✔
198
                    var valueNode = fieldElement.ElementNamedWithFallback("value", elementContext, "Dictionary includes li tag without a `value`");
55,344✔
199

200
                    if (keyNode == null)
55,344✔
201
                    {
48✔
202
                        // error has already been generated
203
                        continue;
48✔
204
                    }
205

206
                    if (valueNode == null)
55,296✔
207
                    {
24✔
208
                        // error has already been generated
209
                        continue;
24✔
210
                    }
211

212
                    var keyPath = new PathDictionaryKey(path);
55,272✔
213
                    var key = Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(keyNode, fileIdentifier, keyPath, UserSettings) }, referencedKeyType, null, readerGlobals, recorderChildContext);
55,272✔
214

215
                    if (key == null)
55,272✔
216
                    {
48✔
217
                        Dbg.Err($"{new Context(fileIdentifier, keyNode, keyPath)}: Dictionary includes null key, skipping pair");
48✔
218
                        continue;
48✔
219
                    }
220

221
                    object originalValue = null;
55,224✔
222
                    if (dict.Contains(key))
55,224✔
223
                    {
72✔
224
                        // Annoyingly the IDictionary interface does not allow for simultaneous retrieval and existence check a la .TryGetValue(), so we get to do a second lookup here
225
                        // This is definitely not the common path, though, so, fine
226
                        originalValue = dict[key];
72✔
227

228
                        if (writtenFields == null || writtenFields.Contains(key))
72✔
229
                        {
24✔
230
                            Dbg.Err($"{elementContext}: Dictionary includes duplicate key `{key.ToString()}`");
24✔
231
                        }
24✔
232
                    }
72✔
233

234
                    writtenFields?.Add(key);
55,224✔
235

236
                    var valuePath = new PathDictionaryValueUnpathable(path);
55,224✔
237
                    dict[key] = Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(valueNode, fileIdentifier,valuePath, UserSettings) }, referencedValueType, originalValue, readerGlobals, recorderChildContext);
55,224✔
238
                }
55,224✔
239
                else
240
                {
2,892✔
241
                    var key = Serialization.ParseString(fieldElement.Name.LocalName, referencedKeyType, null, elementContext);
2,892✔
242

243
                    if (key == null)
2,892✔
244
                    {
×
245
                        // it's really rare for this to happen, I think you could do it with a converter but that's it
246
                        Dbg.Err($"{elementContext}: Dictionary includes null key, skipping pair");
×
247

248
                        // just in case . . .
249
                        if (string.Compare(fieldElement.Name.LocalName, "li", true) == 0)
×
250
                        {
×
251
                            Dbg.Err($"{elementContext}: Did you mean to write `li`? This field is case-sensitive.");
×
252
                        }
×
253

254
                        continue;
×
255
                    }
256

257
                    object originalValue = null;
2,892✔
258
                    if (dict.Contains(key))
2,892✔
259
                    {
288✔
260
                        // Annoyingly the IDictionary interface does not allow for simultaneous retrieval and existence check a la .TryGetValue(), so we get to do a second lookup here
261
                        // This is definitely not the common path, though, so, fine
262
                        originalValue = dict[key];
288✔
263

264
                        if (writtenFields == null || writtenFields.Contains(key))
288✔
265
                        {
192✔
266
                            Dbg.Err($"{elementContext}: Dictionary includes duplicate key `{key.ToString()}`");
192✔
267
                        }
192✔
268
                    }
288✔
269

270
                    Type valueType = referencedValueType;
2,892✔
271
                    if (recorderSettings.bespoke_keytypedict)
2,892✔
272
                    {
72✔
273
                        if (referencedKeyType != typeof(Type))
72✔
274
                        {
×
275
                            Dbg.Err($"{elementContext}: Bespoke_Keytypedict used on a dictionary that isn't a Type key");
×
276
                        }
×
277

278
                        // make sure the value of `key` can be implicitly converted to `referencedValueType`
279
                        if (!referencedValueType.IsAssignableFrom((Type)key))
72✔
280
                        {
×
281
                            Dbg.Err($"{elementContext}: Key `{key}` cannot be implicitly converted to {referencedValueType}");
×
282
                        }
×
283

284
                        valueType = (Type)key;
72✔
285
                    }
72✔
286

287
                    writtenFields?.Add(key);
2,892✔
288

289
                    Path valuePath;
290
                    if (fieldElement.Name.LocalName.Contains('[') || fieldElement.Name.LocalName.Contains(']'))
2,892✔
291
                    {
×
292
                        // I'm not actually sure this is possible
293
                        valuePath = new PathDictionaryValueUnpathable(path);
×
294
                    }
×
295
                    else
296
                    {
2,892✔
297
                        valuePath = new PathDictionaryValue(path, fieldElement.Name.LocalName);
2,892✔
298
                    }
2,892✔
299

300
                    dict[key] = Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(fieldElement, fileIdentifier, valuePath, UserSettings) }, valueType, originalValue, readerGlobals, recorderChildContext);
2,892✔
301
                }
2,892✔
302
            }
58,116✔
303
        }
28,518✔
304

305
        public override void ParseHashset(object hashset, Type referencedType, ReaderGlobals readerGlobals, Recorder.Settings recorderSettings, bool permitPatch)
306
        {
1,212✔
307
            // This is a gigantic pain because HashSet<> doesn't inherit from any non-generic interface that provides the functionality we want
308
            // So we're stuck doing it all through object and reflection
309
            // Thanks, HashSet
310
            // This might be a performance problem and we'll . . . deal with it later I guess?
311
            // This might actually be a good first place to use IL generation.
312

313
            var containsFunction = hashset.GetType().GetMethod("Contains");
1,212✔
314
            var addFunction = hashset.GetType().GetMethod("Add");
1,212✔
315

316
            var recorderChildContext = recorderSettings.CreateChild();
1,212✔
317
            var keyParam = new object[1];   // this is just to cut down on GC churn
1,212✔
318

319
            // avoid the heap allocation if we can
320
            var writtenFields = permitPatch ? new HashSet<object>() : null;
1,212✔
321

322
            foreach (var fieldElement in xml.Elements())
12,144✔
323
            {
4,254✔
324
                var keyPath = new PathHashSetElement(path);
4,254✔
325
                var elementContext = new Context(fileIdentifier, fieldElement, keyPath);
4,254✔
326

327
                // There's a potential bit of ambiguity here if someone does <li /> and expects that to be an actual string named "li".
328
                // Practically, I think this is less likely than someone doing <li></li> and expecting that to be the empty string.
329
                // And there's no other way to express the empty string.
330
                // So . . . we treat that like the empty string.
331
                if (fieldElement.Name.LocalName == "li")
4,254✔
332
                {
3,822✔
333
                    // Treat this like a full node
334
                    var key = Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(fieldElement, fileIdentifier, keyPath, UserSettings) }, referencedType, null, readerGlobals, recorderChildContext);
3,822✔
335

336
                    if (key == null)
3,822✔
337
                    {
×
338
                        Dbg.Err($"{elementContext}: HashSet includes null key, skipping");
×
339
                        continue;
×
340
                    }
341

342
                    keyParam[0] = key;
3,822✔
343

344
                    if ((bool)containsFunction.Invoke(hashset, keyParam) && (writtenFields == null || writtenFields.Contains(key)))
3,822✔
345
                    {
192✔
346
                        Dbg.Err($"{elementContext}: HashSet includes duplicate key `{key.ToString()}`");
192✔
347
                    }
192✔
348
                    writtenFields?.Add(key);
3,822✔
349

350
                    addFunction.Invoke(hashset, keyParam);
3,822✔
351
                }
3,822✔
352
                else
353
                {
432✔
354
                    if (fieldElement.HasElements || !fieldElement.GetText().IsNullOrEmpty())
432✔
355
                    {
48✔
356
                        Dbg.Err($"{elementContext}: HashSet non-li member includes data, ignoring");
48✔
357
                    }
48✔
358

359
                    var key = Serialization.ParseString(fieldElement.Name.LocalName, referencedType, null, elementContext);
432✔
360

361
                    if (key == null)
432✔
362
                    {
×
363
                        // it's really rare for this to happen, I think you could do it with a converter but that's it
364
                        Dbg.Err($"{elementContext}: HashSet includes null key, skipping pair");
×
365
                        continue;
×
366
                    }
367

368
                    keyParam[0] = key;
432✔
369

370
                    if ((bool)containsFunction.Invoke(hashset, keyParam) && (writtenFields == null || writtenFields.Contains(key)))
432✔
371
                    {
24✔
372
                        Dbg.Err($"{elementContext}: HashSet includes duplicate key `{key.ToString()}`");
24✔
373
                    }
24✔
374
                    writtenFields?.Add(key);
432✔
375

376
                    addFunction.Invoke(hashset, keyParam);
432✔
377
                }
432✔
378
            }
4,254✔
379
        }
1,212✔
380

381
        public override void ParseStack(object stack, Type referencedType, ReaderGlobals readerGlobals, Recorder.Settings recorderSettings)
382
        {
66✔
383
            var pushFunction = stack.GetType().GetMethod("Push");
66✔
384

385
            var recorderChildContext = recorderSettings.CreateChild();
66✔
386

387
            int index = (int)stack.GetType().GetProperty("Count").GetValue(stack);
66✔
388
            foreach (var fieldElement in xml.Elements())
654✔
389
            {
228✔
390
                var newPath = new PathIndex(path, index);
228✔
391

392
                if (fieldElement.Name.LocalName != "li")
228✔
393
                {
×
394
                    var elementContext = new Context(fileIdentifier, fieldElement, newPath);
×
395
                    Dbg.Err($"{elementContext}: Tag should be <li>, is <{fieldElement.Name.LocalName}>");
×
396
                }
×
397

398
                pushFunction.Invoke(stack, new object[] { Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(fieldElement, fileIdentifier, newPath, UserSettings) }, referencedType, null, readerGlobals, recorderChildContext) });
228✔
399
                ++index;
228✔
400
            }
228✔
401
        }
66✔
402

403
        public override void ParseQueue(object queue, Type referencedType, ReaderGlobals readerGlobals, Recorder.Settings recorderSettings)
404
        {
66✔
405
            var enqueueFunction = queue.GetType().GetMethod("Enqueue");
66✔
406

407
            var recorderChildContext = recorderSettings.CreateChild();
66✔
408

409
            int index = (int)queue.GetType().GetProperty("Count").GetValue(queue);
66✔
410
            foreach (var fieldElement in xml.Elements())
654✔
411
            {
228✔
412
                var newPath = new PathIndex(path, index);
228✔
413

414
                if (fieldElement.Name.LocalName != "li")
228✔
415
                {
×
416
                    var elementContext = new Context(fileIdentifier, fieldElement, newPath);
×
417
                    Dbg.Err($"{elementContext}: Tag should be <li>, is <{fieldElement.Name.LocalName}>");
×
418
                }
×
419

420
                enqueueFunction.Invoke(queue, new object[] { Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(fieldElement, fileIdentifier, newPath, UserSettings) }, referencedType, null, readerGlobals, recorderChildContext) });
228✔
421
                ++index;
228✔
422
            }
228✔
423
        }
66✔
424

425
        public override void ParseTuple(object[] parameters, Type referencedType, IList<string> parameterNames, ReaderGlobals readerGlobals, Recorder.Settings recorderSettings)
426
        {
1,284✔
427
            int expectedCount = referencedType.GenericTypeArguments.Length;
1,284✔
428
            var recorderChildContext = recorderSettings.CreateChild();
1,284✔
429

430
            var elements = xml.Elements().ToList();
1,284✔
431

432
            bool hasNonLi = false;
1,284✔
433
            foreach (var elementField in elements)
11,052✔
434
            {
3,600✔
435
                if (elementField.Name.LocalName != "li")
3,600✔
436
                {
780✔
437
                    hasNonLi = true;
780✔
438
                }
780✔
439
            }
3,600✔
440

441
            if (!hasNonLi)
1,284✔
442
            {
1,020✔
443
                // Treat it like an indexed array
444

445
                if (elements.Count != parameters.Length)
1,020✔
446
                {
72✔
447
                    Dbg.Err($"{GetContext()}: Tuple expects {expectedCount} parameters but got {elements.Count}");
72✔
448
                }
72✔
449

450
                for (int i = 0; i < Math.Min(parameters.Length, elements.Count); ++i)
7,512✔
451
                {
2,736✔
452
                    parameters[i] = Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(elements[i], fileIdentifier, new PathIndex(path, i), UserSettings) }, referencedType.GenericTypeArguments[i], null, readerGlobals, recorderChildContext);
2,736✔
453
                }
2,736✔
454

455
                // fill in anything missing
456
                for (int i = Math.Min(parameters.Length, elements.Count); i < parameters.Length; ++i)
2,184✔
457
                {
72✔
458
                    parameters[i] = Serialization.GenerateResultFallback(null, referencedType.GenericTypeArguments[i]);
72✔
459
                }
72✔
460
            }
1,020✔
461
            else
462
            {
264✔
463
                // We're doing named lookups instead
464
                if (parameterNames == null)
264✔
465
                {
48✔
466
                    parameterNames = UtilMisc.DefaultTupleNames;
48✔
467
                }
48✔
468

469
                if (parameterNames.Count < expectedCount)
264✔
470
                {
×
471
                    Dbg.Err($"{GetContext()}: Not enough tuple names (this honestly shouldn't even be possible)");
×
472

473
                    // TODO: handle it
474
                }
×
475

476
                bool[] seen = new bool[expectedCount];
264✔
477
                foreach (var elementItem in elements)
2,472✔
478
                {
840✔
479
                    var newPath = new PathMember(path, elementItem.Name.LocalName); // yeah okay
840✔
480
                    var elementContext = new Context(fileIdentifier, elementItem, newPath);
840✔
481

482
                    int index = parameterNames.FirstIndexOf(n => n == elementItem.Name.LocalName);
3,144✔
483

484
                    if (index == -1)
840✔
485
                    {
96✔
486
                        Dbg.Err($"{elementContext}: Found field with unexpected name `{elementItem.Name.LocalName}`");
96✔
487
                        continue;
96✔
488
                    }
489

490
                    if (seen[index])
744✔
491
                    {
24✔
492
                        Dbg.Err($"{elementContext}: Found duplicate of field `{elementItem.Name.LocalName}`");
24✔
493
                    }
24✔
494

495
                    seen[index] = true;
744✔
496
                    parameters[index] = Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(elementItem, fileIdentifier, newPath, UserSettings) }, referencedType.GenericTypeArguments[index], null, readerGlobals, recorderChildContext);
744✔
497
                }
744✔
498

499
                for (int i = 0; i < seen.Length; ++i)
2,208✔
500
                {
840✔
501
                    if (!seen[i])
840✔
502
                    {
120✔
503
                        Dbg.Err($"{GetContext()}: Missing field with name `{parameterNames[i]}`");
120✔
504

505
                        // Patch it up as best we can
506
                        parameters[i] = Serialization.GenerateResultFallback(null, referencedType.GenericTypeArguments[i]);
120✔
507
                    }
120✔
508
                }
840✔
509
            }
264✔
510
        }
1,284✔
511

512
        public override void ParseReflection(object obj, ReaderGlobals readerGlobals, Recorder.Settings recorderSettings)
513
        {
108,146✔
514
            var recorderChildContext = recorderSettings.CreateChild();
108,146✔
515
            var setFields = new HashSet<string>();
108,146✔
516

517
            var type = obj.GetType();
108,146✔
518

519
            foreach (var fieldElement in xml.Elements())
1,084,566✔
520
            {
380,082✔
521
                // Check for fields that have been set multiple times
522
                string fieldName = fieldElement.Name.LocalName;
380,082✔
523
                if (setFields.Contains(fieldName))
380,082✔
524
                {
48✔
525
                    Dbg.Err($"{new Context(fileIdentifier, fieldElement, path)}: Duplicate field `{fieldName}`");
48✔
526
                    // Just allow us to fall through; it's an error, but one with a reasonably obvious handling mechanism
527
                }
48✔
528
                setFields.Add(fieldName);
380,082✔
529

530
                var fieldElementInfo = type.GetFieldFromHierarchy(fieldName);
380,082✔
531
                if (fieldElementInfo == null)
380,082✔
532
                {
144✔
533
                    // Try to find a close match, if we can, just for a better error message
534
                    string match = null;
144✔
535
                    string canonicalFieldName = UtilMisc.LooseMatchCanonicalize(fieldName);
144✔
536

537
                    foreach (var testField in type.GetSerializableFieldsFromHierarchy())
792✔
538
                    {
216✔
539
                        if (UtilMisc.LooseMatchCanonicalize(testField.Name) == canonicalFieldName)
216✔
540
                        {
72✔
541
                            match = testField.Name;
72✔
542

543
                            // We could in theory do something overly clever where we try to find the best name, but I really don't care that much; this is meant as a quick suggestion, not an ironclad solution.
544
                            break;
72✔
545
                        }
546
                    }
144✔
547

548
                    if (match != null)
144✔
549
                    {
72✔
550
                        Dbg.Err($"{new Context(fileIdentifier, fieldElement, path)}: Field `{fieldName}` does not exist in type {type}; did you mean `{match}`?");
72✔
551
                    }
72✔
552
                    else
553
                    {
72✔
554
                        Dbg.Err($"{new Context(fileIdentifier, fieldElement, path)}: Field `{fieldName}` does not exist in type {type}");
72✔
555
                    }
72✔
556

557
                    continue;
144✔
558
                }
559

560
                if (fieldElementInfo.GetCustomAttribute<IndexAttribute>() != null)
379,938✔
561
                {
×
562
                    Dbg.Err($"{new Context(fileIdentifier, fieldElement, path)}: Attempting to set index field `{fieldName}`; these are generated by the dec system");
×
563
                    continue;
×
564
                }
565

566
                if (fieldElementInfo.GetCustomAttribute<NonSerializedAttribute>() != null)
379,938✔
567
                {
24✔
568
                    Dbg.Err($"{new Context(fileIdentifier, fieldElement, path)}: Attempting to set nonserialized field `{fieldName}`");
24✔
569
                    continue;
24✔
570
                }
571

572
                fieldElementInfo.SetValue(obj, Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(fieldElement, fileIdentifier, new PathMember(path, fieldName), UserSettings) }, fieldElementInfo.FieldType, fieldElementInfo.GetValue(obj), readerGlobals, recorderChildContext, fieldInfo: fieldElementInfo));
379,914✔
573
            }
379,914✔
574
        }
108,146✔
575
    }
576
}
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