diff --git a/Jint.Tests/Runtime/InteropTests.cs b/Jint.Tests/Runtime/InteropTests.cs index f1fe981f70..915928ddcf 100644 --- a/Jint.Tests/Runtime/InteropTests.cs +++ b/Jint.Tests/Runtime/InteropTests.cs @@ -3889,4 +3889,70 @@ public void ShouldBeAbleToWriteLengthOfListLike() list.Should().HaveCount(1); list[0].Should().Be(null); } + + // GitHub issue #2173 - Type resolution should use runtime type when declared type has indexer + private class WrapperWithIndexer + { + private readonly Dictionary _properties = new(); + + public object this[string key] + { + get => _properties.TryGetValue(key, out var value) ? value : null!; + set => _properties[key] = value; + } + } + + private class GeometryWrapperWithProperty : WrapperWithIndexer + { + public double X { get; set; } + public double Y { get; set; } + } + + private class FeatureWithBaseTypeProperty + { + public WrapperWithIndexer Geometry { get; set; } = new GeometryWrapperWithProperty { X = 10.5, Y = 20.5 }; + } + + [Fact] + public void ShouldAccessDerivedTypePropertyWhenDeclaredTypeHasIndexer() + { + // GitHub issue #2173: When a property is declared with a base type that has an indexer, + // but the actual runtime value is a derived type with a property, the property should be accessible + var engine = new Engine(); + var feature = new FeatureWithBaseTypeProperty(); + engine.SetValue("feature", feature); + + // Should access the X property from GeometryWrapperWithProperty, not the indexer from WrapperWithIndexer + var result = engine.Evaluate("feature.Geometry.x").AsNumber(); + Assert.Equal(10.5, result); + + var resultY = engine.Evaluate("feature.Geometry.y").AsNumber(); + Assert.Equal(20.5, resultY); + } + + [Fact] + public void ShouldStillAccessIndexerWhenPropertyDoesNotExist() + { + // Ensure the indexer still works when the property doesn't exist on the derived type + var engine = new Engine(); + var feature = new FeatureWithBaseTypeProperty(); + ((GeometryWrapperWithProperty) feature.Geometry)["customKey"] = "customValue"; + engine.SetValue("feature", feature); + + var result = engine.Evaluate("feature.Geometry.customKey"); + Assert.Equal("customValue", result.AsString()); + } + + [Fact] + public void ShouldSetDerivedTypePropertyWhenDeclaredTypeHasIndexer() + { + var engine = new Engine(cfg => cfg.AllowClrWrite()); + var feature = new FeatureWithBaseTypeProperty(); + engine.SetValue("feature", feature); + + engine.Evaluate("feature.Geometry.x = 99.9"); + + var geometry = (GeometryWrapperWithProperty) feature.Geometry; + Assert.Equal(99.9, geometry.X); + } } diff --git a/Jint/Runtime/Interop/ObjectWrapper.cs b/Jint/Runtime/Interop/ObjectWrapper.cs index 5524bee997..a4fbfedb2d 100644 --- a/Jint/Runtime/Interop/ObjectWrapper.cs +++ b/Jint/Runtime/Interop/ObjectWrapper.cs @@ -174,6 +174,25 @@ public override bool Set(JsValue property, JsValue value, JsValue receiver) { // can try utilize fast path var accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, ClrType, member, mustBeReadable: false, mustBeWritable: true); + var actualType = Target.GetType(); + if (ClrType != actualType) + { + // When the declared type differs from the actual runtime type: + // If only an indexer was found, check if the runtime type has a direct property/field/method + // that should take precedence over the indexer + if (accessor is IndexerAccessor) + { + var runtimeAccessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, actualType, member, mustBeReadable: false, mustBeWritable: true); + if (runtimeAccessor is not IndexerAccessor && runtimeAccessor != ConstantValueAccessor.NullAccessor) + { + accessor = runtimeAccessor; + } + } + else if (ReferenceEquals(accessor, ConstantValueAccessor.NullAccessor)) + { + accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, actualType, member, mustBeReadable: false, mustBeWritable: true); + } + } if (ReferenceEquals(accessor, ConstantValueAccessor.NullAccessor)) { @@ -410,9 +429,26 @@ private PropertyDescriptor GetOwnProperty(JsValue property, bool mustBeReadable, } var accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, ClrType, member, mustBeReadable, mustBeWritable); - if (accessor == ConstantValueAccessor.NullAccessor && ClrType != Target.GetType()) - { - accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, Target.GetType(), member, mustBeReadable, mustBeWritable); + var actualType = Target.GetType(); + if (ClrType != actualType) + { + // When the declared type differs from the actual runtime type: + // - If no accessor was found, fall back to the runtime type (original behavior) + // - If only an indexer was found, check if the runtime type has a direct property/field/method + // that should take precedence over the indexer + if (accessor == ConstantValueAccessor.NullAccessor) + { + accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, actualType, member, mustBeReadable, mustBeWritable); + } + else if (accessor is IndexerAccessor) + { + var runtimeAccessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, actualType, member, mustBeReadable, mustBeWritable); + if (runtimeAccessor is not IndexerAccessor && runtimeAccessor != ConstantValueAccessor.NullAccessor) + { + // Prefer direct property/field/method from runtime type over indexer from declared type + accessor = runtimeAccessor; + } + } } var descriptor = accessor.CreatePropertyDescriptor(_engine, Target, member, enumerable: !isDictionary); if (!isDictionary