From 66253b7cdd9909c907bdf270d4214ab4a4bc737a Mon Sep 17 00:00:00 2001 From: Khanh Le Date: Sat, 6 Jun 2026 09:05:19 -0500 Subject: [PATCH] Fix runtime type member writes for CLR wrappers --- .../Runtime/InteropExplicitTypeTests.cs | 27 +++++++++++++++++++ Jint/Runtime/Interop/ObjectWrapper.cs | 11 +++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/Jint.Tests/Runtime/InteropExplicitTypeTests.cs b/Jint.Tests/Runtime/InteropExplicitTypeTests.cs index f202249511..18beb51f07 100644 --- a/Jint.Tests/Runtime/InteropExplicitTypeTests.cs +++ b/Jint.Tests/Runtime/InteropExplicitTypeTests.cs @@ -22,6 +22,16 @@ public class CI1 : Super, I1 string I1.Name { get; } = "CI1 as I1"; } + public class BaseValue + { + public string BaseOnly { get; } = "base"; + } + + public class DerivedValue : BaseValue + { + public int DerivedOnlyProperty { get; set; } + } + public class Indexer { private readonly T t; @@ -67,6 +77,11 @@ public InterfaceHolder() } + public class BaseValueHolder + { + public BaseValue Value { get; } = new DerivedValue(); + } + private readonly Engine _engine; private readonly InterfaceHolder holder; @@ -145,6 +160,18 @@ public void SuperClassFromIndexer() Assert.Equal(holder.IndexerSuper[0].Name, _engine.Evaluate("holder.IndexerSuper[0].Name")); } + [Fact] + public void DerivedRuntimePropertyFromBaseDeclaredProperty() + { + var engine = new Engine(options => options.Interop.ThrowOnUnresolvedMember = true); + var holder = new BaseValueHolder(); + engine.SetValue("holder", holder); + + Assert.Equal("base", engine.Evaluate("holder.Value.BaseOnly")); + engine.Execute("const obj = holder.Value; obj.DerivedOnlyProperty = 123;"); + Assert.Equal(123, ((DerivedValue) holder.Value).DerivedOnlyProperty); + } + public struct NullabeStruct : I1 { public NullabeStruct() diff --git a/Jint/Runtime/Interop/ObjectWrapper.cs b/Jint/Runtime/Interop/ObjectWrapper.cs index 585376403d..1c0c169ce8 100644 --- a/Jint/Runtime/Interop/ObjectWrapper.cs +++ b/Jint/Runtime/Interop/ObjectWrapper.cs @@ -215,7 +215,7 @@ public override bool Set(JsValue property, JsValue value, JsValue receiver) if (_properties is null || !_properties.ContainsKey(member)) { // can try utilize fast path - var accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, ClrType, member, mustBeReadable: false, mustBeWritable: true); + var accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, ClrType, member, mustBeReadable: false, mustBeWritable: true, throwOnError: false); var actualType = Target.GetType(); if (ClrType != actualType) { @@ -224,7 +224,7 @@ public override bool Set(JsValue property, JsValue value, JsValue receiver) // 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); + var runtimeAccessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, actualType, member, mustBeReadable: false, mustBeWritable: true, throwOnError: false); if (runtimeAccessor is not IndexerAccessor && runtimeAccessor != ConstantValueAccessor.NullAccessor) { accessor = runtimeAccessor; @@ -232,12 +232,17 @@ public override bool Set(JsValue property, JsValue value, JsValue receiver) } else if (ReferenceEquals(accessor, ConstantValueAccessor.NullAccessor)) { - accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, actualType, member, mustBeReadable: false, mustBeWritable: true); + accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, actualType, member, mustBeReadable: false, mustBeWritable: true, throwOnError: false); } } if (ReferenceEquals(accessor, ConstantValueAccessor.NullAccessor)) { + if (_engine.Options.Interop.ThrowOnUnresolvedMember) + { + throw new MissingMemberException($"Cannot access property '{member}' on type '{ClrType.FullName}"); + } + // there's no such property, but we can allow extending by calling base // which will add properties, this allows for example JS class to extend a CLR type return base.Set(property, value, receiver);