diff --git a/Jint.Tests/Runtime/InteropTests.cs b/Jint.Tests/Runtime/InteropTests.cs index 9ccdfbada3..d3b7414d63 100644 --- a/Jint.Tests/Runtime/InteropTests.cs +++ b/Jint.Tests/Runtime/InteropTests.cs @@ -90,6 +90,25 @@ public void EngineShouldStringifyADictionaryOfStringAndObjectCorrectly() Assert.Equal("{\"foo\":5,\"bar\":\"A string\"}", result); } + [Fact] + public void ReadOnlyDictionaryShouldNotBeTreatedAsArrayLike() + { + var engine = new Engine(); + + var dictionary = new ReadOnlyDictionary(new Dictionary + { + { "foo", 5 }, + { "bar", "A string" } + }); + engine.SetValue(nameof(dictionary), dictionary); + + var result = engine.Evaluate($"JSON.stringify({nameof(dictionary)})").AsString(); + Assert.Equal("{\"foo\":5,\"bar\":\"A string\"}", result); + + var keys = engine.Evaluate($"Object.keys({nameof(dictionary)})").AsArray(); + Assert.Equal((uint) 2, keys.Length); + } + [Fact] public void EngineShouldRoundtripParsedJSONBackToStringCorrectly() { @@ -3655,6 +3674,28 @@ public Dictionary Metadata } } + /// + /// A custom IReadOnlyDictionary that does NOT implement IDictionary, to verify it's treated as dictionary-like, not array-like. + /// + private class ReadOnlyDictionary : IReadOnlyDictionary + { + private readonly Dictionary _dictionary; + + public ReadOnlyDictionary(Dictionary dictionary) + { + _dictionary = dictionary; + } + + public IEnumerator> GetEnumerator() => _dictionary.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public int Count => _dictionary.Count; + public bool ContainsKey(string key) => _dictionary.ContainsKey(key); + public bool TryGetValue(string key, out object value) => _dictionary.TryGetValue(key, out value!); + public object this[string key] => _dictionary[key]; + public IEnumerable Keys => _dictionary.Keys; + public IEnumerable Values => _dictionary.Values; + } + [Fact] public void CanSelectShadowedPropertiesBasedOnReadableAndWritable() { diff --git a/Jint/Runtime/Interop/TypeDescriptor.cs b/Jint/Runtime/Interop/TypeDescriptor.cs index 30637fb5c7..7ff762d429 100644 --- a/Jint/Runtime/Interop/TypeDescriptor.cs +++ b/Jint/Runtime/Interop/TypeDescriptor.cs @@ -17,6 +17,7 @@ internal sealed class TypeDescriptor private static readonly PropertyInfo _listIndexer = typeof(IList).GetProperty("Item")!; private static readonly Type _genericDictionaryType = typeof(IDictionary<,>); + private static readonly Type _readOnlyGenericDictionaryType = typeof(IReadOnlyDictionary<,>); private static readonly Type _stringType = typeof(string); private readonly MethodInfo? _tryGetValueMethod; @@ -176,13 +177,18 @@ private static void AnalyzeType( var genericTypeDefinition = type.GetGenericTypeDefinition(); // check if object has any generic dictionary signature that accepts string as key - var b = genericTypeDefinition == _genericDictionaryType; - if (b && type.GenericTypeArguments[0] == _stringType) + var isGenericDictionary = genericTypeDefinition == _genericDictionaryType; + var isReadOnlyGenericDictionary = genericTypeDefinition == _readOnlyGenericDictionaryType; + if ((isGenericDictionary || isReadOnlyGenericDictionary) && type.GenericTypeArguments[0] == _stringType) { - tryGetValueMethod = type.GetMethod("TryGetValue"); - removeMethod = type.GetMethod("Remove"); - keysAccessor = type.GetProperty("Keys"); - valueType = type.GenericTypeArguments[1]; + tryGetValueMethod ??= type.GetMethod("TryGetValue"); + keysAccessor ??= type.GetProperty("Keys"); + valueType ??= type.GenericTypeArguments[1]; + + if (isGenericDictionary) + { + removeMethod ??= type.GetMethod("Remove"); + } } isCollection |= genericTypeDefinition == typeof(IReadOnlyCollection<>) || genericTypeDefinition == typeof(ICollection<>); @@ -190,7 +196,7 @@ private static void AnalyzeType( { integerIndexer ??= type.GetProperty("Item"); } - isDictionary |= genericTypeDefinition == _genericDictionaryType; + isDictionary |= isGenericDictionary || isReadOnlyGenericDictionary; } }