Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exclude static members from ObjectWrapper by default #1981

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
56 changes: 33 additions & 23 deletions Jint.Tests.PublicInterface/InteropTests.Json.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Dynamic;
using FluentAssertions;
using Jint.Runtime.Interop;

namespace Jint.Tests.PublicInterface;
Expand Down Expand Up @@ -102,31 +103,27 @@ public void CanStringifyTimeSpanUsingCustomToJsonHook()

Assert.Equal(expected, value);
}

[Fact]
public void CanStringifyUsingSerializeToJson()
{
object testObject = new { Foo = "bar", FooBar = new { Foo = 123.45, Foobar = new DateTime(2022, 7, 16, 0, 0, 0, DateTimeKind.Utc) } };

// without interop

var engineNoInterop = new Engine();
engineNoInterop.SetValue("TimeSpan", TypeReference.CreateTypeReference<TimeSpan>(engineNoInterop));
Assert.Throws<Jint.Runtime.JavaScriptException>(
() => engineNoInterop.Evaluate("JSON.stringify(TimeSpan.FromSeconds(3))"));

engineNoInterop.SetValue("TestObject", testObject);
Assert.Equal(
"{\"Foo\":\"bar\",\"FooBar\":{\"Foo\":123.45,\"Foobar\":\"2022-07-16T00:00:00.000Z\"}}",
engineNoInterop.Evaluate("JSON.stringify(TestObject)"));


var e = new Engine();
e.SetValue("TimeSpan", typeof(TimeSpan));
#if NETFRAMEWORK
e.Evaluate("JSON.stringify(TimeSpan.FromSeconds(3))").AsString().Should().Be("""{"Ticks":30000000,"Days":0,"Hours":0,"Milliseconds":0,"Minutes":0,"Seconds":3,"TotalDays":0.00003472222222222222,"TotalHours":0.0008333333333333333,"TotalMilliseconds":3000,"TotalMinutes":0.05,"TotalSeconds":3}""");
#else
e.Evaluate("JSON.stringify(TimeSpan.FromSeconds(3))").AsString().Should().Be("""{"Ticks":30000000,"Days":0,"Hours":0,"Milliseconds":0,"Microseconds":0,"Nanoseconds":0,"Minutes":0,"Seconds":3,"TotalDays":0.00003472222222222222,"TotalHours":0.0008333333333333334,"TotalMilliseconds":3000,"TotalMicroseconds":3000000,"TotalNanoseconds":3000000000,"TotalMinutes":0.05,"TotalSeconds":3}""");
#endif

e.SetValue("TestObject", testObject);
e.Evaluate("JSON.stringify(TestObject)").AsString().Should().Be("""{"Foo":"bar","FooBar":{"Foo":123.45,"Foobar":"2022-07-16T00:00:00.000Z"}}""");

// interop using Newtonsoft serializer, for example with snake case naming

string Serialize(object o) =>
Newtonsoft.Json.JsonConvert.SerializeObject(o,
new Newtonsoft.Json.JsonSerializerSettings {
ContractResolver = new Newtonsoft.Json.Serialization.DefaultContractResolver {
NamingStrategy = new Newtonsoft.Json.Serialization.SnakeCaseNamingStrategy() } });

var engine = new Engine(options =>
{
options.Interop.SerializeToJson = Serialize;
Expand All @@ -136,13 +133,26 @@ string Serialize(object o) =>

var expected = Serialize(TimeSpan.FromSeconds(3));
var actual = engine.Evaluate("JSON.stringify(TimeSpan.FromSeconds(3));");
Assert.Equal(expected, actual);
actual.AsString().Should().Be(expected);

expected = Serialize(testObject);
actual = engine.Evaluate("JSON.stringify(TestObject)");
Assert.Equal(expected, actual);
actual.AsString().Should().Be(expected);

actual = engine.Evaluate("JSON.stringify({ nestedValue: TestObject })");
Assert.Equal($@"{{""nestedValue"":{expected}}}", actual);
actual.AsString().Should().Be($$"""{"nestedValue":{{expected}}}""");
return;

string Serialize(object o)
{
var settings = new Newtonsoft.Json.JsonSerializerSettings
{
ContractResolver = new Newtonsoft.Json.Serialization.DefaultContractResolver
{
NamingStrategy = new Newtonsoft.Json.Serialization.SnakeCaseNamingStrategy()
}
};
return Newtonsoft.Json.JsonConvert.SerializeObject(o, settings);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

<ItemGroup>
<PackageReference Include="Acornima.Extras" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Flurl.Http.Signed" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="MongoDB.Bson.signed" />
Expand Down
9 changes: 6 additions & 3 deletions Jint.Tests/Runtime/InteropTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public class Bar
[Fact]
public void ShouldStringifyNetObjects()
{
_engine.SetValue("foo", new Foo());
_engine.SetValue("foo", typeof(Foo));
var json = _engine.Evaluate("JSON.stringify(foo.GetBar())").AsString();
Assert.Equal("{\"Test\":\"123\"}", json);
}
Expand Down Expand Up @@ -2781,17 +2781,20 @@ static IEnumerable<string> MemberNameCreator(MemberInfo prop)
options.SetTypeResolver(customTypeResolver);
options.AddExtensionMethods(typeof(CustomNamedExtensions));
});

engine.SetValue("o", new CustomNamed());
Assert.Equal("StringField", engine.Evaluate("o.jsStringField").AsString());
Assert.Equal("StringField", engine.Evaluate("o.jsStringField2").AsString());
Assert.Equal("StaticStringField", engine.Evaluate("o.jsStaticStringField").AsString());
Assert.Equal("StringProperty", engine.Evaluate("o.jsStringProperty").AsString());
Assert.Equal("Method", engine.Evaluate("o.jsMethod()").AsString());
Assert.Equal("StaticMethod", engine.Evaluate("o.jsStaticMethod()").AsString());
Assert.Equal("InterfaceStringProperty", engine.Evaluate("o.jsInterfaceStringProperty").AsString());
Assert.Equal("InterfaceMethod", engine.Evaluate("o.jsInterfaceMethod()").AsString());
Assert.Equal("ExtensionMethod", engine.Evaluate("o.jsExtensionMethod()").AsString());

engine.SetValue("CustomNamed", typeof(CustomNamed));
Assert.Equal("StaticStringField", engine.Evaluate("CustomNamed.jsStaticStringField").AsString());
Assert.Equal("StaticMethod", engine.Evaluate("CustomNamed.jsStaticMethod()").AsString());

engine.SetValue("XmlHttpRequest", typeof(CustomNamedEnum));
engine.Evaluate("o.jsEnumProperty = XmlHttpRequest.HEADERS_RECEIVED;");
Assert.Equal((int) CustomNamedEnum.HeadersReceived, engine.Evaluate("o.jsEnumProperty").AsNumber());
Expand Down
5 changes: 5 additions & 0 deletions Jint/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,11 @@ public class InteropOptions
/// All other values are ignored.
/// </summary>
public MemberTypes ObjectWrapperReportedMemberTypes { get; set; } = MemberTypes.Field | MemberTypes.Property | MemberTypes.Method;

/// <summary>
/// Reported member binding flags when reflecting, defaults to <see cref="BindingFlags.Instance" /> | <see cref="BindingFlags.Public" />.
/// </summary>
public BindingFlags ObjectWrapperReportedBindingFlags { get; set; } = BindingFlags.Instance | BindingFlags.Public;
}

public class ConstraintOptions
Expand Down
5 changes: 2 additions & 3 deletions Jint/Runtime/Interop/ObjectWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,8 @@ private IEnumerable<JsValue> EnumerateOwnPropertyKeys(Types types)
{
var interopOptions = _engine.Options.Interop;

// we take public properties, fields and methods
var bindingFlags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public;

// we take properties, fields and methods
var bindingFlags = interopOptions.ObjectWrapperReportedBindingFlags;
if ((interopOptions.ObjectWrapperReportedMemberTypes & MemberTypes.Property) == MemberTypes.Property)
{
foreach (var p in ClrType.GetProperties(bindingFlags))
Expand Down
4 changes: 2 additions & 2 deletions Jint/Runtime/Interop/TypeResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,11 @@ private ReflectionAccessor ResolvePropertyDescriptorFactory(
// we can always check indexer if there's one, and then fall back to properties if indexer returns null
IndexerAccessor.TryFindIndexer(engine, type, memberName, out var indexerAccessor, out var indexer);

const BindingFlags BindingFlags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public;
var bindingFlags = engine.Options.Interop.ObjectWrapperReportedBindingFlags;

// properties and fields cannot be numbers
if (!isInteger
&& TryFindMemberAccessor(engine, type, memberName, BindingFlags, indexer, out var temp)
&& TryFindMemberAccessor(engine, type, memberName, bindingFlags, indexer, out var temp)
&& (!mustBeReadable || temp.Readable)
&& (!mustBeWritable || temp.Writable))
{
Expand Down