Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions Jint.Tests/Runtime/InteropTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, object> _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);
}
}
42 changes: 39 additions & 3 deletions Jint/Runtime/Interop/ObjectWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
{
Expand Down Expand Up @@ -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
Expand Down