Browse Source

Generic string-keyed dictionary indexing under interop (#1002)

Marko Lahma 3 years ago
parent
commit
dac5a0bea0

+ 23 - 0
Jint.Tests/Runtime/InteropTests.cs

@@ -2898,5 +2898,28 @@ namespace Jint.Tests.Runtime
             var ex = Assert.Throws<JavaScriptException>(() => engine.Execute("a.age = \"It won't work, but it's normal\""));
             Assert.Equal("Input string was not in a correct format.", ex.Message);
         }
+
+        [Fact]
+        public void ShouldBeAbleToIndexJObjectWithStrings()
+        {
+            var engine = new Engine();
+
+            const string json = @"
+            {
+                'Properties': {
+                    'expirationDate': {
+                        'Value': '2021-10-09T00:00:00Z'
+                    }
+                }
+            }";
+
+            var obj = JObject.Parse(json);
+            engine.SetValue("o", obj);
+            var value = engine.Evaluate("o.Properties.expirationDate.Value");
+            var wrapper = Assert.IsAssignableFrom<ObjectWrapper>(value);
+            var token = wrapper.Target as JToken;
+            var localDateTimeString = DateTime.Parse("2021-10-09T00:00:00Z").ToUniversalTime();
+            Assert.Equal(localDateTimeString.ToString(), token.ToString());
+        }
     }
 }

+ 2 - 2
Jint/Runtime/Interop/ObjectWrapper.cs

@@ -103,9 +103,9 @@ namespace Jint.Runtime.Interop
                 var member = stringKey.ToString();
 
                 // expando object for instance
-                if (Target is IDictionary<string, object> stringKeyedDictionary)
+                if (_typeDescriptor.IsStringKeyedGenericDictionary)
                 {
-                    if (stringKeyedDictionary.TryGetValue(member, out var value))
+                    if (_typeDescriptor.TryGetValue(Target, member, out var value))
                     {
                         return FromObject(_engine, value);
                     }

+ 42 - 8
Jint/Runtime/Interop/TypeDescriptor.cs

@@ -9,11 +9,30 @@ namespace Jint.Runtime.Interop
     internal sealed class TypeDescriptor
     {
         private static readonly ConcurrentDictionary<Type, TypeDescriptor> _cache = new();
+        private static readonly Type _genericDictionaryType = typeof(IDictionary<,>);
+        private static readonly Type _stringType = typeof(string);
+
+        private readonly PropertyInfo _stringIndexer;
 
         private TypeDescriptor(Type type)
         {
-            IsArrayLike = DetermineIfObjectIsArrayLikeClrCollection(type);
-            IsDictionary = typeof(IDictionary).IsAssignableFrom(type) || typeof(IDictionary<string, object>).IsAssignableFrom(type);
+            // check if object has any generic dictionary signature that accepts string as key
+            foreach (var i in type.GetInterfaces())
+            {
+                if (i.IsGenericType
+                    && i.GetGenericTypeDefinition() == _genericDictionaryType
+                    && i.GenericTypeArguments[0] == _stringType)
+                {
+                    _stringIndexer = i.GetProperty("Item");
+                    break;
+                }
+            }
+
+            IsDictionary = _stringIndexer is not null || typeof(IDictionary).IsAssignableFrom(type);
+
+            // dictionaries are considered normal-object-like
+            IsArrayLike = !IsDictionary && DetermineIfObjectIsArrayLikeClrCollection(type);
+
             IsEnumerable = typeof(IEnumerable).IsAssignableFrom(type);
 
             if (IsArrayLike)
@@ -26,6 +45,7 @@ namespace Jint.Runtime.Interop
         public bool IsArrayLike { get; }
         public bool IsIntegerIndexedArray { get; }
         public bool IsDictionary { get; }
+        public bool IsStringKeyedGenericDictionary => _stringIndexer is not null;
         public bool IsEnumerable { get; }
         public PropertyInfo LengthProperty { get; }
 
@@ -38,12 +58,6 @@ namespace Jint.Runtime.Interop
 
         private static bool DetermineIfObjectIsArrayLikeClrCollection(Type type)
         {
-            if (typeof(IDictionary).IsAssignableFrom(type) || typeof(IDictionary<string, object>).IsAssignableFrom(type))
-            {
-                // dictionaries are considered normal-object-like
-                return false;
-            }
-
             if (typeof(ICollection).IsAssignableFrom(type))
             {
                 return true;
@@ -65,5 +79,25 @@ namespace Jint.Runtime.Interop
 
             return false;
         }
+
+        public bool TryGetValue(object target, string member, out object o)
+        {
+            if (!IsStringKeyedGenericDictionary)
+            {
+                ExceptionHelper.ThrowInvalidOperationException("Not a string-keyed dictionary");
+            }
+
+            // we could throw when indexing with an invalid key
+            try
+            {
+                o = _stringIndexer.GetValue(target, new [] { member });
+                return true;
+            }
+            catch (TargetInvocationException tie) when (tie.InnerException is KeyNotFoundException)
+            {
+                o = null;
+                return false;
+            }
+        }
     }
 }