-
Notifications
You must be signed in to change notification settings - Fork 0
/
TypeResolver.cs
242 lines (215 loc) · 8.13 KB
/
TypeResolver.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
namespace TypeResolver
{
/// <summary>
/// A resolver for types using a hybrid of the C# style and CLR resolution style.
/// </summary>
/// <remarks>
/// By default this uses C# style, but any type wrapped in brackets [] will use CLR resolution style.
/// CLR resolution style may require the name to be fully qualified,
/// but it also may access any type in the system (including outside of well known paths).
/// The entire input into the resolver may be wrapped in a [] to use CLR resolution over the whole string.
/// </remarks>
public class TypeResolver
{
/// <summary>
/// The regex for the TypeResolver generic format.
/// </summary>
private static readonly Regex TypeFormatRegex = new Regex("^(?<Name>[^<>?]+)(<(?<generics>.+)>)?$");
/// <summary>
/// The well known types to use short cuts to define.
/// These are types that do not exist in the type system,
/// but where the names are well known such as int or float.
/// </summary>
public Dictionary<string, Type> WellKnownTypes { get; } = new Dictionary<string, Type>();
/// <summary>
/// Well known assemblies, and well known namespaces inside those assemblies.
/// </summary>
public List<WellKnownAssembly> WellKnownAssemblies { get; } = new List<WellKnownAssembly>();
/// <summary>
/// An assembly to consider well known
/// </summary>
public class WellKnownAssembly
{
/// <summary>
/// The assembly to consider well known.
/// </summary>
public Assembly Assembly { get; }
/// <summary>
/// The prefix namespaces types should be auto exported from.
/// </summary>
public string[] UsingNamespacePrefix { get; }
/// <summary>
/// Creates a new assembly record over the target assembly, with the predefined using statements applied.
/// </summary>
/// <param name="assembly">The assembly to use as well known.</param>
/// <param name="usingNamespacePrefix">
/// The namespaces to be considered having using declared.
/// These should be suffixed with a . for namespace,
/// or + for a class where children types are being exported.
/// </param>
public WellKnownAssembly(Assembly assembly, params string[] usingNamespacePrefix)
{
if (usingNamespacePrefix.Any(s => !(s.EndsWith('.') || s.EndsWith('+'))))
{
throw new ArgumentException("UsingNamespacePrefix entries must end with a . or +, as they are raw prefixes.", nameof(usingNamespacePrefix));
}
Assembly = assembly;
UsingNamespacePrefix = usingNamespacePrefix;
}
}
/// <summary>
/// Attempts to parse the type name using TypeResolver (mostly like C#) format.
/// </summary>
/// <param name="typeName">The name of the type to resolve.</param>
/// <returns>The resolved type, or null if not found.</returns>
public Type? ParseType(string typeName)
{
try
{
return ParseInternal(typeName);
}
catch
{
// TODO: Log the error.
return null;
}
}
/// <summary>
/// Attempts to parse the type name using TypeResolver (mostly like C#) format.
/// </summary>
/// <param name="typeName">The name of the type to resolve.</param>
/// <returns>The resolved type, or null if not found.</returns>
private Type? ParseInternal(string typeName)
{
typeName = typeName.Trim();
// Type name is to be sent to the CLR type system.
if (typeName.StartsWith('[') && typeName.EndsWith(']'))
{
var trimmedName = typeName.Substring(1, typeName.Length - 2);
return Type.GetType(trimmedName);
}
// String represents a struct to be converted to a nullable type.
if (typeName.EndsWith('?'))
{
var extractedType = ParseInternal(typeName.Remove(typeName.Length - 1));
return extractedType != null && !extractedType.IsByRef
? typeof(Nullable<>).MakeGenericType(extractedType)
: extractedType;
}
// Resolvers below this point fall through to the next if they fail, above this point stops resolution.
return TryResolveWellKnown(typeName) ??
TryResolve(typeName) ??
ResolveGeneric(typeName);
}
/// <summary>
/// Resolves a pre-defined well known type such as 'string' or 'int', mapping the string to the concrete type.
/// </summary>
/// <param name="shorthandTypeName">The short name of the type.</param>
/// <returns>The hard type for the short name, if found.</returns>
private Type? TryResolveWellKnown(string shorthandTypeName)
{
return WellKnownTypes.TryGetValue(shorthandTypeName, out var type) ? type : null;
}
/// <summary>
/// Returns the filled out generic of the specific name and generic type count, or null.
/// </summary>
/// <param name="typeName">The TypeResolver formatted type string to resolve.</param>
/// <returns>The filled out generic, or null if not found.</returns>
private Type? ResolveGeneric(string typeName)
{
var match = TypeFormatRegex.Match(typeName);
if (!match.Success)
{
return null;
}
var name = match.Groups["Name"].Value.Trim();
var genStr = match.Groups["generics"].Value;
List<Type> generics = new List<Type>();
while (!string.IsNullOrWhiteSpace(genStr))
{
var current = ConsumeGenericDeclaration(ref genStr);
var subType = ParseInternal(current);
if (subType == null)
{
return null;
}
generics.Add(subType);
}
var candidate = TryResolve($"{name}`{generics.Count}");
return candidate?.MakeGenericType(generics.ToArray());
}
/// <summary>
/// A form of iterator that splits on commas, but only outside of scopes defined by [] and gators.
/// </summary>
/// <remarks>
/// It is possible to cause very funky scopes by doing things like >]int<[,
/// but those resolutions will fail from tokens not matching up later on so that is OK.
/// The worst case is your type request succeeds even though your format was garbage,
/// so you retrieved a type you already had access to retrieve if you parsed the type correctly.
/// </remarks>
/// <param name="genericStr">the generic string to read through for substrings, this is also used to return the remaining content to read.</param>
/// <returns>A tuple of the current string, and the next string to iterate.</returns>
private static string ConsumeGenericDeclaration(ref string genericStr)
{
var recurLevel = 0;
for (var i = 0; i < genericStr.Length; i++)
{
var tok = genericStr[i];
switch (tok)
{
case ',':
if (recurLevel == 0)
{
var name = genericStr.Substring(0, i).Trim();
genericStr = genericStr.Substring(i + 1);
return name;
}
break;
case '>':
case ']':
recurLevel--;
break;
case '<':
case '[':
recurLevel++;
break;
}
}
// wipe the newly consumed string.
var cachedName = genericStr;
genericStr = "";
return cachedName;
}
/// <summary>
/// Attempt to resolve the type name from the well known assembly list and namespaces.
/// </summary>
/// <param name="typeName">The name of the type to find.</param>
/// <returns>The type if it was found, or null.</returns>
private Type? TryResolve(string typeName)
{
Type? target = null;
foreach (var knownAssembly in WellKnownAssemblies)
{
target = knownAssembly.Assembly.GetType(typeName);
if (target != null)
{
return target;
}
foreach (var ns in knownAssembly.UsingNamespacePrefix)
{
target = knownAssembly.Assembly.GetType(ns + typeName);
if (target != null)
{
return target;
}
}
}
return target;
}
}
}