TypeResolver.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Dynamic;
  3. using System.Reflection;
  4. using System.Threading;
  5. using Jint.Runtime.Interop.Reflection;
  6. namespace Jint.Runtime.Interop
  7. {
  8. /// <summary>
  9. /// Interop strategy for resolving types and members.
  10. /// </summary>
  11. public sealed class TypeResolver
  12. {
  13. public static readonly TypeResolver Default = new();
  14. /// <summary>
  15. /// Registers a filter that determines whether given member is wrapped to interop or returned as undefined.
  16. /// By default allows all but will also be limited by <see cref="InteropOptions.AllowGetType"/> configuration.
  17. /// </summary>
  18. /// <seealso cref="InteropOptions.AllowGetType"/>
  19. public Predicate<MemberInfo> MemberFilter { get; set; } = _ => true;
  20. internal bool Filter(Engine engine, MemberInfo m)
  21. {
  22. return (engine.Options.Interop.AllowGetType || m.Name != nameof(GetType)) && MemberFilter(m);
  23. }
  24. /// <summary>
  25. /// Gives the exposed names for a member. Allows to expose C# convention following member like IsSelected
  26. /// as more JS idiomatic "selected" for example. Defaults to returning the <see cref="MemberInfo.Name"/> as-is.
  27. /// </summary>
  28. public Func<MemberInfo, IEnumerable<string>> MemberNameCreator { get; set; } = NameCreator;
  29. private static IEnumerable<string> NameCreator(MemberInfo info)
  30. {
  31. yield return info.Name;
  32. }
  33. /// <summary>
  34. /// Sets member name comparison strategy when finding CLR objects members.
  35. /// By default member's first character casing is ignored and rest of the name is compared with strict equality.
  36. /// </summary>
  37. public StringComparer MemberNameComparer { get; set; } = DefaultMemberNameComparer.Instance;
  38. internal ReflectionAccessor GetAccessor(
  39. Engine engine,
  40. Type type,
  41. string member,
  42. Func<ReflectionAccessor?>? accessorFactory = null,
  43. bool forWrite = false)
  44. {
  45. var key = new Engine.ClrPropertyDescriptorFactoriesKey(type, member);
  46. var factories = engine._reflectionAccessors;
  47. if (factories.TryGetValue(key, out var accessor))
  48. {
  49. return accessor;
  50. }
  51. accessor = accessorFactory?.Invoke() ?? ResolvePropertyDescriptorFactory(engine, type, member, forWrite);
  52. // racy, we don't care, worst case we'll catch up later
  53. Interlocked.CompareExchange(ref engine._reflectionAccessors,
  54. new Dictionary<Engine.ClrPropertyDescriptorFactoriesKey, ReflectionAccessor>(factories)
  55. {
  56. [key] = accessor
  57. }, factories);
  58. return accessor;
  59. }
  60. private ReflectionAccessor ResolvePropertyDescriptorFactory(
  61. Engine engine,
  62. Type type,
  63. string memberName,
  64. bool forWrite)
  65. {
  66. var isNumber = uint.TryParse(memberName, out _);
  67. // we can always check indexer if there's one, and then fall back to properties if indexer returns null
  68. IndexerAccessor.TryFindIndexer(engine, type, memberName, out var indexerAccessor, out var indexer);
  69. const BindingFlags BindingFlags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public;
  70. // properties and fields cannot be numbers
  71. if (!isNumber
  72. && TryFindMemberAccessor(engine, type, memberName, BindingFlags, indexer, out var temp)
  73. && (!forWrite || temp.Writable))
  74. {
  75. return temp;
  76. }
  77. if (typeof(DynamicObject).IsAssignableFrom(type))
  78. {
  79. return new DynamicObjectAccessor(type, memberName);
  80. }
  81. // if no methods are found check if target implemented indexing
  82. if (indexerAccessor != null)
  83. {
  84. return indexerAccessor;
  85. }
  86. // try to find a single explicit property implementation
  87. List<PropertyInfo>? list = null;
  88. var typeResolverMemberNameComparer = MemberNameComparer;
  89. var typeResolverMemberNameCreator = MemberNameCreator;
  90. foreach (var iface in type.GetInterfaces())
  91. {
  92. foreach (var iprop in iface.GetProperties())
  93. {
  94. if (!Filter(engine, iprop))
  95. {
  96. continue;
  97. }
  98. if (iprop.Name == "Item" && iprop.GetIndexParameters().Length == 1)
  99. {
  100. // never take indexers, should use the actual indexer
  101. continue;
  102. }
  103. foreach (var name in typeResolverMemberNameCreator(iprop))
  104. {
  105. if (typeResolverMemberNameComparer.Equals(name, memberName))
  106. {
  107. list ??= new List<PropertyInfo>();
  108. list.Add(iprop);
  109. }
  110. }
  111. }
  112. }
  113. if (list?.Count == 1)
  114. {
  115. return new PropertyAccessor(memberName, list[0]);
  116. }
  117. // try to find explicit method implementations
  118. List<MethodInfo>? explicitMethods = null;
  119. foreach (var iface in type.GetInterfaces())
  120. {
  121. foreach (var imethod in iface.GetMethods())
  122. {
  123. if (!Filter(engine, imethod))
  124. {
  125. continue;
  126. }
  127. foreach (var name in typeResolverMemberNameCreator(imethod))
  128. {
  129. if (typeResolverMemberNameComparer.Equals(name, memberName))
  130. {
  131. explicitMethods ??= new List<MethodInfo>();
  132. explicitMethods.Add(imethod);
  133. }
  134. }
  135. }
  136. }
  137. if (explicitMethods?.Count > 0)
  138. {
  139. return new MethodAccessor(type, memberName, MethodDescriptor.Build(explicitMethods));
  140. }
  141. // try to find explicit indexer implementations
  142. foreach (var interfaceType in type.GetInterfaces())
  143. {
  144. if (IndexerAccessor.TryFindIndexer(engine, interfaceType, memberName, out var accessor, out _))
  145. {
  146. return accessor;
  147. }
  148. }
  149. if (engine._extensionMethods.TryGetExtensionMethods(type, out var extensionMethods))
  150. {
  151. var matches = new List<MethodInfo>();
  152. foreach (var method in extensionMethods)
  153. {
  154. if (!Filter(engine, method))
  155. {
  156. continue;
  157. }
  158. foreach (var name in typeResolverMemberNameCreator(method))
  159. {
  160. if (typeResolverMemberNameComparer.Equals(name, memberName))
  161. {
  162. matches.Add(method);
  163. }
  164. }
  165. }
  166. if (matches.Count > 0)
  167. {
  168. return new MethodAccessor(type, memberName, MethodDescriptor.Build(matches));
  169. }
  170. }
  171. return ConstantValueAccessor.NullAccessor;
  172. }
  173. internal bool TryFindMemberAccessor(
  174. Engine engine,
  175. Type type,
  176. string memberName,
  177. BindingFlags bindingFlags,
  178. PropertyInfo? indexerToTry,
  179. [NotNullWhen(true)] out ReflectionAccessor? accessor)
  180. {
  181. // look for a property, bit be wary of indexers, we don't want indexers which have name "Item" to take precedence
  182. PropertyInfo? property = null;
  183. var memberNameComparer = MemberNameComparer;
  184. var typeResolverMemberNameCreator = MemberNameCreator;
  185. foreach (var p in type.GetProperties(bindingFlags))
  186. {
  187. if (!Filter(engine, p))
  188. {
  189. continue;
  190. }
  191. // only if it's not an indexer, we can do case-ignoring matches
  192. var isStandardIndexer = p.GetIndexParameters().Length == 1 && p.Name == "Item";
  193. if (!isStandardIndexer)
  194. {
  195. foreach (var name in typeResolverMemberNameCreator(p))
  196. {
  197. if (memberNameComparer.Equals(name, memberName))
  198. {
  199. property = p;
  200. break;
  201. }
  202. }
  203. }
  204. }
  205. if (property != null)
  206. {
  207. accessor = new PropertyAccessor(memberName, property, indexerToTry);
  208. return true;
  209. }
  210. // look for a field
  211. FieldInfo? field = null;
  212. foreach (var f in type.GetFields(bindingFlags))
  213. {
  214. if (!Filter(engine, f))
  215. {
  216. continue;
  217. }
  218. foreach (var name in typeResolverMemberNameCreator(f))
  219. {
  220. if (memberNameComparer.Equals(name, memberName))
  221. {
  222. field = f;
  223. break;
  224. }
  225. }
  226. }
  227. if (field != null)
  228. {
  229. accessor = new FieldAccessor(field, memberName, indexerToTry);
  230. return true;
  231. }
  232. // if no properties were found then look for a method
  233. List<MethodInfo>? methods = null;
  234. void AddMethod(MethodInfo m)
  235. {
  236. if (!Filter(engine, m))
  237. {
  238. return;
  239. }
  240. foreach (var name in typeResolverMemberNameCreator(m))
  241. {
  242. if (memberNameComparer.Equals(name, memberName))
  243. {
  244. methods ??= new List<MethodInfo>();
  245. methods.Add(m);
  246. }
  247. }
  248. }
  249. foreach (var m in type.GetMethods(bindingFlags))
  250. {
  251. AddMethod(m);
  252. }
  253. foreach (var iface in type.GetInterfaces())
  254. {
  255. foreach (var m in iface.GetMethods())
  256. {
  257. AddMethod(m);
  258. }
  259. }
  260. // TPC: need to grab the extension methods here - for overloads
  261. if (engine._extensionMethods.TryGetExtensionMethods(type, out var extensionMethods))
  262. {
  263. foreach (var methodInfo in extensionMethods)
  264. {
  265. AddMethod(methodInfo);
  266. }
  267. }
  268. // Add Object methods to interface
  269. if (type.IsInterface)
  270. {
  271. foreach (var m in typeof(object).GetMethods(bindingFlags))
  272. {
  273. AddMethod(m);
  274. }
  275. }
  276. if (methods?.Count > 0)
  277. {
  278. accessor = new MethodAccessor(type, memberName, MethodDescriptor.Build(methods));
  279. return true;
  280. }
  281. // look for nested type
  282. var nestedType = type.GetNestedType(memberName, bindingFlags);
  283. if (nestedType != null)
  284. {
  285. var typeReference = TypeReference.CreateTypeReference(engine, nestedType);
  286. accessor = new NestedTypeAccessor(typeReference, memberName);
  287. return true;
  288. }
  289. accessor = default;
  290. return false;
  291. }
  292. private sealed class DefaultMemberNameComparer : StringComparer
  293. {
  294. public static readonly StringComparer Instance = new DefaultMemberNameComparer();
  295. public override int Compare(string? x, string? y)
  296. {
  297. throw new NotImplementedException();
  298. }
  299. public override bool Equals(string? x, string? y)
  300. {
  301. if (ReferenceEquals(x, y))
  302. {
  303. return true;
  304. }
  305. if (x == null || y == null)
  306. {
  307. return false;
  308. }
  309. if (x.Length != y.Length)
  310. {
  311. return false;
  312. }
  313. var equals = false;
  314. if (x.Length > 0)
  315. {
  316. equals = char.ToLowerInvariant(x[0]) == char.ToLowerInvariant(y[0]);
  317. }
  318. if (equals && x.Length > 1)
  319. {
  320. #if SUPPORTS_SPAN_PARSE
  321. equals = x.AsSpan(1).SequenceEqual(y.AsSpan(1));
  322. #else
  323. equals = x.Substring(1) == y.Substring(1);
  324. #endif
  325. }
  326. return equals;
  327. }
  328. public override int GetHashCode(string obj)
  329. {
  330. throw new NotImplementedException();
  331. }
  332. }
  333. }
  334. }