IndexerAccessor.cs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. using System.Collections;
  2. using System.Diagnostics.CodeAnalysis;
  3. using System.Globalization;
  4. using System.Reflection;
  5. using Jint.Native;
  6. using Jint.Runtime.Descriptors;
  7. using Jint.Runtime.Descriptors.Specialized;
  8. #pragma warning disable IL2067
  9. namespace Jint.Runtime.Interop.Reflection;
  10. internal sealed class IndexerAccessor : ReflectionAccessor
  11. {
  12. private readonly object _key;
  13. private readonly MethodInfo? _getter;
  14. private readonly MethodInfo? _setter;
  15. private readonly MethodInfo? _containsKey;
  16. private IndexerAccessor(PropertyInfo indexer, MethodInfo? containsKey, object key) : base(indexer.PropertyType)
  17. {
  18. Indexer = indexer;
  19. FirstIndexParameter = indexer.GetIndexParameters()[0];
  20. _containsKey = containsKey;
  21. _key = key;
  22. _getter = indexer.GetGetMethod();
  23. _setter = indexer.GetSetMethod();
  24. }
  25. internal PropertyInfo Indexer { get; }
  26. internal ParameterInfo FirstIndexParameter { get; }
  27. internal static bool TryFindIndexer(
  28. Engine engine,
  29. [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicProperties)] Type targetType,
  30. string propertyName,
  31. [NotNullWhen(true)] out IndexerAccessor? indexerAccessor,
  32. [NotNullWhen(true)] out PropertyInfo? indexer)
  33. {
  34. indexerAccessor = null;
  35. indexer = null;
  36. var paramTypeArray = new Type[1];
  37. // integer keys can be ambiguous as we only know string keys
  38. int? integerKey = null;
  39. if (int.TryParse(propertyName, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intKeyTemp))
  40. {
  41. integerKey = intKeyTemp;
  42. }
  43. var filter = new Func<MemberInfo, bool>(m => engine.Options.Interop.TypeResolver.Filter(engine, targetType, m));
  44. // default indexer wins
  45. var descriptor = TypeDescriptor.Get(targetType);
  46. if (descriptor.IntegerIndexerProperty is not null && !filter(descriptor.IntegerIndexerProperty))
  47. {
  48. indexerAccessor = ComposeIndexerFactory(engine, targetType, descriptor.IntegerIndexerProperty, paramType: typeof(int), propertyName, integerKey, paramTypeArray);
  49. if (indexerAccessor != null)
  50. {
  51. indexer = descriptor.IntegerIndexerProperty;
  52. return true;
  53. }
  54. }
  55. // try to find first indexer having either public getter or setter with matching argument type
  56. PropertyInfo? fallbackIndexer = null;
  57. foreach (var candidate in targetType.GetProperties())
  58. {
  59. if (!filter(candidate))
  60. {
  61. continue;
  62. }
  63. var indexParameters = candidate.GetIndexParameters();
  64. if (indexParameters.Length != 1)
  65. {
  66. continue;
  67. }
  68. if (candidate.GetGetMethod() != null || candidate.GetSetMethod() != null)
  69. {
  70. var paramType = indexParameters[0].ParameterType;
  71. indexerAccessor = ComposeIndexerFactory(engine, targetType, candidate, paramType, propertyName, integerKey, paramTypeArray);
  72. if (indexerAccessor != null)
  73. {
  74. if (paramType != typeof(string) || integerKey is null)
  75. {
  76. // exact match, we don't need to check for integer key
  77. indexer = candidate;
  78. return true;
  79. }
  80. if (fallbackIndexer is null)
  81. {
  82. // our fallback
  83. fallbackIndexer = candidate;
  84. }
  85. }
  86. }
  87. }
  88. if (fallbackIndexer is not null)
  89. {
  90. indexer = fallbackIndexer;
  91. // just to keep compiler happy, we know we have a value
  92. indexerAccessor ??= new IndexerAccessor(indexer, containsKey: null, key: null!);
  93. return true;
  94. }
  95. indexerAccessor = default;
  96. indexer = default;
  97. return false;
  98. }
  99. private static IndexerAccessor? ComposeIndexerFactory(
  100. Engine engine,
  101. [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicProperties)] Type targetType,
  102. PropertyInfo candidate,
  103. Type paramType,
  104. string propertyName,
  105. int? integerKey,
  106. Type[] paramTypeArray)
  107. {
  108. // check for known incompatible types
  109. #if NET8_0_OR_GREATER
  110. if (typeof(System.Text.Json.Nodes.JsonNode).IsAssignableFrom(targetType)
  111. && (targetType != typeof(System.Text.Json.Nodes.JsonArray) || paramType != typeof(int))
  112. && (targetType != typeof(System.Text.Json.Nodes.JsonObject) || paramType != typeof(string)))
  113. {
  114. // we cannot access this[string] with anything else than JsonObject, otherwise itw will throw
  115. // we cannot access this[int] with anything else than JsonArray, otherwise itw will throw
  116. return null;
  117. }
  118. #endif
  119. object? key = null;
  120. // int key is quite common case
  121. if (paramType == typeof(int))
  122. {
  123. if (integerKey is not null)
  124. {
  125. key = integerKey;
  126. }
  127. }
  128. else
  129. {
  130. engine.TypeConverter.TryConvert(propertyName, paramType, CultureInfo.InvariantCulture, out key);
  131. }
  132. if (key is not null)
  133. {
  134. // the key can be converted for this indexer
  135. var indexerProperty = candidate;
  136. // get contains key method to avoid index exception being thrown in dictionaries
  137. paramTypeArray[0] = paramType;
  138. var containsKeyMethod = targetType.GetMethod(nameof(IDictionary<string, string>.ContainsKey), paramTypeArray);
  139. if (containsKeyMethod is null && targetType.IsAssignableFrom(typeof(IDictionary)))
  140. {
  141. paramTypeArray[0] = typeof(object);
  142. containsKeyMethod = targetType.GetMethod(nameof(IDictionary.Contains), paramTypeArray);
  143. }
  144. return new IndexerAccessor(indexerProperty, containsKeyMethod, key);
  145. }
  146. // the key type doesn't work for this indexer
  147. return null;
  148. }
  149. public override bool Readable => Indexer.CanRead;
  150. public override bool Writable => Indexer.CanWrite;
  151. protected override object? DoGetValue(object target, string memberName)
  152. {
  153. if (_getter is null)
  154. {
  155. Throw.InvalidOperationException("Indexer has no public getter.");
  156. return null;
  157. }
  158. object[] parameters = [_key];
  159. if (_containsKey != null)
  160. {
  161. if (_containsKey.Invoke(target, parameters) as bool? != true)
  162. {
  163. return JsValue.Undefined;
  164. }
  165. }
  166. try
  167. {
  168. return _getter.Invoke(target, parameters);
  169. }
  170. catch (TargetInvocationException tie) when (tie.InnerException is KeyNotFoundException)
  171. {
  172. return JsValue.Undefined;
  173. }
  174. }
  175. protected override void DoSetValue(object target, string memberName, object? value)
  176. {
  177. if (_setter is null)
  178. {
  179. Throw.InvalidOperationException("Indexer has no public setter.");
  180. }
  181. object?[] parameters = [_key, value];
  182. _setter.Invoke(target, parameters);
  183. }
  184. public override PropertyDescriptor CreatePropertyDescriptor(Engine engine, object target, string memberName, bool enumerable = true)
  185. {
  186. if (_containsKey != null)
  187. {
  188. if (_containsKey.Invoke(target, [_key]) as bool? != true)
  189. {
  190. return PropertyDescriptor.Undefined;
  191. }
  192. }
  193. return new ReflectionDescriptor(engine, this, target, memberName, enumerable: true);
  194. }
  195. }