瀏覽代碼

Support extension methods (#812)

Marko Lahma 4 年之前
父節點
當前提交
caf8ff8b43

+ 1 - 0
Jint.Tests/Jint.Tests.csproj

@@ -14,6 +14,7 @@
     <Reference Include="Microsoft.CSharp" Condition=" '$(TargetFramework)' == 'net461' " />
   </ItemGroup>
   <ItemGroup>
+    <PackageReference Include="Flurl.Http.Signed" Version="3.0.0" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
     <PackageReference Include="MongoDB.Bson.signed" Version="2.11.2" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />

+ 16 - 0
Jint.Tests/RunnableInDebugOnlyAttribute.cs

@@ -0,0 +1,16 @@
+using System.Diagnostics;
+using Xunit;
+
+namespace Jint.Tests
+{
+    public class RunnableInDebugOnlyAttribute : FactAttribute
+    {
+        public RunnableInDebugOnlyAttribute()
+        {
+            if (!Debugger.IsAttached)
+            {
+                Skip = "Only running in interactive mode.";
+            }
+        }
+    }
+}

+ 0 - 6
Jint.Tests/Runtime/Debugger/BreakPointTests.cs

@@ -1,11 +1,5 @@
 using Esprima;
 using Jint.Runtime.Debugger;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading.Tasks;
 using Xunit;
 
 namespace Jint.Tests.Runtime.Debugger

+ 0 - 5
Jint.Tests/Runtime/Debugger/CallStackTests.cs

@@ -1,9 +1,4 @@
 using Jint.Runtime.Debugger;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
 using Xunit;
 
 namespace Jint.Tests.Runtime.Debugger

+ 0 - 2
Jint.Tests/Runtime/Debugger/ScopeTests.cs

@@ -1,6 +1,4 @@
 using Jint.Native;
-using Jint.Runtime.Debugger;
-using System;
 using Xunit;
 
 namespace Jint.Tests.Runtime.Debugger

+ 1 - 7
Jint.Tests/Runtime/Debugger/StepModeTests.cs

@@ -1,10 +1,4 @@
-using Esprima.Ast;
-using Jint.Runtime.Debugger;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using Jint.Runtime.Debugger;
 using Xunit;
 
 namespace Jint.Tests.Runtime.Debugger

+ 0 - 4
Jint.Tests/Runtime/Debugger/TestHelpers.cs

@@ -1,10 +1,6 @@
 using Esprima.Ast;
 using Jint.Runtime.Debugger;
 using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
 using Xunit;
 
 namespace Jint.Tests.Runtime.Debugger

+ 12 - 0
Jint.Tests/Runtime/ExtensionMethods/CustomStringExtensions.cs

@@ -0,0 +1,12 @@
+using System.Linq;
+
+namespace Jint.Tests.Runtime.ExtensionMethods
+{
+    public static class CustomStringExtensions
+    {
+        public static string Backwards(this string value)
+        {
+            return new string(value.Reverse().ToArray());
+        }
+    }
+}

+ 10 - 0
Jint.Tests/Runtime/ExtensionMethods/DoubleExtensions.cs

@@ -0,0 +1,10 @@
+namespace Jint.Tests.Runtime.ExtensionMethods
+{
+    public static class DoubleExtensions
+    {
+        public static double Add(this double integer, int add)
+        {
+            return integer + add;
+        }
+    }
+}

+ 49 - 0
Jint.Tests/Runtime/ExtensionMethods/ExtensionMethodsTest.cs

@@ -0,0 +1,49 @@
+using Jint.Tests.Runtime.Domain;
+using Xunit;
+
+namespace Jint.Tests.Runtime.ExtensionMethods
+{
+    public class ExtensionMethodsTest
+    {
+        [Fact]
+        public void ShouldInvokeObjectExtensionMethod()
+        {
+            var person = new Person();
+            person.Name = "Mickey Mouse";
+            person.Age = 35;
+
+            var options = new Options();
+            options.AddExtensionMethods(typeof(PersonExtensions));
+
+            var engine = new Engine(options);
+            engine.SetValue("person", person);
+            var age = engine.Execute("person.MultiplyAge(2)").GetCompletionValue().AsInteger();
+
+            Assert.Equal(70, age);
+        }
+
+        [Fact]
+        public void ShouldInvokeStringExtensionMethod()
+        {
+            var options = new Options();
+            options.AddExtensionMethods(typeof(CustomStringExtensions));
+
+            var engine = new Engine(options);
+            var result = engine.Execute("\"Hello World!\".Backwards()").GetCompletionValue().AsString();
+
+            Assert.Equal("!dlroW olleH", result);
+        }
+
+        [Fact]
+        public void ShouldInvokeNumberExtensionMethod()
+        {
+            var options = new Options();
+            options.AddExtensionMethods(typeof(DoubleExtensions));
+
+            var engine = new Engine(options);
+            var result = engine.Execute("let numb = 27; numb.Add(13)").GetCompletionValue().AsInteger();
+
+            Assert.Equal(40, result);
+        }
+    }
+}

+ 33 - 0
Jint.Tests/Runtime/ExtensionMethods/FlurlExtensionTest.cs

@@ -0,0 +1,33 @@
+using Flurl.Http;
+
+namespace Jint.Tests.Runtime.ExtensionMethods
+{
+    public class FlurlExtensionTest
+    {
+        [RunnableInDebugOnlyAttribute]
+        public void CanUseFlurlExtensionMethods()
+        {
+            var engine = new Engine(options =>
+            {
+                options.AddExtensionMethods(
+                    typeof(GeneratedExtensions),
+                    typeof(Flurl.GeneratedExtensions));
+            });
+
+            const string script = @"
+var result = 'https://httpbin.org/anything'
+        .AppendPathSegment('person')
+        .SetQueryParams({ a: 1, b: 2 })
+        .WithOAuthBearerToken('my_oauth_token')
+        .PostJsonAsync({
+            first_name: 'Claire',
+            last_name: 'Underwood'
+         }).GetAwaiter().GetResult();
+";
+
+            engine.Execute(script);
+
+            var result = engine.GetValue("result").ToObject();
+        }
+    }
+}

+ 12 - 0
Jint.Tests/Runtime/ExtensionMethods/PersonExtensions.cs

@@ -0,0 +1,12 @@
+using Jint.Tests.Runtime.Domain;
+
+namespace Jint.Tests.Runtime.ExtensionMethods
+{
+    public static class PersonExtensions
+    {
+        public static int MultiplyAge(this Person person, int factor)
+        {
+            return person.Age * factor;
+        }
+    }
+}

+ 11 - 4
Jint.Tests/Runtime/JsValueConversionTests.cs

@@ -9,10 +9,17 @@ namespace Jint.Tests.Runtime
 {
     public class JsValueConversionTests
     {
+        private Engine _engine;
+
+        public JsValueConversionTests()
+        {
+            _engine = new Engine();
+        }
+
         [Fact]
         public void ShouldBeAnArray()
         {
-            var value = new ArrayInstance(null);
+            var value = new ArrayInstance(_engine);
             Assert.Equal(false, value.IsBoolean());
             Assert.Equal(true, value.IsArray());
             Assert.Equal(false, value.IsDate());
@@ -48,7 +55,7 @@ namespace Jint.Tests.Runtime
         [Fact]
         public void ShouldBeADate()
         {
-            var value = new DateInstance(null);
+            var value = new DateInstance(_engine);
             Assert.Equal(false, value.IsBoolean());
             Assert.Equal(false, value.IsArray());
             Assert.Equal(true, value.IsDate());
@@ -99,7 +106,7 @@ namespace Jint.Tests.Runtime
         [Fact]
         public void ShouldBeAnObject()
         {
-            var value = new ObjectInstance(null);
+            var value = new ObjectInstance(_engine);
             Assert.Equal(false, value.IsBoolean());
             Assert.Equal(false, value.IsArray());
             Assert.Equal(false, value.IsDate());
@@ -116,7 +123,7 @@ namespace Jint.Tests.Runtime
         [Fact]
         public void ShouldBeARegExp()
         {
-            var value = new RegExpInstance(null);
+            var value = new RegExpInstance(_engine);
             Assert.Equal(false, value.IsBoolean());
             Assert.Equal(false, value.IsArray());
             Assert.Equal(false, value.IsDate());

+ 1 - 12
Jint/Engine.cs

@@ -170,7 +170,7 @@ namespace Jint
             // their configuration is delayed to a later step
 
             // trigger initialization
-            Global.GetProperty(JsString.Empty);
+            Global.EnsureInitialized();
 
             // this is implementation dependent, and only to pass some unit tests
             Global._prototype = Object.PrototypeObject;
@@ -199,18 +199,7 @@ namespace Jint
             _argumentsInstancePool = new ArgumentsInstancePool(this);
             _jsValueArrayPool = new JsValueArrayPool();
 
-            if (Options._IsClrAllowed)
-            {
-                Global.SetProperty("System", new PropertyDescriptor(new NamespaceReference(this, "System"), PropertyFlag.AllForbidden));
-                Global.SetProperty("importNamespace", new PropertyDescriptor(new ClrFunctionInstance(
-                    this,
-                    "importNamespace",
-                    (thisObj, arguments) => new NamespaceReference(this, TypeConverter.ToString(arguments.At(0)))), PropertyFlag.AllForbidden));
-            }
-
             Options.Apply(this);
-
-            ClrTypeConverter ??= new DefaultTypeConverter(this);
         }
 
         internal LexicalEnvironment GlobalEnvironment { get; }

+ 32 - 23
Jint/Extensions/ReflectionExtensions.cs

@@ -1,5 +1,8 @@
 using System;
+using System.Collections.Generic;
+using System.Linq;
 using System.Reflection;
+using System.Runtime.CompilerServices;
 
 namespace Jint.Extensions
 {
@@ -7,37 +10,43 @@ namespace Jint.Extensions
     {
         internal static void SetValue(this MemberInfo memberInfo, object forObject, object value)
         {
-            switch (memberInfo.MemberType)
+            if (memberInfo.MemberType == MemberTypes.Field)
             {
-                case MemberTypes.Field:
-                    var fieldInfo = (FieldInfo) memberInfo;
-                    if (value != null && fieldInfo.FieldType.IsAssignableFrom(value.GetType()))
-                    {
-                        fieldInfo.SetValue(forObject, value);
-                    }
-
-                    break;
-                case MemberTypes.Property:
-                    var propertyInfo = (PropertyInfo) memberInfo;
-                    if (value != null && propertyInfo.PropertyType.IsAssignableFrom(value.GetType()))
-                    {
-                        propertyInfo.SetValue(forObject, value);
-                    }
-                    break;
+                var fieldInfo = (FieldInfo) memberInfo;
+                if (value != null && fieldInfo.FieldType.IsInstanceOfType(value))
+                {
+                    fieldInfo.SetValue(forObject, value);
+                }
+            }
+            else if (memberInfo.MemberType == MemberTypes.Property)
+            {
+                var propertyInfo = (PropertyInfo) memberInfo;
+                if (value != null && propertyInfo.PropertyType.IsInstanceOfType(value))
+                {
+                    propertyInfo.SetValue(forObject, value);
+                }
             }
         }
 
         internal static Type GetDefinedType(this MemberInfo memberInfo)
         {
-            switch (memberInfo)
+            return memberInfo switch
             {
-                case PropertyInfo propertyInfo:
-                    return propertyInfo.PropertyType;
-                case FieldInfo fieldInfo:
-                    return fieldInfo.FieldType;
-            }
+                PropertyInfo propertyInfo => propertyInfo.PropertyType,
+                FieldInfo fieldInfo => fieldInfo.FieldType,
+                _ => null
+            };
+        }
 
-            return null;
+        internal static IEnumerable<MethodInfo> GetExtensionMethods(this Type type)
+        {
+            return type.GetMethods(BindingFlags.Public | BindingFlags.Static)
+                .Where(m => m.IsExtensionMethod());
+        }
+
+        private static bool IsExtensionMethod(this MethodBase methodInfo)
+        {
+            return methodInfo.IsDefined(typeof(ExtensionAttribute), true);
         }
     }
 }

+ 1 - 2
Jint/Native/Object/ObjectInstance.cs

@@ -256,7 +256,6 @@ namespace Jint.Native.Object
             return keys;
         }
 
-
         protected virtual void AddProperty(JsValue property, PropertyDescriptor descriptor)
         {
             SetProperty(property, descriptor);
@@ -860,7 +859,7 @@ namespace Jint.Native.Object
         }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        protected void EnsureInitialized()
+        protected internal void EnsureInitialized()
         {
             if (_initialized)
             {

+ 69 - 7
Jint/Options.cs

@@ -1,12 +1,16 @@
 using System;
 using System.Collections.Generic;
+using System.Dynamic;
 using System.Globalization;
 using System.Linq;
 using System.Reflection;
 using Jint.Native;
 using Jint.Native.Object;
+using Jint.Runtime;
 using Jint.Runtime.Interop;
 using Jint.Runtime.Debugger;
+using Jint.Runtime.Descriptors;
+using Jint.Runtime.Interop.Reflection;
 using Jint.Runtime.References;
 
 namespace Jint
@@ -15,22 +19,25 @@ namespace Jint
 
     public sealed class Options
     {
-        private readonly List<IConstraint> _constraints = new List<IConstraint>();
+        private readonly List<IConstraint> _constraints = new();
         private bool _strict;
         private DebuggerStatementHandling _debuggerStatementHandling;
         private bool _allowClr;
         private bool _allowClrWrite = true;
-        private readonly List<IObjectConverter> _objectConverters = new List<IObjectConverter>();
+        private readonly List<IObjectConverter> _objectConverters = new();
         private Func<Engine, object, ObjectInstance> _wrapObjectHandler;
         private MemberAccessorDelegate _memberAccessor;
         private int _maxRecursionDepth = -1;
         private TimeSpan _regexTimeoutInterval = TimeSpan.FromSeconds(10);
         private CultureInfo _culture = CultureInfo.CurrentCulture;
         private TimeZoneInfo _localTimeZone = TimeZoneInfo.Local;
-        private List<Assembly> _lookupAssemblies = new List<Assembly>();
+        private List<Assembly> _lookupAssemblies = new();
         private Predicate<Exception> _clrExceptionsHandler;
         private IReferenceResolver _referenceResolver = DefaultReferenceResolver.Instance;
-        private readonly List<Action<Engine>> _configurations = new List<Action<Engine>>();
+        private readonly List<Action<Engine>> _configurations = new();
+
+        private readonly List<Type> _extensionMethodClassTypes = new();
+        internal ExtensionMethodCache _extensionMethods = ExtensionMethodCache.Empty;
 
         /// <summary>
         /// Run the script in strict mode.
@@ -80,6 +87,45 @@ namespace Jint
             return this;
         }
 
+        public Options AddExtensionMethods(params Type[] types)
+        {
+            _extensionMethodClassTypes.AddRange(types);
+            _extensionMethods = ExtensionMethodCache.Build(_extensionMethodClassTypes);
+            return this;
+        }
+
+        private void AttachExtensionMethodsToPrototypes(Engine engine)
+        {
+            AttachExtensionMethodsToPrototype(engine, engine.Array.PrototypeObject, typeof(Array));
+            AttachExtensionMethodsToPrototype(engine, engine.Boolean.PrototypeObject, typeof(bool));
+            AttachExtensionMethodsToPrototype(engine, engine.Date.PrototypeObject, typeof(DateTime));
+            AttachExtensionMethodsToPrototype(engine, engine.Number.PrototypeObject, typeof(double));
+            AttachExtensionMethodsToPrototype(engine, engine.Object.PrototypeObject, typeof(ExpandoObject));
+            AttachExtensionMethodsToPrototype(engine, engine.RegExp.PrototypeObject, typeof(System.Text.RegularExpressions.Regex));
+            AttachExtensionMethodsToPrototype(engine, engine.String.PrototypeObject, typeof(string));
+        }
+
+        private void AttachExtensionMethodsToPrototype(Engine engine, ObjectInstance prototype, Type objectType)
+        {
+            if (!_extensionMethods.TryGetExtensionMethods(objectType, out var methods))
+            {
+                return;
+            }
+
+            foreach (var overloads in methods.GroupBy(x => x.Name))
+            {
+                var functionInstance = new MethodInfoFunctionInstance(engine, MethodDescriptor.Build(overloads.ToList()));
+                var descriptor = new PropertyDescriptor(functionInstance, PropertyFlag.None);
+
+                // make sure we register both lower case and upper case
+                prototype.SetOwnProperty(overloads.Key, descriptor);
+                if (char.IsUpper(overloads.Key[0]))
+                {
+                    prototype.SetOwnProperty(char.ToLower(overloads.Key[0]) + overloads.Key.Substring(1), descriptor);
+                }
+            }
+        }
+
         /// <summary>
         /// If no known type could be guessed, objects are normally wrapped as an
         /// ObjectInstance using class ObjectWrapper. This function can be used to
@@ -229,6 +275,24 @@ namespace Jint
             {
                 configuration?.Invoke(engine);
             }
+            
+            // add missing bits if needed
+            if (_allowClr)
+            {
+                engine.Global.SetProperty("System", new PropertyDescriptor(new NamespaceReference(engine, "System"), PropertyFlag.AllForbidden));
+                engine.Global.SetProperty("importNamespace", new PropertyDescriptor(new ClrFunctionInstance(
+                    engine, 
+                    "importNamespace",
+                    func: (thisObj, arguments) => new NamespaceReference(engine, TypeConverter.ToString(arguments.At(0)))), PropertyFlag.AllForbidden));
+            }
+
+            if (_extensionMethodClassTypes.Count > 0)
+            {
+                AttachExtensionMethodsToPrototypes(engine);
+            }
+            
+            // ensure defaults
+            engine.ClrTypeConverter ??= new DefaultTypeConverter(engine);
         }
 
         internal bool IsStrict => _strict;
@@ -237,8 +301,6 @@ namespace Jint
 
         internal bool IsDebugMode { get; private set; }
 
-        internal bool _IsClrAllowed => _allowClr;
-
         internal bool _IsClrWriteAllowed => _allowClrWrite;
 
         internal Predicate<Exception> _ClrExceptionsHandler => _clrExceptionsHandler;
@@ -261,7 +323,7 @@ namespace Jint
         internal TimeZoneInfo _LocalTimeZone => _localTimeZone;
 
         internal IReferenceResolver  ReferenceResolver => _referenceResolver;
-        
+
         private sealed class DefaultReferenceResolver : IReferenceResolver
         {
             public static readonly DefaultReferenceResolver Instance = new DefaultReferenceResolver();

+ 0 - 1
Jint/Runtime/Debugger/DebugHandler.cs

@@ -6,7 +6,6 @@ using Jint.Native.Function;
 using Jint.Runtime.Descriptors;
 using Jint.Runtime.Environments;
 using Jint.Runtime.Interop;
-using Jint.Runtime.Interpreter.Statements;
 
 namespace Jint.Runtime.Debugger
 {

+ 2 - 3
Jint/Runtime/Interop/DefaultTypeConverter.cs

@@ -224,7 +224,7 @@ namespace Jint.Runtime.Interop
                 // value types
                 if (type.IsValueType && constructors.Length > 0)
                 {
-                    return null;
+                    ExceptionHelper.ThrowArgumentException("No valid constructors found");
                 }
 
                 // reference types - return null if no valid constructor is found
@@ -242,8 +242,7 @@ namespace Jint.Runtime.Interop
 
                     if (!found)
                     {
-                        // found no valid constructor
-                        return null;
+                        ExceptionHelper.ThrowArgumentException("No valid constructors found");
                     }
                 }
 

+ 4 - 0
Jint/Runtime/Interop/MethodDescriptor.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Reflection;
+using System.Runtime.CompilerServices;
 
 namespace Jint.Runtime.Interop
 {
@@ -10,6 +11,8 @@ namespace Jint.Runtime.Interop
         {
             Method = method;
             Parameters = method.GetParameters();
+            IsExtensionMethod = method.IsDefined(typeof(ExtensionAttribute), true);
+
             foreach (var parameter in Parameters)
             {
                 if (Attribute.IsDefined(parameter, typeof(ParamArrayAttribute)))
@@ -29,6 +32,7 @@ namespace Jint.Runtime.Interop
         public ParameterInfo[] Parameters { get; }
         public bool HasParams { get; }
         public int ParameterDefaultValuesCount { get; }
+        public bool IsExtensionMethod { get; }
 
         public static MethodDescriptor[] Build<T>(List<T> source) where T : MethodBase
         {

+ 29 - 12
Jint/Runtime/Interop/MethodInfoFunctionInstance.cs

@@ -1,4 +1,5 @@
-using System.Globalization;
+using System;
+using System.Globalization;
 using System.Linq.Expressions;
 using System.Reflection;
 using Jint.Native;
@@ -20,10 +21,19 @@ namespace Jint.Runtime.Interop
 
         public override JsValue Call(JsValue thisObject, JsValue[] jsArguments)
         {
-            JsValue[] ArgumentProvider(MethodDescriptor method) =>
-                method.HasParams
+            JsValue[] ArgumentProvider(MethodDescriptor method)
+            {
+                if (method.IsExtensionMethod)
+                {
+                    var jsArgumentsTemp = new JsValue[1 + jsArguments.Length];
+                    jsArgumentsTemp[0] = thisObject;
+                    Array.Copy(jsArguments, 0, jsArgumentsTemp, 1, jsArguments.Length);
+                    jsArguments = jsArgumentsTemp;
+                }
+                return method.HasParams
                     ? ProcessParamsArrays(jsArguments, method)
                     : jsArguments;
+            }
 
             var converter = Engine.ClrTypeConverter;
 
@@ -32,27 +42,34 @@ namespace Jint.Runtime.Interop
             {
                 var method = tuple.Item1;
                 var arguments = tuple.Item2;
+                var methodParameters = method.Parameters;
 
-                if (parameters == null || parameters.Length != arguments.Length)
+                if (parameters == null || parameters.Length != methodParameters.Length)
                 {
-                    parameters = new object[arguments.Length];
+                    parameters = new object[methodParameters.Length];
                 }
-                var methodParameters = method.Parameters;
                 var argumentsMatch = true;
 
-                for (var i = 0; i < arguments.Length; i++)
+                for (var i = 0; i < parameters.Length; i++)
                 {
-                    var parameterType = methodParameters[i].ParameterType;
+                    var methodParameter = methodParameters[i];
+                    var parameterType = methodParameter.ParameterType;
+                    var argument = arguments.Length > i ? arguments[i] : null;
 
                     if (typeof(JsValue).IsAssignableFrom(parameterType))
                     {
-                        parameters[i] = arguments[i];
+                        parameters[i] = argument;
+                    }
+                    else if (argument is null)
+                    {
+                        // optional
+                        parameters[i] = System.Type.Missing;
                     }
-                    else if (parameterType == typeof(JsValue[]) && arguments[i].IsArray())
+                    else if (parameterType == typeof(JsValue[]) && argument.IsArray())
                     {
                         // Handle specific case of F(params JsValue[])
 
-                        var arrayInstance = arguments[i].AsArray();
+                        var arrayInstance = argument.AsArray();
                         var len = TypeConverter.ToInt32(arrayInstance.Get(CommonProperties.Length, this));
                         var result = new JsValue[len];
                         for (uint k = 0; k < len; k++)
@@ -63,7 +80,7 @@ namespace Jint.Runtime.Interop
                     }
                     else
                     {
-                        if (!converter.TryConvert(arguments[i].ToObject(), parameterType, CultureInfo.InvariantCulture, out parameters[i]))
+                        if (!converter.TryConvert(argument.ToObject(), parameterType, CultureInfo.InvariantCulture, out parameters[i]))
                         {
                             argumentsMatch = false;
                             break;

+ 13 - 0
Jint/Runtime/Interop/ObjectWrapper.cs

@@ -311,6 +311,19 @@ namespace Jint.Runtime.Interop
                 }
             }
 
+            if (engine.Options._extensionMethods.TryGetExtensionMethods(type, out var extensionMethods))
+            {
+                var matches = new List<MethodInfo>();
+                foreach (var method in extensionMethods)
+                {
+                    if (EqualsIgnoreCasing(method.Name, memberName))
+                    {
+                        matches.Add(method);
+                    }
+                }
+                return new MethodAccessor(MethodDescriptor.Build(matches));
+            }
+
             return ConstantValueAccessor.NullAccessor;
         }
 

+ 97 - 0
Jint/Runtime/Interop/Reflection/ExtensionMethodCache.cs

@@ -0,0 +1,97 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using Jint.Extensions;
+
+namespace Jint.Runtime.Interop.Reflection
+{
+    /// <summary>
+    /// A extension method lookup that can be shared between engines, build based on extension methods provided via options.
+    /// </summary>
+    internal class ExtensionMethodCache
+    {
+        internal static readonly ExtensionMethodCache Empty = new(new Dictionary<Type, MethodInfo[]>());
+
+        // starting point containing only extension methods targeting one type, based on given options configuration
+        private readonly Dictionary<Type, MethodInfo[]> _allExtensionMethods;
+
+        // cache of all possibilities for type including base types and implemented interfaces
+        private Dictionary<Type, MethodInfo[]> _extensionMethods = new();
+
+        private ExtensionMethodCache(Dictionary<Type, MethodInfo[]> extensionMethods)
+        {
+            _allExtensionMethods = extensionMethods;
+        }
+		
+        internal static ExtensionMethodCache Build(List<Type> extensionMethodContainerTypes)
+        {
+            var methodsByTarget = extensionMethodContainerTypes
+                .SelectMany(x => x.GetExtensionMethods())
+                .GroupBy(x => x.GetParameters()[0].ParameterType)
+                .ToDictionary(x => x.Key, x => x.ToArray());
+
+            return new ExtensionMethodCache(methodsByTarget);
+        }
+
+        public bool HasMethods => _allExtensionMethods.Count > 0;
+
+        public bool TryGetExtensionMethods(Type objectType, out MethodInfo[] methods)
+        {
+            var methodLookup = _extensionMethods;
+
+            if (methodLookup.TryGetValue(objectType, out methods))
+            {
+                return true;
+            }
+
+            var results = new List<MethodInfo>();
+            if (_allExtensionMethods.TryGetValue(objectType, out var ownExtensions))
+            {
+                results.AddRange(ownExtensions);
+            }
+
+            foreach (var parentType in GetParentTypes(objectType))
+            {
+                if (_allExtensionMethods.TryGetValue(parentType, out var parentExtensions))
+                {
+                    results.AddRange(parentExtensions);
+                }
+            }
+
+            methods = results.ToArray();
+
+            // racy, we don't care, worst case we'll catch up later
+            Interlocked.CompareExchange(ref _extensionMethods, new Dictionary<Type, MethodInfo[]>(methodLookup)
+            {
+                [objectType] = methods
+            }, methodLookup);
+
+            return methods.Length > 0;
+        }
+
+        private static IEnumerable<Type> GetParentTypes(Type type)
+        {
+            // is there any base type?
+            if (type == null)
+            {
+                yield break;
+            }
+
+            // return all implemented or inherited interfaces
+            foreach (var i in type.GetInterfaces())
+            {
+                yield return i;
+            }
+
+            // return all inherited types
+            var currentBaseType = type.BaseType;
+            while (currentBaseType != null)
+            {
+                yield return currentBaseType;
+                currentBaseType = currentBaseType.BaseType;
+            }
+        }
+    }
+}

+ 2 - 20
Jint/Runtime/TypeConverter.cs

@@ -551,7 +551,8 @@ namespace Jint.Runtime
             {
                 var parameterInfos = m.Parameters;
                 var arguments = argumentProvider(m);
-                if (parameterInfos.Length == arguments.Length)
+                if (arguments.Length <= parameterInfos.Length 
+                    && arguments.Length >= parameterInfos.Length - m.ParameterDefaultValuesCount)
                 {
                     if (methods.Length == 0 && arguments.Length == 0)
                     {
@@ -562,25 +563,6 @@ namespace Jint.Runtime
                     matchingByParameterCount ??= new List<Tuple<MethodDescriptor, JsValue[]>>();
                     matchingByParameterCount.Add(new Tuple<MethodDescriptor, JsValue[]>(m, arguments));
                 }
-                else if (parameterInfos.Length > arguments.Length)
-                {
-                    // check if we got enough default values to provide all parameters (or more in case some default values are provided/overwritten)
-                    if (parameterInfos.Length <= arguments.Length + m.ParameterDefaultValuesCount)
-                    {
-                        // create missing arguments from default values
-                        var argsWithDefaults = new JsValue[parameterInfos.Length];
-                        Array.Copy(arguments, argsWithDefaults, arguments.Length);
-                        for (var i = arguments.Length; i < parameterInfos.Length; i++)
-                        {
-                            var param = parameterInfos[i];
-                            var value = JsValue.FromObject(engine, param.DefaultValue);
-                            argsWithDefaults[i] = value;
-                        }
-
-                        matchingByParameterCount ??= new List<Tuple<MethodDescriptor, JsValue[]>>();
-                        matchingByParameterCount.Add(new Tuple<MethodDescriptor, JsValue[]>(m, argsWithDefaults));
-                    }
-                }
             }
 
             if (matchingByParameterCount == null)