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

zorbathut / dec / 16529426855

25 Jul 2025 06:53PM UTC coverage: 89.952% (-0.2%) from 90.121%
16529426855

push

github

zorbathut
Improved error messages on fatally malformed XML.

5264 of 5852 relevant lines covered (89.95%)

217748.61 hits per line

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

96.38
/src/UtilType.cs
1
using System;
2
using System.Collections.Concurrent;
3
using System.Collections.Generic;
4
using System.Linq;
5
using System.Reflection;
6
using System.Text.RegularExpressions;
7

8
namespace Dec
9
{
10

11
    internal static class UtilType
12
    {
13
        // Our Official Type Format:
14
        // Namespaces are separated by .'s, for example, LowerNamespace.UpperNamespace.ClassName
15
        // Member classes are also separted by .'s, which means you can have LowerNamespace.ClassName.MemberClass
16
        // C# wants to do those with +'s, but we're using .'s for readability and XML compatibility reasons.
17
        // Generics currently use <>'s as you would expect. This isn't compatible with XML tags but I'm just kind of living with it for now.
18
        // And yes, this also isn't compatible with C#'s internal types.
19

20
        // When serializing types, we chop off as much of the prefix as we can. When deserializing types, we error if there's ambiguity based on existing prefixes.
21

22
        private struct PrimitiveTypeLookup
23
        {
24
            public Type type;
25
            public string str;
26
        }
27
        private static readonly PrimitiveTypeLookup[] PrimitiveTypes = new PrimitiveTypeLookup[]
12✔
28
        {
12✔
29
            new PrimitiveTypeLookup { type = typeof(bool), str = "bool" },
12✔
30
            new PrimitiveTypeLookup { type = typeof(int), str = "int" },
12✔
31
            new PrimitiveTypeLookup { type = typeof(byte), str = "byte" },
12✔
32
            new PrimitiveTypeLookup { type = typeof(sbyte), str = "sbyte" },
12✔
33
            new PrimitiveTypeLookup { type = typeof(char), str = "char" },
12✔
34
            new PrimitiveTypeLookup { type = typeof(decimal), str = "decimal" },
12✔
35
            new PrimitiveTypeLookup { type = typeof(double), str = "double" },
12✔
36
            new PrimitiveTypeLookup { type = typeof(float), str = "float" },
12✔
37
            new PrimitiveTypeLookup { type = typeof(int), str = "int" },
12✔
38
            new PrimitiveTypeLookup { type = typeof(uint), str = "uint" },
12✔
39
            new PrimitiveTypeLookup { type = typeof(long), str = "long" },
12✔
40
            new PrimitiveTypeLookup { type = typeof(ulong), str = "ulong" },
12✔
41
            new PrimitiveTypeLookup { type = typeof(short), str = "short" },
12✔
42
            new PrimitiveTypeLookup { type = typeof(ushort), str = "ushort" },
12✔
43
            new PrimitiveTypeLookup { type = typeof(object), str = "object" },
12✔
44
            new PrimitiveTypeLookup { type = typeof(string), str = "string" },
12✔
45
        };
12✔
46

47
        private static Regex GenericParameterMatcher = new Regex("`[0-9]+", RegexOptions.Compiled);
12✔
48
        private static Dictionary<(string, int), Type[]> StrippedTypeCache = null;  // this is fine not being concurrent; it's set once and then never modified
12✔
49
        private static Type GetTypeFromAnyAssembly(string text, int gparams, Context context)
50
        {
8,750✔
51
            // This is technically unnecessary if we're not parsing a generic, but we may as well do it because the cache will still be faster for nongenerics.
52
            // If we really wanted a perf boost here, we'd do one pass for non-generic objects, then do it again on a cache miss to fill it with generic stuff.
53
            // But there's probably much better performance boosts to be seen throughout this.
54
            if (StrippedTypeCache == null)
8,750✔
55
            {
2,214✔
56
                StrippedTypeCache = AppDomain.CurrentDomain.GetAssemblies()
2,214✔
57
                    .SelectMany(asm => {
861,857✔
58
                        try
2,214✔
59
                        {
861,857✔
60
                            return asm.GetTypes();
861,857✔
61
                        }
2,214✔
62
                        catch (ReflectionTypeLoadException reflectionException)
×
63
                        {
×
64
                            // This is very difficult to code-coverage - it happens on some platforms sometimes, but not on our automatic test server.
2,214✔
65
                            // To test this, we'd have to create a fake .dll that existed just to trigger this issue.
2,214✔
66
                            return reflectionException.Types.Where(t => t != null);
×
67
                        }
2,214✔
68
                    })
861,857✔
69
                    .Where(t => t.DeclaringType == null)    // we have to split these up anyway, so including declaring types just makes our life a little harder
35,276,346✔
70
                    .Distinct()
2,214✔
71
                    .GroupBy(t => {
22,183,301✔
72
                        if (t.IsGenericType)
22,183,301✔
73
                        {
1,262,514✔
74
                            return (t.FullName.Substring(0, t.FullName.IndexOfUnbounded('`')), t.GetGenericArguments().Length);
1,262,514✔
75
                        }
2,214✔
76
                        else
2,214✔
77
                        {
20,923,001✔
78
                            return (t.FullName, 0);
20,923,001✔
79
                        }
2,214✔
80
                    })
22,183,301✔
81
                    .ToDictionary(
2,214✔
82
                        group => group.Key,
20,947,901✔
83
                        group => group.ToArray());
20,947,901✔
84
            }
2,214✔
85

86
            var result = StrippedTypeCache.TryGetValue((text, gparams));
8,750✔
87

88
            if (result == null)
8,750✔
89
            {
5,430✔
90
                return null;
5,430✔
91
            }
92
            else if (result.Length == 1)
3,320✔
93
            {
3,320✔
94
                return result[0];
3,320✔
95
            }
96
            else
97
            {
×
98
                Dbg.Err($"{context}: Too many types found with name {text}");
×
99
                return result[0];
×
100
            }
101
        }
8,750✔
102

103
        private static Type ParseSubtype(Type root, string text, ref List<Type> genericTypes, Context context)
104
        {
5,962✔
105
            if (root == null)
5,962✔
106
            {
216✔
107
                return null;
216✔
108
            }
109

110
            if (text.Length == 0)
5,746✔
111
            {
3,074✔
112
                return root;
3,074✔
113
            }
114

115
            int previousTypes = genericTypes?.Count ?? 0;
2,672✔
116
            if (!ParsePiece(text, context, out int endIndex, out string token, ref genericTypes))
2,672✔
117
            {
30✔
118
                return null;
30✔
119
            }
120
            int addedTypes = (genericTypes?.Count ?? 0) - previousTypes;
2,642✔
121

122
            Type chosenType;
123
            if (addedTypes == 0)
2,642✔
124
            {
2,492✔
125
                chosenType = root.GetNestedType(token, BindingFlags.Public | BindingFlags.NonPublic);
2,492✔
126
            }
2,492✔
127
            else
128
            {
150✔
129
                chosenType = root.GetNestedType($"{token}`{addedTypes}", BindingFlags.Public | BindingFlags.NonPublic);
150✔
130
            }
150✔
131

132
            // Chain on to another call in case we have a further-nested class-of-class
133
            return ParseSubtype(chosenType, text.SubstringSafe(endIndex), ref genericTypes, context);
2,642✔
134
        }
5,962✔
135

136
        internal static bool ParsePiece(string input, Context context, out int endIndex, out string name, ref List<Type> types)
137
        {
11,452✔
138
            // Isolate the first token; this is the distance from our current index to the first . or < that *isn't* the beginning of a class name.
139
            int nameEnd = Math.Min(input.IndexOfUnbounded('.'), input.IndexOfUnbounded('<', 1));
11,452✔
140
            name = input.Substring(0, nameEnd);
11,452✔
141

142
            // If we have a < we need to extract generic arguments.
143
            if (nameEnd < input.Length && input[nameEnd] == '<')
11,452✔
144
            {
900✔
145
                if (!ParseGenericParams(input.Substring(nameEnd + 1), context, out int endOfGenericsAdjustment, ref types))
900✔
146
                {
48✔
147
                    // just kinda give up to ensure we don't get trapped in a loop
148
                    Dbg.Err($"{context}: Failed to parse generic arguments for type containing {input}");
48✔
149
                    endIndex = input.Length;
48✔
150
                    return false;
48✔
151
                }
152
                endIndex = nameEnd + endOfGenericsAdjustment + 3; // adjustment for <>.
852✔
153
                // . . . but also, make sure we don't have a trailing dot!
154
                if (endIndex == input.Length && input[endIndex - 1] == '.')
852✔
155
                {
12✔
156
                    Dbg.Err($"{context}: Type containing `{input}` has trailing .");
12✔
157
                    return false;
12✔
158
                }
159
            }
840✔
160
            else
161
            {
10,552✔
162
                endIndex = nameEnd + 1; // adjustment for .
10,552✔
163
            }
10,552✔
164

165
            return true;
11,392✔
166
        }
11,452✔
167

168
        // returns false on error
169
        internal static bool ParseGenericParams(string tstring, Context context, out int endIndex, ref List<Type> types)
170
        {
900✔
171
            int depth = 0;
900✔
172
            endIndex = 0;
900✔
173

174
            int typeStart = 0;
900✔
175

176
            for (endIndex = 0; endIndex < tstring.Length; ++endIndex)
15,576✔
177
            {
7,776✔
178
                char c = tstring[endIndex];
7,776✔
179
                switch (c)
7,776✔
180
                {
181
                    case '<':
182
                        depth++;
90✔
183
                        break;
90✔
184

185
                    case '>':
186
                        depth--;
978✔
187
                        break;
978✔
188

189
                    case ',':
190
                        if (depth == 0)
246✔
191
                        {
228✔
192
                            if (types == null)
228✔
193
                            {
198✔
194
                                types = new List<Type>();
198✔
195
                            }
198✔
196
                            types.Add(UtilType.ParseDecFormatted(tstring.Substring(typeStart, endIndex - typeStart).Trim(), context));
228✔
197
                            typeStart = endIndex + 1;
228✔
198
                        }
228✔
199
                        break;
246✔
200
                }
201

202
                if (depth < 0)
7,776✔
203
                {
888✔
204
                    break;
888✔
205
                }
206
            }
6,888✔
207

208
            if (depth != -1)
900✔
209
            {
12✔
210
                Dbg.Err($"{context}: Mismatched angle brackets when parsing generic type component `{tstring}`");
12✔
211
                return false;
12✔
212
            }
213

214
            if (endIndex + 1 < tstring.Length && tstring[endIndex + 1] != '.')
888✔
215
            {
36✔
216
                Dbg.Err($"{context}: Unexpected character after end of generic type definition");
36✔
217
                return false;
36✔
218
            }
219

220
            if (endIndex != typeStart)
852✔
221
            {
816✔
222
                if (types == null)
816✔
223
                {
570✔
224
                    types = new List<Type>();
570✔
225
                }
570✔
226
                types.Add(UtilType.ParseDecFormatted(tstring.Substring(typeStart, endIndex - typeStart).Trim(), context));
816✔
227
            }
816✔
228

229
            return true;
852✔
230
        }
900✔
231

232
        private static Type ParseIndependentType(string text, Context context)
233
        {
4,352✔
234
            // This function just tries to find a class with a specific namespace; we no longer worry about `using`.
235
            // Our challenge is to find a function with the right ordering of generic arguments. NS.Foo`1.Bar is different from NS.Foo.Bar`1, for example.
236
            // We're actually going to transform our input into C#'s generic-argument layout so we can find the right instance easily.
237

238
            // This is complicated by the fact that the compiler can generate class names with <> in them - that is, the *name*, not the generic specialization.
239
            // As an added bonus, the namespace is signaled differently, so we're going to be chopping this up awkwardly as we do it.
240

241
            int nextTokenEnd = 0;
4,352✔
242
            string currentPrefix = "";
4,352✔
243
            while (nextTokenEnd < text.Length)
9,512✔
244
            {
8,780✔
245
                // Parse another chunk
246
                // This involves an unnecessary amount of copying - this should be fixed, but this is currently not the bottleneck anywhere I've seen.
247
                List<Type> genericParameters = null; // avoid churn if we can
8,780✔
248
                if (!ParsePiece(text.Substring(nextTokenEnd), context, out int currentTokenLength, out string tokenText, ref genericParameters))
8,780✔
249
                {
30✔
250
                    // parse error, abort
251
                    return null;
30✔
252
                }
253

254
                // update next token position
255
                nextTokenEnd += currentTokenLength;
8,750✔
256

257
                // update our currentPrefix
258
                if (currentPrefix.Length == 0)
8,750✔
259
                {
4,328✔
260
                    currentPrefix = tokenText;
4,328✔
261
                }
4,328✔
262
                else
263
                {
4,422✔
264
                    currentPrefix += "." + tokenText;
4,422✔
265
                }
4,422✔
266

267
                // This is the thing we're going to test to see if is a class.
268
                var parsedType = GetTypeFromAnyAssembly(currentPrefix, genericParameters?.Count ?? 0, context);
8,750✔
269

270
                if (parsedType != null)
8,750✔
271
                {
3,320✔
272
                    // We found the root! Keep on digging.
273
                    Type primitiveType = ParseSubtype(parsedType, text.SubstringSafe(nextTokenEnd), ref genericParameters, context);
3,320✔
274
                    if (primitiveType != null && genericParameters != null)
3,320✔
275
                    {
468✔
276
                        primitiveType = primitiveType.MakeGenericType(genericParameters.ToArray());
468✔
277
                    }
468✔
278
                    return primitiveType;
3,320✔
279
                }
280

281
                // We did not! Continue on.
282
                if (genericParameters != null)
5,430✔
283
                {
270✔
284
                    // . . . but we can't go past a set of generics, namespace generics aren't a thing. So we're done.
285
                    return null;
270✔
286
                }
287
            }
5,160✔
288

289
            // Ran out of string, no match.
290
            return null;
732✔
291
        }
4,352✔
292

293
        private static Regex ArrayRankParser = new Regex(@"\[([,]*)\]$", RegexOptions.Compiled);
12✔
294
        private static ConcurrentDictionary<string, Type> ParseCache = new ConcurrentDictionary<string, Type>();
12✔
295
        internal static Type ParseDecFormatted(string text, Context context)
296
        {
557,066✔
297
            if (text == "")
557,066✔
298
            {
6✔
299
                Dbg.Err($"{context}: The empty string is not a valid type");
6✔
300
                return null;
6✔
301
            }
302

303
            if (text.Contains("{"))
557,060✔
304
            {
30✔
305
                // There's a bunch of times you might want to put full template typenames in XML and doing so is a pain due to XML treating angled brackets specially
306
                // so we allow for {} in its place
307
                // "why not ()" because that's tuple syntax
308
                // "why not []" because that's array syntax
309
                text = text.Replace("{", "<").Replace("}", ">");
30✔
310
            }
30✔
311

312
            if (ParseCache.TryGetValue(text, out Type cacheVal))
557,060✔
313
            {
536,616✔
314
                if (cacheVal == null)
536,616✔
315
                {
6✔
316
                    Dbg.Err($"{context}: Repeating previous failure to parse type named `{text}`");
6✔
317
                }
6✔
318
                return cacheVal;
536,616✔
319
            }
320

321
            if (Config.TestParameters?.explicitTypes != null)
20,444✔
322
            {
19,016✔
323
                // Test override, we check the test types first
324
                foreach (var explicitType in Config.TestParameters.explicitTypes)
216,276✔
325
                {
88,200✔
326
                    if (text == explicitType.Name)
88,200✔
327
                    {
17,172✔
328
                        ParseCache[text] = explicitType;
17,172✔
329
                        return explicitType;
17,172✔
330
                    }
331
                }
71,028✔
332
            }
1,844✔
333

334
            int arrayRanks = 0;
3,272✔
335
            string originalText = text;
3,272✔
336
            if (ArrayRankParser.Match(text) is Match match && match.Success)
3,272✔
337
            {
162✔
338
                arrayRanks = match.Groups[1].Length + 1;
162✔
339
                text = text.Substring(0, text.Length - match.Length);
162✔
340
            }
162✔
341

342
            // We need to find a class that matches the least number of tokens. Namespaces can't be generics so at most this continues until we hit a namespace.
343
            var possibleTypes = Config.UsingNamespaces
3,272✔
344
                .Select(ns => ParseIndependentType($"{ns}.{text}", context))
4,352✔
345
                .Concat(ParseIndependentType(text, context))
3,272✔
346
                .Where(t => t != null)
7,624✔
347
                .ToArray();
3,272✔
348

349
            Type result;
350
            if (possibleTypes.Length == 0)
3,272✔
351
            {
240✔
352
                Dbg.Err($"{context}: Couldn't find type named `{text}`");
240✔
353
                result = null;
240✔
354
            }
240✔
355
            else if (possibleTypes.Length > 1)
3,032✔
356
            {
42✔
357
                Dbg.Err($"{context}: Found too many types named `{text}` ({possibleTypes.Select(t => t.FullName).ToCommaString()})");
126✔
358
                result = possibleTypes[0];
42✔
359
            }
42✔
360
            else
361
            {
2,990✔
362
                result = possibleTypes[0];
2,990✔
363
            }
2,990✔
364

365
            if (arrayRanks == 1)
3,272✔
366
            {
156✔
367
                // I'm not totally sure why MakeArrayType(1) does the wrong thing here
368
                result = result.MakeArrayType();
156✔
369
            }
156✔
370
            else if (arrayRanks > 1)
3,116✔
371
            {
6✔
372
                result = result.MakeArrayType(arrayRanks);
6✔
373
            }
6✔
374

375
            ParseCache[originalText] = result;
3,272✔
376
            return result;
3,272✔
377
        }
557,066✔
378

379
        private static readonly Regex GenericParameterReplacementRegex = new Regex(@"`\d+", RegexOptions.Compiled);
12✔
380
        private static Dictionary<Type, string> ComposeDecCache = new Dictionary<Type, string>();
12✔
381
        internal static string ComposeDecFormatted(this Type type)
382
        {
404,662✔
383
            if (ComposeDecCache.TryGetValue(type, out string cacheVal))
404,662✔
384
            {
391,376✔
385
                return cacheVal;
391,376✔
386
            }
387

388
            if (Config.TestParameters?.explicitTypes != null)
13,286✔
389
            {
12,002✔
390
                // Test override, we check the test types first
391
                foreach (var explicitType in Config.TestParameters.explicitTypes)
120,924✔
392
                {
47,748✔
393
                    if (type == explicitType)
47,748✔
394
                    {
10,578✔
395
                        string result = explicitType.Name;
10,578✔
396
                        ComposeDecCache[type] = result;
10,578✔
397
                        return result;
10,578✔
398
                    }
399
                }
37,170✔
400
            }
1,424✔
401

402
            {
2,708✔
403
                // Main parsing
404
                Type baseType = type;
2,708✔
405
                if (type.IsConstructedGenericType)
2,708✔
406
                {
432✔
407
                    baseType = type.GetGenericTypeDefinition();
432✔
408
                }
432✔
409

410
                string baseString = baseType.FullName.Replace("+", ".");
2,708✔
411
                string bestPrefix = "";
2,708✔
412
                foreach (var prefix in Config.UsingNamespaces)
9,036✔
413
                {
456✔
414
                    string prospective = prefix + ".";
456✔
415
                    if (bestPrefix.Length < prospective.Length && baseString.StartsWith(prospective))
456✔
416
                    {
390✔
417
                        bestPrefix = prospective;
390✔
418
                    }
390✔
419
                }
456✔
420

421
                baseString = baseString.Remove(0, bestPrefix.Length);
2,708✔
422

423
                string result;
424
                if (type.IsConstructedGenericType)
2,708✔
425
                {
432✔
426
                    var genericTypes = type.GenericTypeArguments.Select(t => t.ComposeDecFormatted()).ToList();
1,062✔
427
                    int paramIndex = 0;
432✔
428
                    result = GenericParameterReplacementRegex.Replace(baseString, match =>
432✔
429
                        {
912✔
430
                            int numParams = int.Parse(match.Value.Substring(1));
912✔
431
                            var replacement = string.Join(", ", genericTypes.GetRange(paramIndex, numParams));
912✔
432
                            paramIndex += numParams;
912✔
433
                            return $"<{replacement}>";
912✔
434
                        });
912✔
435
                }
432✔
436
                else
437
                {
2,276✔
438
                    result = baseString;
2,276✔
439
                }
2,276✔
440

441
                ComposeDecCache[type] = result;
2,708✔
442
                return result;
2,708✔
443
            }
444
        }
404,662✔
445

446
        private static Dictionary<Type, string> ComposeCSCache = new Dictionary<Type, string>();
12✔
447
        internal static string ComposeCSFormatted(this Type type)
448
        {
11,862✔
449
            if (ComposeCSCache.TryGetValue(type, out string cacheVal))
11,862✔
450
            {
8,376✔
451
                return cacheVal;
8,376✔
452
            }
453

454
            string result;
455
            if (type.IsConstructedGenericType)
3,486✔
456
            {
138✔
457
                result = type.GetGenericTypeDefinition().ToString();
138✔
458
                result = result.Substring(0, result.IndexOf('`'));
138✔
459
            }
138✔
460
            else
461
            {
3,348✔
462
                result = type.ToString();
3,348✔
463
            }
3,348✔
464

465
            // strip out the namespace/class distinction
466
            result = result.Replace('+', '.');
3,486✔
467

468
            if (type.IsConstructedGenericType)
3,486✔
469
            {
138✔
470
                result += "<" + string.Join(", ", type.GetGenericArguments().Select(arg => ComposeCSFormatted(arg))) + ">";
312✔
471
            }
138✔
472

473
            ComposeCSCache[type] = result;
3,486✔
474

475
            return result;
3,486✔
476
        }
11,862✔
477

478
        internal static void ClearCache()
479
        {
36,246✔
480
            ComposeDecCache.Clear();
36,246✔
481
            ComposeCSCache.Clear();
36,246✔
482
            ParseCache.Clear();
36,246✔
483
            StrippedTypeCache = null;
36,246✔
484

485
            // Seed with our primitive types
486
            for (int i = 0; i < PrimitiveTypes.Length; ++i)
1,232,364✔
487
            {
579,936✔
488
                ComposeDecCache[PrimitiveTypes[i].type] = PrimitiveTypes[i].str;
579,936✔
489
                ParseCache[PrimitiveTypes[i].str] = PrimitiveTypes[i].type;
579,936✔
490
            }
579,936✔
491
        }
36,246✔
492

493
        static UtilType()
494
        {
12✔
495
            // seed the cache
496
            ClearCache();
12✔
497
        }
12✔
498

499
        internal enum DecDatabaseStatus
500
        {
501
            Invalid,
502
            Abstract,
503
            Root,
504
            Branch,
505
        }
506
        private static Dictionary<Type, DecDatabaseStatus> GetDecDatabaseStatusCache = new Dictionary<Type, DecDatabaseStatus>();
12✔
507
        internal static DecDatabaseStatus GetDecDatabaseStatus(this Type type)
508
        {
78,132✔
509
            if (!GetDecDatabaseStatusCache.TryGetValue(type, out var result))
78,132✔
510
            {
1,344✔
511
                if (!typeof(Dec).IsAssignableFrom(type))
1,344✔
512
                {
6✔
513
                    Dbg.Err($"Queried the dec hierarchy status of a type {type} that doesn't even inherit from Dec.");
6✔
514

515
                    result = DecDatabaseStatus.Invalid;
6✔
516
                }
6✔
517
                else if (type.GetCustomAttribute<AbstractAttribute>(false) != null)
1,338✔
518
                {
72✔
519
                    if (!type.IsAbstract)
72✔
520
                    {
12✔
521
                        Dbg.Err($"Type {type} is tagged Dec.Abstract, but is not abstract.");
12✔
522
                    }
12✔
523

524
                    if (type.BaseType != typeof(object) && GetDecDatabaseStatus(type.BaseType) > DecDatabaseStatus.Abstract)
72✔
525
                    {
18✔
526
                        Dbg.Err($"Type {type} is tagged Dec.Abstract, but inherits from {type.BaseType} which is within the database.");
18✔
527
                    }
18✔
528

529
                    result = DecDatabaseStatus.Abstract;
72✔
530
                }
72✔
531
                else if (type.BaseType.GetCustomAttribute<AbstractAttribute>(false) != null)
1,266✔
532
                {
1,140✔
533
                    // We do this just to validate everything beneath this. It'd better return Abstract! More importantly, it goes through all parents and makes sure they're consistent.
534
                    GetDecDatabaseStatus(type.BaseType);
1,140✔
535

536
                    result = DecDatabaseStatus.Root;
1,140✔
537
                }
1,140✔
538
                else
539
                {
126✔
540
                    // Further validation. This time we really hope it returns Abstract or Root. More importantly, it goes through all parents and makes sure they're consistent.
541
                    GetDecDatabaseStatus(type.BaseType);
126✔
542

543
                    // Our parent isn't NotDatabaseRootAttribute. We are not a database root, but we also can't say anything meaningful about our parents.
544
                    result = DecDatabaseStatus.Branch;
126✔
545
                }
126✔
546

547
                GetDecDatabaseStatusCache.Add(type, result);
1,344✔
548
            }
1,344✔
549

550
            return result;
78,132✔
551
        }
78,132✔
552

553
        private static Dictionary<Type, Type> GetDecRootTypeCache = new Dictionary<Type, Type>();
12✔
554
        internal static Type GetDecRootType(this Type type)
555
        {
139,302✔
556
            if (!GetDecRootTypeCache.TryGetValue(type, out var result))
139,302✔
557
            {
1,290✔
558
                if (GetDecDatabaseStatus(type) <= DecDatabaseStatus.Abstract)
1,290✔
559
                {
36✔
560
                    Dbg.Err($"{type} does not exist within a database hierarchy.");
36✔
561
                    result = null;
36✔
562
                }
36✔
563
                else
564
                {
1,254✔
565
                    Type currentType = type;
1,254✔
566
                    while (GetDecDatabaseStatus(currentType) == DecDatabaseStatus.Branch)
1,398✔
567
                    {
144✔
568
                        currentType = currentType.BaseType;
144✔
569
                    }
144✔
570

571
                    result = currentType;
1,254✔
572
                }
1,254✔
573

574
                GetDecRootTypeCache.Add(type, result);
1,290✔
575
            }
1,290✔
576

577
            return result;
139,302✔
578
        }
139,302✔
579

580
        internal static bool CanBeShared(this Type type)
581
        {
2,792,070✔
582
            return Util.CanBeShared(type);
2,792,070✔
583
        }
2,792,070✔
584

585
        private static ConcurrentDictionary<Type, bool> CanBeCloneCopiedCache = new ConcurrentDictionary<Type, bool>();
12✔
586
        internal static bool CanBeCloneCopied(this Type type)
587
        {
369,120✔
588
            if (CanBeCloneCopiedCache.TryGetValue(type, out var result))
369,120✔
589
            {
368,250✔
590
                return result;
368,250✔
591
            }
592

593
            result = type.IsPrimitive || typeof(Dec).IsAssignableFrom(type) || type == typeof(string) || type == typeof(Type) || type.GetCustomAttribute<CloneStructPiecewiseAttribute>() != null || type.GetCustomAttribute<CloneClassAsSharedRef>() != null;
870✔
594

595
            if (!result)
870✔
596
            {
822✔
597
                // gotta check for a converter
598
                var converter = Serialization.ConverterFor(type);
822✔
599
                if (converter != null && converter.TreatAsValuelike())
822✔
600
                {
×
601
                    result = true;
×
602
                }
×
603
            }
822✔
604

605
            CanBeCloneCopiedCache[type] = result;
870✔
606

607
            return result;
870✔
608
        }
369,120✔
609

610
        private static ConcurrentDictionary<Type, bool> CanBeConstructedCache = new ConcurrentDictionary<Type, bool>();
12✔
611
        internal static bool CanBeConstructed(this Type type)
612
        {
703,722✔
613
            if (CanBeConstructedCache.TryGetValue(type, out var result))
703,722✔
614
            {
703,026✔
615
                return result;
703,026✔
616
            }
617

618
            // Check if the type implements IRecordable
619
            if (typeof(IRecordable).IsAssignableFrom(type))
696✔
620
            {
612✔
621
                // For IRecordable types, check if they can be constructed
622
                result = type.IsValueType || type.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { }, null) != null;
612✔
623
            }
612✔
624
            else
625
            {
84✔
626
                // Check if there's a converter for this type
627
                var converter = Serialization.ConverterFor(type);
84✔
628
                if (converter != null)
84✔
629
                {
84✔
630
                    if (converter is ConverterString)
84✔
631
                    {
30✔
632
                        // ConverterString types can always be recorded as they handle their own construction
633
                        result = true;
30✔
634
                    }
30✔
635
                    else if (converter is ConverterFactory)
54✔
636
                    {
18✔
637
                        // ConverterFactory types can always be recorded as they have their own Create method
638
                        result = true;
18✔
639
                    }
18✔
640
                    else if (converter is ConverterRecord)
36✔
641
                    {
36✔
642
                        // ConverterRecord types need a default constructor
643
                        result = type.IsValueType || type.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { }, null) != null;
36✔
644
                    }
36✔
645
                    else
646
                    {
×
647
                        // Unknown converter type, assume it can't be recorded
648
                        result = false;
×
649
                        Dbg.Err($"Unknown converter type, internal error, please report");
×
650
                    }
×
651
                }
84✔
652
                else
653
                {
×
654
                    // No converter and not IRecordable, cannot be recorded (also how did we get here)
655
                    result = false;
×
656
                }
×
657
            }
84✔
658

659
            CanBeConstructedCache[type] = result;
696✔
660

661
            return result;
696✔
662
        }
703,722✔
663

664
        internal enum ParseModeCategory
665
        {
666
            Dec,
667
            Object,
668
            OrderedContainer,
669
            UnorderedContainer,
670
            Value,
671
        }
672
        internal static ParseModeCategory CalculateSerializationModeCategory(this Type type, Converter converter, bool isRootDec)
673
        {
1,824,594✔
674
            if (isRootDec && typeof(Dec).IsAssignableFrom(type))
1,824,594✔
675
            {
×
676
                return ParseModeCategory.Dec;
×
677
            }
678
            else if (false
1,824,594✔
679
                || type.IsPrimitive
1,824,594✔
680
                || type == typeof(string)
1,824,594✔
681
                || type == typeof(Type)
1,824,594✔
682
                || (!isRootDec && typeof(Dec).IsAssignableFrom(type)) // unnecessary isRootDec test here is to shortcut the expensive IsAssignableFrom call
1,824,594✔
683
                || typeof(Enum).IsAssignableFrom(type)
1,824,594✔
684
                || converter is ConverterString
1,824,594✔
685
                || (System.ComponentModel.TypeDescriptor.GetConverter(type)?.CanConvertFrom(typeof(string)) ?? false)   // this is last because it's slow
1,824,594✔
686
            )
1,824,594✔
687
            {
481,496✔
688
                return ParseModeCategory.Value;
481,496✔
689
            }
690
            else if (
1,343,098✔
691
                (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) ||
1,343,098✔
692
                type.IsArray ||
1,343,098✔
693
                (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Stack<>)) ||
1,343,098✔
694
                (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Queue<>))
1,343,098✔
695
            )
1,343,098✔
696
            {
159,810✔
697
                return ParseModeCategory.OrderedContainer;
159,810✔
698
            }
699
            else if (
1,183,288✔
700
                (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) ||
1,183,288✔
701
                (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(HashSet<>))
1,183,288✔
702
            )
1,183,288✔
703
            {
43,542✔
704
                return ParseModeCategory.UnorderedContainer;
43,542✔
705
            }
706
            else if (type.IsGenericType && (
1,139,746✔
707
                    type.GetGenericTypeDefinition() == typeof(Tuple<>) ||
1,139,746✔
708
                    type.GetGenericTypeDefinition() == typeof(Tuple<,>) ||
1,139,746✔
709
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,>) ||
1,139,746✔
710
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,>) ||
1,139,746✔
711
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,>) ||
1,139,746✔
712
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,>) ||
1,139,746✔
713
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,>) ||
1,139,746✔
714
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,,>) ||
1,139,746✔
715
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<>) ||
1,139,746✔
716
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,>) ||
1,139,746✔
717
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,>) ||
1,139,746✔
718
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,>) ||
1,139,746✔
719
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,>) ||
1,139,746✔
720
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,>) ||
1,139,746✔
721
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,>) ||
1,139,746✔
722
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,,>)
1,139,746✔
723
                ))
1,139,746✔
724
            {
1,284✔
725
                return ParseModeCategory.Value;
1,284✔
726
            }
727
            else
728
            {
1,138,462✔
729
                return ParseModeCategory.Object;
1,138,462✔
730
            }
731
        }
1,824,594✔
732
    }
733
}
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