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

zorbathut / dec / 11139544798

02 Oct 2024 07:04AM UTC coverage: 90.567% (-0.1%) from 90.69%
11139544798

push

github

zorbathut
Fix several Clone pathways that would incorrectly Record objects with a TreatAsValuelike converter, also improving performance.

4570 of 5046 relevant lines covered (90.57%)

191526.06 hits per line

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

97.61
/src/UtilType.cs
1
using System.Collections.Concurrent;
2

3
namespace Dec
4
{
5
    using System;
6
    using System.Collections.Generic;
7
    using System.Linq;
8
    using System.Reflection;
9
    using System.Text.RegularExpressions;
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[]
10✔
28
        {
10✔
29
            new PrimitiveTypeLookup { type = typeof(bool), str = "bool" },
10✔
30
            new PrimitiveTypeLookup { type = typeof(int), str = "int" },
10✔
31
            new PrimitiveTypeLookup { type = typeof(byte), str = "byte" },
10✔
32
            new PrimitiveTypeLookup { type = typeof(sbyte), str = "sbyte" },
10✔
33
            new PrimitiveTypeLookup { type = typeof(char), str = "char" },
10✔
34
            new PrimitiveTypeLookup { type = typeof(decimal), str = "decimal" },
10✔
35
            new PrimitiveTypeLookup { type = typeof(double), str = "double" },
10✔
36
            new PrimitiveTypeLookup { type = typeof(float), str = "float" },
10✔
37
            new PrimitiveTypeLookup { type = typeof(int), str = "int" },
10✔
38
            new PrimitiveTypeLookup { type = typeof(uint), str = "uint" },
10✔
39
            new PrimitiveTypeLookup { type = typeof(long), str = "long" },
10✔
40
            new PrimitiveTypeLookup { type = typeof(ulong), str = "ulong" },
10✔
41
            new PrimitiveTypeLookup { type = typeof(short), str = "short" },
10✔
42
            new PrimitiveTypeLookup { type = typeof(ushort), str = "ushort" },
10✔
43
            new PrimitiveTypeLookup { type = typeof(object), str = "object" },
10✔
44
            new PrimitiveTypeLookup { type = typeof(string), str = "string" },
10✔
45
        };
10✔
46

47
        private static Regex GenericParameterMatcher = new Regex("`[0-9]+", RegexOptions.Compiled);
10✔
48
        private static Dictionary<(string, int), Type[]> StrippedTypeCache = null;  // this is fine not being concurrent; it's set once and then never modified
10✔
49
        private static Type GetTypeFromAnyAssembly(string text, int gparams, InputContext context)
50
        {
6,749✔
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)
6,749✔
55
            {
1,695✔
56
                StrippedTypeCache = AppDomain.CurrentDomain.GetAssemblies()
1,695✔
57
                    .SelectMany(asm => {
631,452✔
58
                        try
1,695✔
59
                        {
631,452✔
60
                            return asm.GetTypes();
631,452✔
61
                        }
1,695✔
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.
1,695✔
65
                            // To test this, we'd have to create a fake .dll that existed just to trigger this issue.
1,695✔
66
                            return reflectionException.Types.Where(t => t != null);
×
67
                        }
1,695✔
68
                    })
631,452✔
69
                    .Where(t => t.DeclaringType == null)    // we have to split these up anyway, so including declaring types just makes our life a little harder
26,534,244✔
70
                    .Distinct()
1,695✔
71
                    .GroupBy(t => {
16,743,665✔
72
                        if (t.IsGenericType)
16,743,665✔
73
                        {
916,117✔
74
                            return (t.FullName.Substring(0, t.FullName.IndexOfUnbounded('`')), t.GetGenericArguments().Length);
916,117✔
75
                        }
1,695✔
76
                        else
1,695✔
77
                        {
15,829,243✔
78
                            return (t.FullName, 0);
15,829,243✔
79
                        }
1,695✔
80
                    })
16,743,665✔
81
                    .ToDictionary(
1,695✔
82
                        group => group.Key,
15,841,588✔
83
                        group => group.ToArray());
15,841,588✔
84
            }
1,695✔
85

86
            var result = StrippedTypeCache.TryGetValue((text, gparams));
6,749✔
87

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

103
        private static Type ParseSubtype(Type root, string text, ref List<Type> genericTypes, InputContext context)
104
        {
4,562✔
105
            if (root == null)
4,562✔
106
            {
180✔
107
                return null;
180✔
108
            }
109

110
            if (text.Length == 0)
4,382✔
111
            {
2,311✔
112
                return root;
2,311✔
113
            }
114

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

122
            Type chosenType;
123
            if (addedTypes == 0)
2,046✔
124
            {
1,921✔
125
                chosenType = root.GetNestedType(token, BindingFlags.Public | BindingFlags.NonPublic);
1,921✔
126
            }
1,921✔
127
            else
128
            {
125✔
129
                chosenType = root.GetNestedType($"{token}`{addedTypes}", BindingFlags.Public | BindingFlags.NonPublic);
125✔
130
            }
125✔
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,046✔
134
        }
4,562✔
135

136
        internal static bool ParsePiece(string input, InputContext context, out int endIndex, out string name, ref List<Type> types)
137
        {
8,845✔
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));
8,845✔
140
            name = input.Substring(0, nameEnd);
8,845✔
141

142
            // If we have a < we need to extract generic arguments.
143
            if (nameEnd < input.Length && input[nameEnd] == '<')
8,845✔
144
            {
731✔
145
                if (!ParseGenericParams(input.Substring(nameEnd + 1), context, out int endOfGenericsAdjustment, ref types))
731✔
146
                {
40✔
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}");
40✔
149
                    endIndex = input.Length;
40✔
150
                    return false;
40✔
151
                }
152
                endIndex = nameEnd + endOfGenericsAdjustment + 3; // adjustment for <>.
691✔
153
                // . . . but also, make sure we don't have a trailing dot!
154
                if (endIndex == input.Length && input[endIndex - 1] == '.')
691✔
155
                {
10✔
156
                    Dbg.Err($"{context}: Type containing `{input}` has trailing .");
10✔
157
                    return false;
10✔
158
                }
159
            }
681✔
160
            else
161
            {
8,114✔
162
                endIndex = nameEnd + 1; // adjustment for .
8,114✔
163
            }
8,114✔
164

165
            return true;
8,795✔
166
        }
8,845✔
167

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

174
            int typeStart = 0;
731✔
175

176
            for (endIndex = 0; endIndex < tstring.Length; ++endIndex)
12,714✔
177
            {
6,347✔
178
                char c = tstring[endIndex];
6,347✔
179
                switch (c)
6,347✔
180
                {
181
                    case '<':
182
                        depth++;
75✔
183
                        break;
75✔
184

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

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

202
                if (depth < 0)
6,347✔
203
                {
721✔
204
                    break;
721✔
205
                }
206
            }
5,626✔
207

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

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

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

229
            return true;
691✔
230
        }
731✔
231

232
        private static Type ParseIndependentType(string text, InputContext context)
233
        {
3,376✔
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;
3,376✔
242
            string currentPrefix = "";
3,376✔
243
            while (nextTokenEnd < text.Length)
7,384✔
244
            {
6,774✔
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
6,774✔
248
                if (!ParsePiece(text.Substring(nextTokenEnd), context, out int currentTokenLength, out string tokenText, ref genericParameters))
6,774✔
249
                {
25✔
250
                    // parse error, abort
251
                    return null;
25✔
252
                }
253

254
                // update next token position
255
                nextTokenEnd += currentTokenLength;
6,749✔
256

257
                // update our currentPrefix
258
                if (currentPrefix.Length == 0)
6,749✔
259
                {
3,356✔
260
                    currentPrefix = tokenText;
3,356✔
261
                }
3,356✔
262
                else
263
                {
3,393✔
264
                    currentPrefix += "." + tokenText;
3,393✔
265
                }
3,393✔
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);
6,749✔
269

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

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

289
            // Ran out of string, no match.
290
            return null;
610✔
291
        }
3,376✔
292

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

303
            if (text.Contains("{"))
462,837✔
304
            {
25✔
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("}", ">");
25✔
310
            }
25✔
311

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

321
            if (Config.TestParameters?.explicitTypes != null)
15,906✔
322
            {
14,896✔
323
                // Test override, we check the test types first
324
                foreach (var explicitType in Config.TestParameters.explicitTypes)
176,544✔
325
                {
72,643✔
326
                    if (text == explicitType.Name)
72,643✔
327
                    {
13,430✔
328
                        ParseCache[text] = explicitType;
13,430✔
329
                        return explicitType;
13,430✔
330
                    }
331
                }
59,213✔
332
            }
1,466✔
333

334
            int arrayRanks = 0;
2,476✔
335
            string originalText = text;
2,476✔
336
            if (ArrayRankParser.Match(text) is Match match && match.Success)
2,476✔
337
            {
60✔
338
                arrayRanks = match.Groups[1].Length + 1;
60✔
339
                text = text.Substring(0, text.Length - match.Length);
60✔
340
            }
60✔
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
2,476✔
344
                .Select(ns => ParseIndependentType($"{ns}.{text}", context))
3,376✔
345
                .Concat(ParseIndependentType(text, context))
2,476✔
346
                .Where(t => t != null)
5,852✔
347
                .ToArray();
2,476✔
348

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

365
            if (arrayRanks == 1)
2,476✔
366
            {
55✔
367
                // I'm not totally sure why MakeArrayType(1) does the wrong thing here
368
                result = result.MakeArrayType();
55✔
369
            }
55✔
370
            else if (arrayRanks > 1)
2,421✔
371
            {
5✔
372
                result = result.MakeArrayType(arrayRanks);
5✔
373
            }
5✔
374

375
            ParseCache[originalText] = result;
2,476✔
376
            return result;
2,476✔
377
        }
462,842✔
378

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

388
            if (Config.TestParameters?.explicitTypes != null)
6,106✔
389
            {
5,271✔
390
                // Test override, we check the test types first
391
                foreach (var explicitType in Config.TestParameters.explicitTypes)
59,329✔
392
                {
23,878✔
393
                    if (type == explicitType)
23,878✔
394
                    {
4,240✔
395
                        string result = explicitType.Name;
4,240✔
396
                        ComposeDecCache[type] = result;
4,240✔
397
                        return result;
4,240✔
398
                    }
399
                }
19,638✔
400
            }
1,031✔
401

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

410
                string baseString = baseType.FullName.Replace("+", ".");
1,866✔
411
                string bestPrefix = "";
1,866✔
412
                foreach (var prefix in Config.UsingNamespaces)
6,358✔
413
                {
380✔
414
                    string prospective = prefix + ".";
380✔
415
                    if (bestPrefix.Length < prospective.Length && baseString.StartsWith(prospective))
380✔
416
                    {
325✔
417
                        bestPrefix = prospective;
325✔
418
                    }
325✔
419
                }
380✔
420

421
                baseString = baseString.Remove(0, bestPrefix.Length);
1,866✔
422

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

441
                ComposeDecCache[type] = result;
1,866✔
442
                return result;
1,866✔
443
            }
444
        }
222,967✔
445

446
        private static Dictionary<Type, string> ComposeCSCache = new Dictionary<Type, string>();
10✔
447
        internal static string ComposeCSFormatted(this Type type)
448
        {
9,695✔
449
            if (ComposeCSCache.TryGetValue(type, out string cacheVal))
9,695✔
450
            {
6,955✔
451
                return cacheVal;
6,955✔
452
            }
453

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

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

468
            if (type.IsConstructedGenericType)
2,740✔
469
            {
95✔
470
                result += "<" + string.Join(", ", type.GetGenericArguments().Select(arg => ComposeCSFormatted(arg))) + ">";
220✔
471
            }
95✔
472

473
            ComposeCSCache[type] = result;
2,740✔
474

475
            return result;
2,740✔
476
        }
9,695✔
477

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

485
            // Seed with our primitive types
486
            for (int i = 0; i < PrimitiveTypes.Length; ++i)
905,080✔
487
            {
425,920✔
488
                ComposeDecCache[PrimitiveTypes[i].type] = PrimitiveTypes[i].str;
425,920✔
489
                ParseCache[PrimitiveTypes[i].str] = PrimitiveTypes[i].type;
425,920✔
490
            }
425,920✔
491
        }
26,620✔
492

493
        static UtilType()
494
        {
10✔
495
            // seed the cache
496
            ClearCache();
10✔
497
        }
10✔
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>();
10✔
507
        internal static DecDatabaseStatus GetDecDatabaseStatus(this Type type)
508
        {
63,230✔
509
            if (!GetDecDatabaseStatusCache.TryGetValue(type, out var result))
63,230✔
510
            {
1,095✔
511
                if (!typeof(Dec).IsAssignableFrom(type))
1,095✔
512
                {
5✔
513
                    Dbg.Err($"Queried the dec hierarchy status of a type {type} that doesn't even inherit from Dec.");
5✔
514

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

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

529
                    result = DecDatabaseStatus.Abstract;
60✔
530
                }
60✔
531
                else if (type.BaseType.GetCustomAttribute<AbstractAttribute>(false) != null)
1,030✔
532
                {
925✔
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);
925✔
535

536
                    result = DecDatabaseStatus.Root;
925✔
537
                }
925✔
538
                else
539
                {
105✔
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);
105✔
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;
105✔
545
                }
105✔
546

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

550
            return result;
63,230✔
551
        }
63,230✔
552

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

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

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

577
            return result;
114,050✔
578
        }
114,050✔
579

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

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

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

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

605
            CanBeCloneCopiedCache[type] = result;
605✔
606

607
            return result;
605✔
608
        }
295,940✔
609

610
        internal enum ParseModeCategory
611
        {
612
            Dec,
613
            Object,
614
            OrderedContainer,
615
            UnorderedContainer,
616
            Value,
617
        }
618
        internal static ParseModeCategory CalculateSerializationModeCategory(this Type type, Converter converter, bool isRootDec)
619
        {
1,511,554✔
620
            if (isRootDec && typeof(Dec).IsAssignableFrom(type))
1,511,554✔
621
            {
×
622
                return ParseModeCategory.Dec;
×
623
            }
624
            else if (false
1,511,554✔
625
                || type.IsPrimitive
1,511,554✔
626
                || type == typeof(string)
1,511,554✔
627
                || type == typeof(Type)
1,511,554✔
628
                || (!isRootDec && typeof(Dec).IsAssignableFrom(type)) // unnecessary isRootDec test here is to shortcut the expensive IsAssignableFrom call
1,511,554✔
629
                || typeof(Enum).IsAssignableFrom(type)
1,511,554✔
630
                || converter is ConverterString
1,511,554✔
631
                || (System.ComponentModel.TypeDescriptor.GetConverter(type)?.CanConvertFrom(typeof(string)) ?? false)   // this is last because it's slow
1,511,554✔
632
            )
1,511,554✔
633
            {
400,351✔
634
                return ParseModeCategory.Value;
400,351✔
635
            }
636
            else if (
1,111,203✔
637
                (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) ||
1,111,203✔
638
                type.IsArray ||
1,111,203✔
639
                (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Stack<>)) ||
1,111,203✔
640
                (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Queue<>))
1,111,203✔
641
            )
1,111,203✔
642
            {
131,030✔
643
                return ParseModeCategory.OrderedContainer;
131,030✔
644
            }
645
            else if (
980,173✔
646
                (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) ||
980,173✔
647
                (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(HashSet<>))
980,173✔
648
            )
980,173✔
649
            {
35,665✔
650
                return ParseModeCategory.UnorderedContainer;
35,665✔
651
            }
652
            else if (type.IsGenericType && (
944,508✔
653
                    type.GetGenericTypeDefinition() == typeof(Tuple<>) ||
944,508✔
654
                    type.GetGenericTypeDefinition() == typeof(Tuple<,>) ||
944,508✔
655
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,>) ||
944,508✔
656
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,>) ||
944,508✔
657
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,>) ||
944,508✔
658
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,>) ||
944,508✔
659
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,>) ||
944,508✔
660
                    type.GetGenericTypeDefinition() == typeof(Tuple<,,,,,,,>) ||
944,508✔
661
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<>) ||
944,508✔
662
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,>) ||
944,508✔
663
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,>) ||
944,508✔
664
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,>) ||
944,508✔
665
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,>) ||
944,508✔
666
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,>) ||
944,508✔
667
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,>) ||
944,508✔
668
                    type.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,,>)
944,508✔
669
                ))
944,508✔
670
            {
1,065✔
671
                return ParseModeCategory.Value;
1,065✔
672
            }
673
            else
674
            {
943,443✔
675
                return ParseModeCategory.Object;
943,443✔
676
            }
677
        }
1,511,554✔
678
    }
679
}
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