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

zorbathut / dec / 9307326907

30 May 2024 06:18PM UTC coverage: 91.303% (-0.1%) from 91.401%
9307326907

Pull #7

github

zorbathut
Decs can now include `class` tags, if you need to specify a type that doesn't work as an XML node name.
Pull Request #7: Decs can now include class tags, if you need to specify a type that doesn't work as an XML node name.

4336 of 4749 relevant lines covered (91.3%)

183722.72 hits per line

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

91.86
/src/ReaderXml.cs
1
namespace Dec
2
{
3
    using System;
4
    using System.Collections;
5
    using System.Collections.Generic;
6
    using System.IO;
7
    using System.Linq;
8
    using System.Reflection;
9
    using System.Xml;
10
    using System.Xml.Linq;
11

12
    internal class ReaderFileDecXml : ReaderFileDec
13
    {
14
        private XDocument doc;
15
        private string fileIdentifier;
16
        private Recorder.IUserSettings userSettings;
17

18
        public static ReaderFileDecXml Create(TextReader input, string identifier, Recorder.IUserSettings userSettings)
19
        {
12,330✔
20
            XDocument doc;
21

22
            using (var _ = new CultureInfoScope(Config.CultureInfo))
12,330✔
23
            {
12,330✔
24
                try
25
                {
12,330✔
26
                    var settings = new XmlReaderSettings();
12,330✔
27
                    settings.IgnoreWhitespace = true;
12,330✔
28
                    using (var reader = XmlReader.Create(input, settings))
12,330✔
29
                    {
12,330✔
30
                        doc = XDocument.Load(reader, LoadOptions.SetLineInfo);
12,330✔
31
                    }
12,270✔
32
                }
12,270✔
33
                catch (System.Xml.XmlException e)
60✔
34
                {
60✔
35
                    Dbg.Ex(e);
60✔
36
                    return null;
60✔
37
                }
38
            }
12,270✔
39

40
            var result = new ReaderFileDecXml();
12,270✔
41
            result.doc = doc;
12,270✔
42
            result.fileIdentifier = identifier;
12,270✔
43
            result.userSettings = userSettings;
12,270✔
44
            return result;
12,270✔
45
        }
12,330✔
46

47
        public override List<ReaderDec> ParseDecs()
48
        {
12,265✔
49
            if (doc.Elements().Count() > 1)
12,265✔
50
            {
×
51
                // This isn't testable, unfortunately; XDocument doesn't even support multiple root elements.
52
                Dbg.Err($"{fileIdentifier}: Found {doc.Elements().Count()} root elements instead of the expected 1");
×
53
            }
×
54

55
            var result = new List<ReaderDec>();
12,265✔
56

57
            foreach (var rootElement in doc.Elements())
61,325✔
58
            {
12,265✔
59
                var rootContext = new InputContext(fileIdentifier, rootElement);
12,265✔
60
                if (rootElement.Name.LocalName != "Decs")
12,265✔
61
                {
20✔
62
                    Dbg.Wrn($"{rootContext}: Found root element with name `{rootElement.Name.LocalName}` when it should be `Decs`");
20✔
63
                }
20✔
64

65
                foreach (var decElement in rootElement.Elements())
129,185✔
66
                {
46,195✔
67
                    var readerDec = new ReaderDec();
46,195✔
68

69
                    readerDec.inputContext = new InputContext(fileIdentifier, decElement);
46,195✔
70
                    string typeName = decElement.Name.LocalName;
46,195✔
71

72
                    readerDec.type = UtilType.ParseDecFormatted(typeName, readerDec.inputContext);
46,195✔
73
                    if (readerDec.type == null || !typeof(Dec).IsAssignableFrom(readerDec.type))
46,195✔
74
                    {
40✔
75
                        Dbg.Err($"{readerDec.inputContext}: {typeName} is being used as a Dec but does not inherit from Dec.Dec");
40✔
76
                        continue;
40✔
77
                    }
78

79
                    if (decElement.Attribute("decName") == null)
46,155✔
80
                    {
20✔
81
                        Dbg.Err($"{readerDec.inputContext}: No dec name provided, add a `decName=` attribute to the {typeName} tag (example: <{typeName} decName=\"TheNameOfYour{typeName}\">)");
20✔
82
                        continue;
20✔
83
                    }
84

85
                    readerDec.name = decElement.Attribute("decName").Value;
46,135✔
86
                    if (!UtilMisc.ValidateDecName(readerDec.name, readerDec.inputContext))
46,135✔
87
                    {
80✔
88
                        continue;
80✔
89
                    }
90

91
                    // Consume decName so we know it's not hanging around
92
                    decElement.Attribute("decName").Remove();
46,055✔
93

94
                    // Parse `class` if we can
95
                    if (decElement.Attribute("class") is var classAttribute && classAttribute != null)
46,055✔
96
                    {
30✔
97
                        var parsedClass = (Type)Serialization.ParseString(decElement.Attribute("class").Value,
30✔
98
                            typeof(Type), null, readerDec.inputContext);
30✔
99

100
                        if (parsedClass == null)
30✔
101
                        {
×
102
                            // we have presumably already reported an error
103
                        }
×
104
                        else if (!readerDec.type.IsAssignableFrom(parsedClass))
30✔
105
                        {
×
106
                            Dbg.Err($"{readerDec.inputContext}: Attribute-parsed class {parsedClass} is not a subclass of {readerDec.type}; using the original class");
×
107
                        }
×
108
                        else
109
                        {
30✔
110
                            // yay
111
                            readerDec.type = parsedClass;
30✔
112
                        }
30✔
113

114
                        // clean up
115
                        classAttribute.Remove();
30✔
116
                    }
30✔
117

118
                    // Check to see if we're abstract
119
                    {
46,055✔
120
                        var abstractAttribute = decElement.Attribute("abstract");
46,055✔
121
                        if (abstractAttribute != null)
46,055✔
122
                        {
1,120✔
123
                            if (!bool.TryParse(abstractAttribute.Value, out bool abstrct))
1,120✔
124
                            {
20✔
125
                                Dbg.Err($"{readerDec.inputContext}: Error encountered when parsing abstract attribute");
20✔
126
                            }
20✔
127
                            readerDec.abstrct = abstrct; // little dance to deal with the fact that readerDec.abstrct is a `bool?`
1,120✔
128

129
                            abstractAttribute.Remove();
1,120✔
130
                        }
1,120✔
131
                    }
46,055✔
132

133
                    // Get our parent info
134
                    {
46,055✔
135
                        var parentAttribute = decElement.Attribute("parent");
46,055✔
136
                        if (parentAttribute != null)
46,055✔
137
                        {
1,300✔
138
                            readerDec.parent = parentAttribute.Value;
1,300✔
139

140
                            parentAttribute.Remove();
1,300✔
141
                        }
1,300✔
142
                    }
46,055✔
143

144
                    // Everything looks good!
145
                    readerDec.node = new ReaderNodeXml(decElement, fileIdentifier, userSettings);
46,055✔
146

147
                    result.Add(readerDec);
46,055✔
148
                }
46,055✔
149
            }
12,265✔
150

151
            return result;
12,265✔
152
        }
12,265✔
153
    }
154

155
    internal class ReaderFileRecorderXml : ReaderFileRecorder
156
    {
157
        private XElement record;
158
        private string fileIdentifier;
159
        private Recorder.IUserSettings userSettings;
160

161
        public static ReaderFileRecorderXml Create(string input, string identifier, Recorder.IUserSettings userSettings)
162
        {
2,445✔
163
            XDocument doc;
164

165
            try
166
            {
2,445✔
167
                doc = XDocument.Parse(input, LoadOptions.SetLineInfo);
2,445✔
168
            }
2,430✔
169
            catch (System.Xml.XmlException e)
15✔
170
            {
15✔
171
                Dbg.Ex(e);
15✔
172
                return null;
15✔
173
            }
174

175
            if (doc.Elements().Count() > 1)
2,430✔
176
            {
×
177
                // This isn't testable, unfortunately; XDocument doesn't even support multiple root elements.
178
                Dbg.Err($"{identifier}: Found {doc.Elements().Count()} root elements instead of the expected 1");
×
179
            }
×
180

181
            var record = doc.Elements().First();
2,430✔
182
            if (record.Name.LocalName != "Record")
2,430✔
183
            {
5✔
184
                Dbg.Wrn($"{new InputContext(identifier, record)}: Found root element with name `{record.Name.LocalName}` when it should be `Record`");
5✔
185
            }
5✔
186

187
            var recordFormatVersion = record.ElementNamed("recordFormatVersion");
2,430✔
188
            if (recordFormatVersion == null)
2,430✔
189
            {
10✔
190
                Dbg.Err($"{new InputContext(identifier, record)}: Missing record format version, assuming the data is up-to-date");
10✔
191
            }
10✔
192
            else if (recordFormatVersion.GetText() != "1")
2,420✔
193
            {
15✔
194
                Dbg.Err($"{new InputContext(identifier, recordFormatVersion)}: Unknown record format version {recordFormatVersion.GetText()}, expected 1 or earlier");
15✔
195

196
                // I would rather not guess about this
197
                return null;
15✔
198
            }
199

200
            var result = new ReaderFileRecorderXml();
2,415✔
201
            result.record = record;
2,415✔
202
            result.fileIdentifier = identifier;
2,415✔
203
            result.userSettings = userSettings;
2,415✔
204

205
            return result;
2,415✔
206
        }
2,445✔
207

208
        public override List<ReaderRef> ParseRefs()
209
        {
2,415✔
210
            var result = new List<ReaderRef>();
2,415✔
211

212
            var refs = record.ElementNamed("refs");
2,415✔
213
            if (refs != null)
2,415✔
214
            {
965✔
215
                foreach (var reference in refs.Elements())
415,375✔
216
                {
206,240✔
217
                    var readerRef = new ReaderRef();
206,240✔
218

219
                    var context = new InputContext(fileIdentifier, reference);
206,240✔
220

221
                    if (reference.Name.LocalName != "Ref")
206,240✔
222
                    {
5✔
223
                        Dbg.Wrn($"{context}: Reference element should be named 'Ref'");
5✔
224
                    }
5✔
225

226
                    readerRef.id = reference.Attribute("id")?.Value;
206,240✔
227
                    if (readerRef.id == null)
206,240✔
228
                    {
5✔
229
                        Dbg.Err($"{context}: Missing reference ID");
5✔
230
                        continue;
5✔
231
                    }
232

233
                    // Further steps don't know how to deal with this, so we just strip it
234
                    reference.Attribute("id").Remove();
206,235✔
235

236
                    var className = reference.Attribute("class")?.Value;
206,235✔
237
                    if (className == null)
206,235✔
238
                    {
5✔
239
                        Dbg.Err($"{context}: Missing reference class name");
5✔
240
                        continue;
5✔
241
                    }
242

243
                    readerRef.type = (Type)Serialization.ParseString(className, typeof(Type), null, context);
206,230✔
244
                    if (readerRef.type.IsValueType)
206,230✔
245
                    {
5✔
246
                        Dbg.Err($"{context}: Reference assigned type {readerRef.type}, which is a value type");
5✔
247
                        continue;
5✔
248
                    }
249

250
                    readerRef.node = new ReaderNodeXml(reference, fileIdentifier, userSettings);
206,225✔
251
                    result.Add(readerRef);
206,225✔
252
                }
206,225✔
253
            }
965✔
254

255
            return result;
2,415✔
256
        }
2,415✔
257

258
        public override ReaderNodeParseable ParseNode()
259
        {
2,415✔
260
            var data = record.ElementNamed("data");
2,415✔
261
            if (data == null)
2,415✔
262
            {
5✔
263
                Dbg.Err($"{new InputContext(fileIdentifier, record)}: No data element provided. This is not very recoverable.");
5✔
264

265
                return null;
5✔
266
            }
267

268
            return new ReaderNodeXml(data, fileIdentifier, userSettings);
2,410✔
269
        }
2,415✔
270
    }
271

272
    internal class ReaderNodeXml : ReaderNodeParseable
273
    {
274
        public override Recorder.IUserSettings UserSettings { get; }
1,159,399✔
275

276
        public ReaderNodeXml(XElement xml, string fileIdentifier, Recorder.IUserSettings userSettings)
1,414,029✔
277
        {
1,414,029✔
278
            this.UserSettings = userSettings;
1,414,029✔
279
            this.xml = xml;
1,414,029✔
280
            this.fileIdentifier = fileIdentifier;
1,414,029✔
281
        }
1,414,029✔
282

283
        public override InputContext GetInputContext()
284
        {
3,380,550✔
285
            return new InputContext(fileIdentifier, xml);
3,380,550✔
286
        }
3,380,550✔
287

288
        public override ReaderNode GetChildNamed(string name)
289
        {
634,270✔
290
            var child = xml.ElementNamed(name);
634,270✔
291
            return child == null ? null : new ReaderNodeXml(child, fileIdentifier, UserSettings);
634,270✔
292
        }
634,270✔
293
        public override string[] GetAllChildren()
294
        {
319,150✔
295
            return xml.Elements().Select(e => e.Name.LocalName).ToArray();
953,090✔
296
        }
319,150✔
297

298
        public override string GetText()
299
        {
1,804,180✔
300
            return xml.GetText();
1,804,180✔
301
        }
1,804,180✔
302

303
        public override string GetMetadata(Metadata metadata)
304
        {
12,715,811✔
305
            return xml.Attribute(metadata.ToLowerString())?.Value;
12,715,811✔
306
        }
12,715,811✔
307

308
        private readonly HashSet<string> metadataNames = UtilMisc.GetEnumValues<Metadata>().Select(metadata => metadata.ToLowerString()).ToHashSet();
7,070,145✔
309
        public override string GetMetadataUnrecognized()
310
        {
1,412,859✔
311
            if (!xml.HasAttributes)
1,412,859✔
312
            {
661,386✔
313
                return null;
661,386✔
314
            }
315

316
            var unrecognized = string.Join(", ", xml.Attributes().Select(attr => attr.Name.LocalName).Where(name => !metadataNames.Contains(name)));
2,255,289✔
317
            return unrecognized == string.Empty ? null : unrecognized;
751,473✔
318
        }
1,412,859✔
319

320
        public override bool HasChildren()
321
        {
1,411,419✔
322
            return xml.Elements().Any();
1,411,419✔
323
        }
1,411,419✔
324

325
        public override int[] GetArrayDimensions(int rank)
326
        {
875✔
327
            // The actual processing will be handled by ParseArray, so we're not doing much validation here right now
328
            int[] results = new int[rank];
875✔
329
            var tier = xml;
875✔
330
            for (int i = 0; i < rank; ++i)
3,740✔
331
            {
1,065✔
332
                results[i] = tier.Elements().Count();
1,065✔
333

334
                tier = tier.Elements().FirstOrDefault();
1,065✔
335
                if (tier == null)
1,065✔
336
                {
70✔
337
                    // ran out of elements; stop now, we'll leave them full of 0's
338
                    break;
70✔
339
                }
340
            }
995✔
341

342
            return results;
875✔
343
        }
875✔
344

345
        public override void ParseList(IList list, Type referencedType, ReaderContext readerContext, Recorder.Context recorderContext)
346
        {
45,375✔
347
            var recorderChildContext = recorderContext.CreateChild();
45,375✔
348

349
            foreach (var fieldElement in xml.Elements())
350,375✔
350
            {
107,125✔
351
                if (fieldElement.Name.LocalName != "li")
107,125✔
352
                {
40✔
353
                    var elementContext = new InputContext(fileIdentifier, fieldElement);
40✔
354
                    Dbg.Err($"{elementContext}: Tag should be <li>, is <{fieldElement.Name.LocalName}>");
40✔
355
                }
40✔
356

357
                list.Add(Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(fieldElement, fileIdentifier, UserSettings) }, referencedType, null, readerContext, recorderChildContext));
107,125✔
358
            }
107,125✔
359

360
            list.GetType().GetField("_version", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(list, Util.CollectionDeserializationVersion);
45,375✔
361
        }
45,375✔
362

363
        private void ParseArrayRank(ReaderNodeXml node, ReaderContext readerContext, Recorder.Context recorderContext, Array value, Type referencedType, int rank, int[] indices, int startAt)
364
        {
1,425✔
365
            if (rank == indices.Length)
1,425✔
366
            {
825✔
367
                value.SetValue(Serialization.ParseElement(new List<ReaderNodeParseable>() { node }, referencedType, null, readerContext, recorderContext), indices);
825✔
368
            }
825✔
369
            else
370
            {
600✔
371
                // this is kind of unnecessary but it's also an irrelevant perf hit
372
                var recorderChildContext = recorderContext.CreateChild();
600✔
373

374
                int elementCount = node.xml.Elements().Count();
600✔
375
                int rankLength = value.GetLength(rank);
600✔
376
                if (elementCount > rankLength)
600✔
377
                {
15✔
378
                    Dbg.Err($"{node.GetInputContext()}: Array dimension {rank} expects {rankLength} elements but got {elementCount}; truncating");
15✔
379
                }
15✔
380
                else if (elementCount < rankLength)
585✔
381
                {
30✔
382
                    Dbg.Err($"{node.GetInputContext()}: Array dimension {rank} expects {rankLength} elements but got {elementCount}; padding with default values");
30✔
383
                }
30✔
384

385
                int i = 0;
600✔
386
                foreach (var fieldElement in node.xml.Elements())
4,485✔
387
                {
1,350✔
388
                    if (i >= rankLength)
1,350✔
389
                    {
15✔
390
                        // truncate, we're done here
391
                        break;
15✔
392
                    }
393

394
                    if (fieldElement.Name.LocalName != "li")
1,335✔
395
                    {
×
396
                        var elementContext = new InputContext(fileIdentifier, fieldElement);
×
397
                        Dbg.Err($"{elementContext}: Tag should be <li>, is <{fieldElement.Name.LocalName}>");
×
398
                    }
×
399

400
                    indices[rank] = startAt + i++;
1,335✔
401
                    ParseArrayRank(new ReaderNodeXml(fieldElement, fileIdentifier, UserSettings), readerContext, recorderChildContext, value, referencedType, rank + 1, indices, 0);
1,335✔
402
                }
1,335✔
403
            }
600✔
404
        }
1,425✔
405

406
        public override void ParseArray(Array array, Type referencedType, ReaderContext readerContext, Recorder.Context recorderContext, int startOffset)
407
        {
850✔
408
            var recorderChildContext = recorderContext.CreateChild();
850✔
409

410
            if (array.Rank == 1)
850✔
411
            {
760✔
412
                // fast path
413
                int i = 0;
760✔
414
                foreach (var fieldElement in xml.Elements())
8,400✔
415
                {
3,060✔
416
                    if (fieldElement.Name.LocalName != "li")
3,060✔
417
                    {
40✔
418
                        var elementContext = new InputContext(fileIdentifier, fieldElement);
40✔
419
                        Dbg.Err($"{elementContext}: Tag should be <li>, is <{fieldElement.Name.LocalName}>");
40✔
420
                    }
40✔
421

422
                    array.SetValue(Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(fieldElement, fileIdentifier, UserSettings) }, referencedType, null, readerContext, recorderChildContext), startOffset + i++);
3,060✔
423
                }
3,060✔
424
            }
760✔
425
            else
426
            {
90✔
427
                // slow path
428
                var indices = new int[array.Rank];
90✔
429
                ParseArrayRank(this, readerContext, recorderChildContext, array, referencedType, 0, indices, startOffset);
90✔
430
            }
90✔
431
        }
850✔
432

433
        public override void ParseDictionary(IDictionary dict, Type referencedKeyType, Type referencedValueType, ReaderContext readerContext, Recorder.Context recorderContext, bool permitPatch)
434
        {
23,275✔
435
            var recorderChildContext = recorderContext.CreateChild();
23,275✔
436

437
            // avoid the heap allocation if we can
438
            var writtenFields = permitPatch ? new HashSet<object>() : null;
23,275✔
439

440
            foreach (var fieldElement in xml.Elements())
165,085✔
441
            {
47,630✔
442
                var elementContext = new InputContext(fileIdentifier, fieldElement);
47,630✔
443

444
                if (fieldElement.Name.LocalName == "li")
47,630✔
445
                {
45,985✔
446
                    // Treat this like a key/value pair
447
                    var keyNode = fieldElement.ElementNamedWithFallback("key", elementContext, "Dictionary includes li tag without a `key`");
45,985✔
448
                    var valueNode = fieldElement.ElementNamedWithFallback("value", elementContext, "Dictionary includes li tag without a `value`");
45,985✔
449

450
                    if (keyNode == null)
45,985✔
451
                    {
40✔
452
                        // error has already been generated
453
                        continue;
40✔
454
                    }
455

456
                    if (valueNode == null)
45,945✔
457
                    {
20✔
458
                        // error has already been generated
459
                        continue;
20✔
460
                    }
461

462
                    var key = Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(keyNode, fileIdentifier, UserSettings) }, referencedKeyType, null, readerContext, recorderChildContext);
45,925✔
463

464
                    if (key == null)
45,925✔
465
                    {
35✔
466
                        Dbg.Err($"{new InputContext(fileIdentifier, keyNode)}: Dictionary includes null key, skipping pair");
35✔
467
                        continue;
35✔
468
                    }
469

470
                    object originalValue = null;
45,890✔
471
                    if (dict.Contains(key))
45,890✔
472
                    {
60✔
473
                        // 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
474
                        // This is definitely not the common path, though, so, fine
475
                        originalValue = dict[key];
60✔
476

477
                        if (writtenFields == null || writtenFields.Contains(key))
60✔
478
                        {
20✔
479
                            Dbg.Err($"{elementContext}: Dictionary includes duplicate key `{key.ToString()}`");
20✔
480
                        }
20✔
481
                    }
60✔
482

483
                    writtenFields?.Add(key);
45,890✔
484

485
                    dict[key] = Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(valueNode, fileIdentifier,UserSettings) }, referencedValueType, originalValue, readerContext, recorderChildContext);
45,890✔
486
                }
45,890✔
487
                else
488
                {
1,645✔
489
                    var key = Serialization.ParseString(fieldElement.Name.LocalName, referencedKeyType, null, elementContext);
1,645✔
490

491
                    if (key == null)
1,645✔
492
                    {
×
493
                        // it's really rare for this to happen, I think you could do it with a converter but that's it
494
                        Dbg.Err($"{elementContext}: Dictionary includes null key, skipping pair");
×
495

496
                        // just in case . . .
497
                        if (string.Compare(fieldElement.Name.LocalName, "li", true) == 0)
×
498
                        {
×
499
                            Dbg.Err($"{elementContext}: Did you mean to write `li`? This field is case-sensitive.");
×
500
                        }
×
501

502
                        continue;
×
503
                    }
504

505
                    object originalValue = null;
1,645✔
506
                    if (dict.Contains(key))
1,645✔
507
                    {
240✔
508
                        // 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
509
                        // This is definitely not the common path, though, so, fine
510
                        originalValue = dict[key];
240✔
511

512
                        if (writtenFields == null || writtenFields.Contains(key))
240✔
513
                        {
160✔
514
                            Dbg.Err($"{elementContext}: Dictionary includes duplicate key `{key.ToString()}`");
160✔
515
                        }
160✔
516
                    }
240✔
517

518
                    writtenFields?.Add(key);
1,645✔
519

520
                    dict[key] = Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(fieldElement, fileIdentifier, UserSettings) }, referencedValueType, originalValue, readerContext, recorderChildContext);
1,645✔
521
                }
1,645✔
522
            }
47,535✔
523
        }
23,275✔
524

525
        public override void ParseHashset(object hashset, Type referencedType, ReaderContext readerContext, Recorder.Context recorderContext, bool permitPatch)
526
        {
980✔
527
            // This is a gigantic pain because HashSet<> doesn't inherit from any non-generic interface that provides the functionality we want
528
            // So we're stuck doing it all through object and reflection
529
            // Thanks, HashSet
530
            // This might be a performance problem and we'll . . . deal with it later I guess?
531
            // This might actually be a good first place to use IL generation.
532

533
            var containsFunction = hashset.GetType().GetMethod("Contains");
980✔
534
            var addFunction = hashset.GetType().GetMethod("Add");
980✔
535

536
            var recorderChildContext = recorderContext.CreateChild();
980✔
537
            var keyParam = new object[1];   // this is just to cut down on GC churn
980✔
538

539
            // avoid the heap allocation if we can
540
            var writtenFields = permitPatch ? new HashSet<object>() : null;
980✔
541

542
            foreach (var fieldElement in xml.Elements())
9,940✔
543
            {
3,500✔
544
                var elementContext = new InputContext(fileIdentifier, fieldElement);
3,500✔
545

546
                // There's a potential bit of ambiguity here if someone does <li /> and expects that to be an actual string named "li".
547
                // Practically, I think this is less likely than someone doing <li></li> and expecting that to be the empty string.
548
                // And there's no other way to express the empty string.
549
                // So . . . we treat that like the empty string.
550
                if (fieldElement.Name.LocalName == "li")
3,500✔
551
                {
3,140✔
552
                    // Treat this like a full node
553
                    var key = Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(fieldElement, fileIdentifier, UserSettings) }, referencedType, null, readerContext, recorderChildContext);
3,140✔
554

555
                    if (key == null)
3,140✔
556
                    {
×
557
                        Dbg.Err($"{elementContext}: HashSet includes null key, skipping");
×
558
                        continue;
×
559
                    }
560

561
                    keyParam[0] = key;
3,140✔
562

563
                    if ((bool)containsFunction.Invoke(hashset, keyParam) && (writtenFields == null || writtenFields.Contains(key)))
3,140✔
564
                    {
160✔
565
                        Dbg.Err($"{elementContext}: HashSet includes duplicate key `{key.ToString()}`");
160✔
566
                    }
160✔
567
                    writtenFields?.Add(key);
3,140✔
568

569
                    addFunction.Invoke(hashset, keyParam);
3,140✔
570
                }
3,140✔
571
                else
572
                {
360✔
573
                    if (fieldElement.HasElements || !fieldElement.GetText().IsNullOrEmpty())
360✔
574
                    {
40✔
575
                        Dbg.Err($"{elementContext}: HashSet non-li member includes data, ignoring");
40✔
576
                    }
40✔
577

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

580
                    if (key == null)
360✔
581
                    {
×
582
                        // it's really rare for this to happen, I think you could do it with a converter but that's it
583
                        Dbg.Err($"{elementContext}: HashSet includes null key, skipping pair");
×
584
                        continue;
×
585
                    }
586

587
                    keyParam[0] = key;
360✔
588

589
                    if ((bool)containsFunction.Invoke(hashset, keyParam) && (writtenFields == null || writtenFields.Contains(key)))
360✔
590
                    {
20✔
591
                        Dbg.Err($"{elementContext}: HashSet includes duplicate key `{key.ToString()}`");
20✔
592
                    }
20✔
593
                    writtenFields?.Add(key);
360✔
594

595
                    addFunction.Invoke(hashset, keyParam);
360✔
596
                }
360✔
597
            }
3,500✔
598
        }
980✔
599

600
        public override void ParseStack(object stack, Type referencedType, ReaderContext readerContext, Recorder.Context recorderContext)
601
        {
50✔
602
            var pushFunction = stack.GetType().GetMethod("Push");
50✔
603

604
            var recorderChildContext = recorderContext.CreateChild();
50✔
605

606
            foreach (var fieldElement in xml.Elements())
490✔
607
            {
170✔
608
                if (fieldElement.Name.LocalName != "li")
170✔
609
                {
×
610
                    var elementContext = new InputContext(fileIdentifier, fieldElement);
×
611
                    Dbg.Err($"{elementContext}: Tag should be <li>, is <{fieldElement.Name.LocalName}>");
×
612
                }
×
613

614
                pushFunction.Invoke(stack, new object[] { Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(fieldElement, fileIdentifier, UserSettings) }, referencedType, null, readerContext, recorderChildContext) });
170✔
615
            }
170✔
616
        }
50✔
617

618
        public override void ParseQueue(object queue, Type referencedType, ReaderContext readerContext, Recorder.Context recorderContext)
619
        {
50✔
620
            var enqueueFunction = queue.GetType().GetMethod("Enqueue");
50✔
621

622
            var recorderChildContext = recorderContext.CreateChild();
50✔
623

624
            foreach (var fieldElement in xml.Elements())
490✔
625
            {
170✔
626
                if (fieldElement.Name.LocalName != "li")
170✔
627
                {
×
628
                    var elementContext = new InputContext(fileIdentifier, fieldElement);
×
629
                    Dbg.Err($"{elementContext}: Tag should be <li>, is <{fieldElement.Name.LocalName}>");
×
630
                }
×
631

632
                enqueueFunction.Invoke(queue, new object[] { Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(fieldElement, fileIdentifier, UserSettings) }, referencedType, null, readerContext, recorderChildContext) });
170✔
633
            }
170✔
634
        }
50✔
635

636
        public override void ParseTuple(object[] parameters, Type referencedType, IList<string> parameterNames, ReaderContext readerContext, Recorder.Context recorderContext)
637
        {
1,050✔
638
            int expectedCount = referencedType.GenericTypeArguments.Length;
1,050✔
639
            var recorderChildContext = recorderContext.CreateChild();
1,050✔
640

641
            var elements = xml.Elements().ToList();
1,050✔
642

643
            bool hasNonLi = false;
1,050✔
644
            foreach (var elementField in elements)
9,070✔
645
            {
2,960✔
646
                if (elementField.Name.LocalName != "li")
2,960✔
647
                {
650✔
648
                    hasNonLi = true;
650✔
649
                }
650✔
650
            }
2,960✔
651

652
            if (!hasNonLi)
1,050✔
653
            {
830✔
654
                // Treat it like an indexed array
655

656
                if (elements.Count != parameters.Length)
830✔
657
                {
60✔
658
                    Dbg.Err($"{GetInputContext()}: Tuple expects {expectedCount} parameters but got {elements.Count}");
60✔
659
                }
60✔
660

661
                for (int i = 0; i < Math.Min(parameters.Length, elements.Count); ++i)
6,140✔
662
                {
2,240✔
663
                    parameters[i] = Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(elements[i], fileIdentifier, UserSettings) }, referencedType.GenericTypeArguments[i], null, readerContext, recorderChildContext);
2,240✔
664
                }
2,240✔
665

666
                // fill in anything missing
667
                for (int i = Math.Min(parameters.Length, elements.Count); i < parameters.Length; ++i)
1,780✔
668
                {
60✔
669
                    parameters[i] = Serialization.GenerateResultFallback(null, referencedType.GenericTypeArguments[i]);
60✔
670
                }
60✔
671
            }
830✔
672
            else
673
            {
220✔
674
                // We're doing named lookups instead
675
                if (parameterNames == null)
220✔
676
                {
40✔
677
                    parameterNames = UtilMisc.DefaultTupleNames;
40✔
678
                }
40✔
679

680
                if (parameterNames.Count < expectedCount)
220✔
681
                {
×
682
                    Dbg.Err($"{GetInputContext()}: Not enough tuple names (this honestly shouldn't even be possible)");
×
683

684
                    // TODO: handle it
685
                }
×
686

687
                bool[] seen = new bool[expectedCount];
220✔
688
                foreach (var elementItem in elements)
2,060✔
689
                {
700✔
690
                    var elementContext = new InputContext(fileIdentifier, elementItem);
700✔
691

692
                    int index = parameterNames.FirstIndexOf(n => n == elementItem.Name.LocalName);
2,620✔
693

694
                    if (index == -1)
700✔
695
                    {
80✔
696
                        Dbg.Err($"{elementContext}: Found field with unexpected name `{elementItem.Name.LocalName}`");
80✔
697
                        continue;
80✔
698
                    }
699

700
                    if (seen[index])
620✔
701
                    {
20✔
702
                        Dbg.Err($"{elementContext}: Found duplicate of field `{elementItem.Name.LocalName}`");
20✔
703
                    }
20✔
704

705
                    seen[index] = true;
620✔
706
                    parameters[index] = Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(elementItem, fileIdentifier, UserSettings) }, referencedType.GenericTypeArguments[index], null, readerContext, recorderChildContext);
620✔
707
                }
620✔
708

709
                for (int i = 0; i < seen.Length; ++i)
1,840✔
710
                {
700✔
711
                    if (!seen[i])
700✔
712
                    {
100✔
713
                        Dbg.Err($"{GetInputContext()}: Missing field with name `{parameterNames[i]}`");
100✔
714

715
                        // Patch it up as best we can
716
                        parameters[i] = Serialization.GenerateResultFallback(null, referencedType.GenericTypeArguments[i]);
100✔
717
                    }
100✔
718
                }
700✔
719
            }
220✔
720
        }
1,050✔
721

722
        public override void ParseReflection(object obj, ReaderContext readerContext, Recorder.Context recorderContext)
723
        {
89,256✔
724
            var recorderChildContext = recorderContext.CreateChild();
89,256✔
725
            var setFields = new HashSet<string>();
89,256✔
726

727
            var type = obj.GetType();
89,256✔
728

729
            foreach (var fieldElement in xml.Elements())
896,146✔
730
            {
314,204✔
731
                // Check for fields that have been set multiple times
732
                string fieldName = fieldElement.Name.LocalName;
314,204✔
733
                if (setFields.Contains(fieldName))
314,204✔
734
                {
40✔
735
                    Dbg.Err($"{new InputContext(fileIdentifier, fieldElement)}: Duplicate field `{fieldName}`");
40✔
736
                    // Just allow us to fall through; it's an error, but one with a reasonably obvious handling mechanism
737
                }
40✔
738
                setFields.Add(fieldName);
314,204✔
739

740
                var fieldElementInfo = type.GetFieldFromHierarchy(fieldName);
314,204✔
741
                if (fieldElementInfo == null)
314,204✔
742
                {
120✔
743
                    // Try to find a close match, if we can, just for a better error message
744
                    string match = null;
120✔
745
                    string canonicalFieldName = UtilMisc.LooseMatchCanonicalize(fieldName);
120✔
746

747
                    foreach (var testField in type.GetSerializableFieldsFromHierarchy())
660✔
748
                    {
180✔
749
                        if (UtilMisc.LooseMatchCanonicalize(testField.Name) == canonicalFieldName)
180✔
750
                        {
60✔
751
                            match = testField.Name;
60✔
752

753
                            // 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.
754
                            break;
60✔
755
                        }
756
                    }
120✔
757

758
                    if (match != null)
120✔
759
                    {
60✔
760
                        Dbg.Err($"{new InputContext(fileIdentifier, fieldElement)}: Field `{fieldName}` does not exist in type {type}; did you mean `{match}`?");
60✔
761
                    }
60✔
762
                    else
763
                    {
60✔
764
                        Dbg.Err($"{new InputContext(fileIdentifier, fieldElement)}: Field `{fieldName}` does not exist in type {type}");
60✔
765
                    }
60✔
766

767
                    continue;
120✔
768
                }
769

770
                if (fieldElementInfo.GetCustomAttribute<IndexAttribute>() != null)
314,084✔
771
                {
×
772
                    Dbg.Err($"{new InputContext(fileIdentifier, fieldElement)}: Attempting to set index field `{fieldName}`; these are generated by the dec system");
×
773
                    continue;
×
774
                }
775

776
                if (fieldElementInfo.GetCustomAttribute<NonSerializedAttribute>() != null)
314,084✔
777
                {
20✔
778
                    Dbg.Err($"{new InputContext(fileIdentifier, fieldElement)}: Attempting to set nonserialized field `{fieldName}`");
20✔
779
                    continue;
20✔
780
                }
781

782
                fieldElementInfo.SetValue(obj, Serialization.ParseElement(new List<ReaderNodeParseable>() { new ReaderNodeXml(fieldElement, fileIdentifier, UserSettings) }, fieldElementInfo.FieldType, fieldElementInfo.GetValue(obj), readerContext, recorderChildContext, fieldInfo: fieldElementInfo));
314,064✔
783
            }
314,064✔
784
        }
89,256✔
785

786
        private XElement xml;
787
        private string fileIdentifier;
788
    }
789
}
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