Ver código fonte

Allow controlling exposed names for interop members (#974)

Marko Lahma 3 anos atrás
pai
commit
7a74e0c369

+ 67 - 0
Jint.Tests/Runtime/Domain/CustomNamed.cs

@@ -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 - 0
Jint.Tests/Runtime/InteropTests.cs

@@ -2701,5 +2701,51 @@ namespace Jint.Tests.Runtime
 
             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());
+        }
+
     }
 }

+ 20 - 20
Jint/Runtime/Interop/TypeReference.cs

@@ -16,29 +16,23 @@ namespace Jint.Runtime.Interop
         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)
@@ -148,16 +142,22 @@ namespace Jint.Runtime.Interop
             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));
+                        }
                     }
                 }
 

+ 52 - 17
Jint/Runtime/Interop/TypeResolver.cs

@@ -21,6 +21,17 @@ namespace Jint.Runtime.Interop
         /// </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.
@@ -81,6 +92,7 @@ namespace Jint.Runtime.Interop
             // 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())
@@ -96,10 +108,13 @@ namespace Jint.Runtime.Interop
                         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);
+                        }
                     }
                 }
             }
@@ -120,10 +135,13 @@ namespace Jint.Runtime.Interop
                         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);
+                        }
                     }
                 }
             }
@@ -152,9 +170,12 @@ namespace Jint.Runtime.Interop
                         continue;
                     }
 
-                    if (typeResolverMemberNameComparer.Equals(method.Name, memberName))
+                    foreach (var name in typeResolverMemberNameCreator(method))
                     {
-                        matches.Add(method);
+                        if (typeResolverMemberNameComparer.Equals(name, memberName))
+                        {
+                            matches.Add(method);
+                        }
                     }
                 }
 
@@ -176,6 +197,8 @@ namespace Jint.Runtime.Interop
         {
             // 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))
@@ -185,10 +208,16 @@ namespace Jint.Runtime.Interop
 
                 // 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;
+                        }
+                    }
                 }
             }
 
@@ -207,10 +236,13 @@ namespace Jint.Runtime.Interop
                     continue;
                 }
 
-                if (MemberNameComparer.Equals(f.Name, memberName))
+                foreach (var name in typeResolverMemberNameCreator(f))
                 {
-                    field = f;
-                    break;
+                    if (memberNameComparer.Equals(name, memberName))
+                    {
+                        field = f;
+                        break;
+                    }
                 }
             }
 
@@ -229,10 +261,13 @@ namespace Jint.Runtime.Interop
                     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);
+                    }
                 }
             }