Skip to content

Commit ad29f4d

Browse files
committed
Added the TypeHelper utility class
1 parent 0357f12 commit ad29f4d

File tree

3 files changed

+195
-0
lines changed

3 files changed

+195
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ Fixes
1616
- Fixed undrawn elements not updating their area correctly even if their children are drawn or accessed
1717
- Fixed rare unbound recursion issue with auto-hiding scroll bars and elements that just about don't fit the panel
1818

19+
### MLEM.Data
20+
Additions
21+
- Added the TypeHelper utility class
22+
1923
## 8.0.0
2024

2125
### MLEM

MLEM.Data/TypeHelper.cs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
5+
namespace MLEM.Data {
6+
/// <summary>
7+
/// This class contains a set of static helper methods for dealing with the string representations of <see cref="Type"/> names, including generic and non-generic types, and especially for converting between types and their <see cref="Type.AssemblyQualifiedName"/>.
8+
/// </summary>
9+
public class TypeHelper {
10+
11+
/// <summary>
12+
/// Returns the assembly-qualified names of the generic type arguments that the type with the given <paramref name="assemblyQualifiedName"/> has.
13+
/// Note that "recursive" generic type arguments (ie. generic type arguments that themselves have generic type arguments) are not automatically resolved.
14+
/// </summary>
15+
/// <param name="assemblyQualifiedName">The type's name, which can either be assembly-qualified or not.</param>
16+
/// <returns>The generic type arguments the the given type has, or an empty collection if it is not generic.</returns>
17+
/// <remarks>
18+
/// Adapted from https://github.com/JamesNK/Newtonsoft.Json/blob/4e13299d4b0ec96bd4df9954ef646bd2d1b5bf2a/Src/Newtonsoft.Json/Serialization/DefaultSerializationBinder.cs#L136.
19+
/// </remarks>
20+
public static IEnumerable<string> GetGenericTypeArguments(string assemblyQualifiedName) {
21+
var genericStart = assemblyQualifiedName.IndexOf('[');
22+
if (genericStart >= 0) {
23+
var depth = 0;
24+
var argStart = 0;
25+
for (var i = genericStart + 1; i < assemblyQualifiedName.Length - 1; i++) {
26+
var c = assemblyQualifiedName[i];
27+
if (c == '[') {
28+
if (depth == 0)
29+
argStart = i + 1;
30+
depth++;
31+
} else if (c == ']') {
32+
depth--;
33+
if (depth == 0)
34+
yield return assemblyQualifiedName.Substring(argStart, i - argStart).Trim();
35+
}
36+
}
37+
}
38+
}
39+
40+
/// <summary>
41+
/// Removes the assembly metadata from the given <paramref name="assemblyQualifiedName"/>, specifically the version, public key token, and further metadata after the assembly name.
42+
/// A non-assembly-qualified type name is returned unchanged.
43+
/// </summary>
44+
/// <param name="assemblyQualifiedName">The type's name, which can either be assembly-qualified or not.</param>
45+
/// <returns>The <paramref name="assemblyQualifiedName"/> with assembly metadata like the version and public key token removed.</returns>
46+
/// <remarks>
47+
/// Adapted from https://github.com/JamesNK/Newtonsoft.Json/blob/36b605f683c48a976b00ee6a351b97dd2265c7c9/Src/Newtonsoft.Json/Utilities/ReflectionUtils.cs#L183.
48+
/// </remarks>
49+
public static string RemoveAssemblyMetadata(string assemblyQualifiedName) {
50+
var builder = new StringBuilder();
51+
var inAssemblyName = false;
52+
var skippingMetadata = false;
53+
var mayBeArray = false;
54+
for (var i = 0; i < assemblyQualifiedName.Length; i++) {
55+
var current = assemblyQualifiedName[i];
56+
switch (current) {
57+
case '[':
58+
inAssemblyName = false;
59+
skippingMetadata = false;
60+
mayBeArray = true;
61+
builder.Append(current);
62+
break;
63+
case ']':
64+
inAssemblyName = false;
65+
skippingMetadata = false;
66+
mayBeArray = false;
67+
builder.Append(current);
68+
break;
69+
case ',':
70+
if (mayBeArray) {
71+
builder.Append(current);
72+
} else if (!inAssemblyName) {
73+
inAssemblyName = true;
74+
builder.Append(current);
75+
} else {
76+
skippingMetadata = true;
77+
}
78+
break;
79+
default:
80+
mayBeArray = false;
81+
if (!skippingMetadata)
82+
builder.Append(current);
83+
break;
84+
}
85+
}
86+
87+
return builder.ToString();
88+
}
89+
90+
/// <summary>
91+
/// Joins the given <paramref name="type"/> name and <paramref name="assembly"/> into their assembly-qualified name, which is the type name, a comma and a space, and the assembly name.
92+
/// </summary>
93+
/// <param name="type">The type name.</param>
94+
/// <param name="assembly">The assembly name.</param>
95+
/// <returns>The assembly-qualified name.</returns>
96+
public static string JoinAssemblyQualifiedName(string type, string assembly) {
97+
return $"{type}, {assembly}";
98+
}
99+
100+
/// <summary>
101+
/// Splits the given <paramref name="assemblyQualifiedName"/> into a type name and an assembly name (if present).
102+
/// </summary>
103+
/// <param name="assemblyQualifiedName">The assembly-qualified name to split. If this is a type name only, the returned assembly will be <see langword="null"/>.</param>
104+
/// <returns>A tuple containing the type name and the assembly name (if it was present in the original assembly-qualified name, otherwise <see langword="null"/>).</returns>
105+
public static (string Type, string Assembly) SplitAssemblyQualifiedName(string assemblyQualifiedName) {
106+
var commaIdx = -1;
107+
var genericDepth = 0;
108+
for (var i = 0; i < assemblyQualifiedName.Length; i++) {
109+
var c = assemblyQualifiedName[i];
110+
if (c == '[') {
111+
genericDepth++;
112+
} else if (c == ']') {
113+
genericDepth--;
114+
} else if (c == ',' && genericDepth == 0) {
115+
commaIdx = i;
116+
break;
117+
}
118+
}
119+
if (commaIdx < 0) {
120+
return (assemblyQualifiedName, null);
121+
} else {
122+
return (assemblyQualifiedName.Substring(0, commaIdx).Trim(), assemblyQualifiedName.Substring(commaIdx + 1).Trim());
123+
}
124+
}
125+
126+
}
127+
}

Tests/TypeTests.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using Microsoft.Xna.Framework;
5+
using NUnit.Framework;
6+
using TypeHelper = MLEM.Data.TypeHelper;
7+
8+
namespace Tests;
9+
10+
public class TypeTests {
11+
12+
[Test]
13+
public void TestConversions() {
14+
Type[] types = [
15+
typeof(string),
16+
typeof(int),
17+
typeof(Vector2),
18+
typeof(BitConverter),
19+
typeof(List<string>),
20+
typeof(Dictionary<int, string>),
21+
typeof(Dictionary<KeyValuePair<int, string>, string>)
22+
];
23+
foreach (var type in types) {
24+
var split = TypeHelper.SplitAssemblyQualifiedName(type.AssemblyQualifiedName);
25+
Assert.AreEqual(type.FullName, split.Type);
26+
Assert.AreEqual(type.Assembly.GetName().FullName, split.Assembly);
27+
var joined = TypeHelper.JoinAssemblyQualifiedName(split.Type, split.Assembly);
28+
Assert.AreEqual(type.AssemblyQualifiedName, joined);
29+
}
30+
}
31+
32+
[Test]
33+
public void TestGenerics() {
34+
const string spc = "System.Private.CoreLib";
35+
(Type, string[])[] types = [
36+
(typeof(string), []),
37+
(typeof(int), []),
38+
(typeof(Vector2), []),
39+
(typeof(List<string>), [$"System.String, {spc}"]),
40+
(typeof(Dictionary<int, string>), [$"System.Int32, {spc}", $"System.String, {spc}"]),
41+
(typeof(Dictionary<KeyValuePair<int, string>, string>), [$"System.Collections.Generic.KeyValuePair`2[[System.Int32, {spc}],[System.String, {spc}]], {spc}", $"System.String, {spc}"]),
42+
];
43+
foreach (var (type, expected) in types) {
44+
var args = TypeHelper.GetGenericTypeArguments(type.AssemblyQualifiedName).Select(TypeHelper.RemoveAssemblyMetadata).ToArray();
45+
Assert.AreEqual(expected, args);
46+
}
47+
}
48+
49+
[Test]
50+
public void TestRemoveAssemblyDetails() {
51+
const string spc = "System.Private.CoreLib";
52+
(Type, string)[] types = [
53+
(typeof(string), $"System.String, {spc}"),
54+
(typeof(int), $"System.Int32, {spc}"),
55+
(typeof(Vector2), "Microsoft.Xna.Framework.Vector2, MonoGame.Framework"),
56+
(typeof(Dictionary<int, string>), $"System.Collections.Generic.Dictionary`2[[System.Int32, {spc}],[System.String, {spc}]], {spc}"),
57+
];
58+
foreach (var (type, expected) in types) {
59+
Assert.IsTrue(type.AssemblyQualifiedName.Contains("PublicKeyToken"));
60+
Assert.AreEqual(expected, TypeHelper.RemoveAssemblyMetadata(type.AssemblyQualifiedName));
61+
}
62+
}
63+
64+
}

0 commit comments

Comments
 (0)