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

Allow controlling exposed names for interop members #974

Merged
merged 1 commit into from
Oct 11, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
67 changes: 67 additions & 0 deletions Jint.Tests/Runtime/Domain/CustomNamed.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System;

namespace Jint.Tests.Runtime.Domain
{
[AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
public class CustomNameAttribute : Attribute
{
public CustomNameAttribute(string name)
{
Name = name;
}

public string Name { get; }
}

public interface ICustomNamed
{
[CustomName("jsInterfaceStringProperty")]
public string InterfaceStringProperty { get; }

[CustomName("jsInterfaceMethod")]
public string InterfaceMethod();
}

[CustomName("jsCustomName")]
public class CustomNamed : ICustomNamed
{
[CustomName("jsStringField")]
[CustomName("jsStringField2")]
public string StringField = "StringField";

[CustomName("jsStaticStringField")]
public static string StaticStringField = "StaticStringField";

[CustomName("jsStringProperty")]
public string StringProperty => "StringProperty";

[CustomName("jsMethod")]
public string Method() => "Method";

[CustomName("jsStaticMethod")]
public static string StaticMethod() => "StaticMethod";

public string InterfaceStringProperty => "InterfaceStringProperty";

public string InterfaceMethod() => "InterfaceMethod";

[CustomName("jsEnumProperty")]
public CustomNamedEnum EnumProperty { get; set; }
}

[CustomName("XmlHttpRequest")]
public enum CustomNamedEnum
{
[CustomName("NONE")]
None = 0,

[CustomName("HEADERS_RECEIVED")]
HeadersReceived = 2
}

public static class CustomNamedExtensions
{
[CustomName("jsExtensionMethod")]
public static string ExtensionMethod(this CustomNamed customNamed) => "ExtensionMethod";
}
}
46 changes: 46 additions & 0 deletions Jint.Tests/Runtime/InteropTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2701,5 +2701,51 @@ public void CanCheckIfCallable()

Assert.True(callable.Call().AsBoolean());
}

[Fact]
public void CanGiveCustomNameToInteropMembers()
{
static IEnumerable<string> MemberNameCreator(MemberInfo prop)
{
var attributes = prop.GetCustomAttributes(typeof(CustomNameAttribute), inherit: true);
if (attributes.Length > 0)
{
foreach (CustomNameAttribute attribute in attributes)
{
yield return attribute.Name;
}
}
else
{
yield return prop.Name;
}
}

var customTypeResolver = new TypeResolver
{
MemberNameCreator = MemberNameCreator
};

var engine = new Engine(options =>
{
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("XmlHttpRequest", typeof(CustomNamedEnum));
engine.Evaluate("o.jsEnumProperty = XmlHttpRequest.HEADERS_RECEIVED;");
Assert.Equal((int) CustomNamedEnum.HeadersReceived, engine.Evaluate("o.jsEnumProperty").AsNumber());
}

}
}
40 changes: 20 additions & 20 deletions Jint/Runtime/Interop/TypeReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,23 @@ public sealed class TypeReference : FunctionInstance, IConstructor, IObjectWrapp
private static readonly ConcurrentDictionary<Type, MethodDescriptor[]> _constructorCache = new();
private static readonly ConcurrentDictionary<Tuple<Type, string>, ReflectionAccessor> _memberAccessors = new();

private TypeReference(
Engine engine,
Realm realm)
: base(engine, realm, _name, FunctionThisMode.Global, ObjectClass.TypeReference)
private TypeReference(Engine engine, Type type)
: base(engine, engine.Realm, _name, FunctionThisMode.Global, ObjectClass.TypeReference)
{
ReferenceType = type;

_prototype = engine.Realm.Intrinsics.Function.PrototypeObject;
_length = PropertyDescriptor.AllForbiddenDescriptor.NumberZero;
_prototypeDescriptor = new PropertyDescriptor(engine.Realm.Intrinsics.Object.PrototypeObject, PropertyFlag.AllForbidden);

PreventExtensions();
}

public Type ReferenceType { get; private set; }
public Type ReferenceType { get; }

public static TypeReference CreateTypeReference(Engine engine, Type type)
{
var obj = new TypeReference(engine, engine.Realm);
obj.PreventExtensions();
obj.ReferenceType = type;

// The value of the [[Prototype]] internal property of the TypeReference constructor is the Function prototype object
obj._prototype = engine.Realm.Intrinsics.Function.PrototypeObject;
obj._length = PropertyDescriptor.AllForbiddenDescriptor.NumberZero;

// The initial value of Boolean.prototype is the Boolean prototype object
obj._prototypeDescriptor = new PropertyDescriptor(engine.Realm.Intrinsics.Object.PrototypeObject, PropertyFlag.AllForbidden);

return obj;
return new TypeReference(engine, type);
}

public override JsValue Call(JsValue thisObject, JsValue[] arguments)
Expand Down Expand Up @@ -148,16 +142,22 @@ private ReflectionAccessor ResolveMemberAccessor(Type type, string name)
if (type.IsEnum)
{
var memberNameComparer = typeResolver.MemberNameComparer;
var typeResolverMemberNameCreator = typeResolver.MemberNameCreator;

var enumValues = Enum.GetValues(type);
var enumNames = Enum.GetNames(type);

for (var i = 0; i < enumValues.Length; i++)
{
if (memberNameComparer.Equals(enumNames.GetValue(i), name))
var enumOriginalName = enumNames.GetValue(i).ToString();
var member = type.GetMember(enumOriginalName)[0];
foreach (var exposedName in typeResolverMemberNameCreator(member))
{
var value = enumValues.GetValue(i);
return new ConstantValueAccessor(JsNumber.Create(value));
if (memberNameComparer.Equals(name, exposedName))
{
var value = enumValues.GetValue(i);
return new ConstantValueAccessor(JsNumber.Create(value));
}
}
}

Expand Down
69 changes: 52 additions & 17 deletions Jint/Runtime/Interop/TypeResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ public sealed class TypeResolver
/// </summary>
public Predicate<MemberInfo> MemberFilter { get; set; } = _ => true;

/// <summary>
/// Gives the exposed names for a member. Allows to expose C# convention following member like IsSelected
/// as more JS idiomatic "selected" for example. Defaults to returning the <see cref="MemberInfo.Name"/> as-is.
/// </summary>
public Func<MemberInfo, IEnumerable<string>> MemberNameCreator { get; set; } = NameCreator;

private static IEnumerable<string> NameCreator(MemberInfo info)
{
yield return info.Name;
}

/// <summary>
/// Sets member name comparison strategy when finding CLR objects members.
/// By default member's first character casing is ignored and rest of the name is compared with strict equality.
Expand Down Expand Up @@ -81,6 +92,7 @@ internal ReflectionAccessor GetAccessor(Engine engine, Type type, string member,
// try to find a single explicit property implementation
List<PropertyInfo> list = null;
var typeResolverMemberNameComparer = MemberNameComparer;
var typeResolverMemberNameCreator = MemberNameCreator;
foreach (var iface in type.GetInterfaces())
{
foreach (var iprop in iface.GetProperties())
Expand All @@ -96,10 +108,13 @@ internal ReflectionAccessor GetAccessor(Engine engine, Type type, string member,
continue;
}

if (typeResolverMemberNameComparer.Equals(iprop.Name, memberName))
foreach (var name in typeResolverMemberNameCreator(iprop))
{
list ??= new List<PropertyInfo>();
list.Add(iprop);
if (typeResolverMemberNameComparer.Equals(name, memberName))
{
list ??= new List<PropertyInfo>();
list.Add(iprop);
}
}
}
}
Expand All @@ -120,10 +135,13 @@ internal ReflectionAccessor GetAccessor(Engine engine, Type type, string member,
continue;
}

if (typeResolverMemberNameComparer.Equals(imethod.Name, memberName))
foreach (var name in typeResolverMemberNameCreator(imethod))
{
explicitMethods ??= new List<MethodInfo>();
explicitMethods.Add(imethod);
if (typeResolverMemberNameComparer.Equals(name, memberName))
{
explicitMethods ??= new List<MethodInfo>();
explicitMethods.Add(imethod);
}
}
}
}
Expand Down Expand Up @@ -152,9 +170,12 @@ internal ReflectionAccessor GetAccessor(Engine engine, Type type, string member,
continue;
}

if (typeResolverMemberNameComparer.Equals(method.Name, memberName))
foreach (var name in typeResolverMemberNameCreator(method))
{
matches.Add(method);
if (typeResolverMemberNameComparer.Equals(name, memberName))
{
matches.Add(method);
}
}
}

Expand All @@ -176,6 +197,8 @@ internal ReflectionAccessor GetAccessor(Engine engine, Type type, string member,
{
// look for a property, bit be wary of indexers, we don't want indexers which have name "Item" to take precedence
PropertyInfo property = null;
var memberNameComparer = MemberNameComparer;
var typeResolverMemberNameCreator = MemberNameCreator;
foreach (var p in type.GetProperties(bindingFlags))
{
if (!MemberFilter(p))
Expand All @@ -185,10 +208,16 @@ internal ReflectionAccessor GetAccessor(Engine engine, Type type, string member,

// only if it's not an indexer, we can do case-ignoring matches
var isStandardIndexer = p.GetIndexParameters().Length == 1 && p.Name == "Item";
if (!isStandardIndexer && MemberNameComparer.Equals(p.Name, memberName))
if (!isStandardIndexer)
{
property = p;
break;
foreach (var name in typeResolverMemberNameCreator(p))
{
if (memberNameComparer.Equals(name, memberName))
{
property = p;
break;
}
}
}
}

Expand All @@ -207,10 +236,13 @@ internal ReflectionAccessor GetAccessor(Engine engine, Type type, string member,
continue;
}

if (MemberNameComparer.Equals(f.Name, memberName))
foreach (var name in typeResolverMemberNameCreator(f))
{
field = f;
break;
if (memberNameComparer.Equals(name, memberName))
{
field = f;
break;
}
}
}

Expand All @@ -229,10 +261,13 @@ internal ReflectionAccessor GetAccessor(Engine engine, Type type, string member,
continue;
}

if (MemberNameComparer.Equals(m.Name, memberName))
foreach (var name in typeResolverMemberNameCreator(m))
{
methods ??= new List<MethodInfo>();
methods.Add(m);
if (memberNameComparer.Equals(name, memberName))
{
methods ??= new List<MethodInfo>();
methods.Add(m);
}
}
}

Expand Down