Skip to content

Commit

Permalink
Support optional constructor parameters under interop (#1517)
Browse files Browse the repository at this point in the history
Co-authored-by: Marko Lahma <[email protected]>
  • Loading branch information
mainlyer and lahma authored Mar 29, 2023
1 parent 75a7b12 commit 54e1a47
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 2 deletions.
45 changes: 45 additions & 0 deletions Jint.Tests/Runtime/ConstructorSignature.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Jint.Runtime.Interop;

namespace Jint.Tests.Runtime
{
public class ConstructorSignature
{
[Fact]
public void OptionalConstructorParameters()
{
var engine = new Engine();

engine.SetValue("A", TypeReference.CreateTypeReference(engine, typeof(A)));

// ParamArray tests
Assert.Equal("3", engine.Evaluate("new A(1, 2).Result").AsString());
Assert.Equal("3", engine.Evaluate("new A(1, 2, null).Result").AsString());
Assert.Equal("3", engine.Evaluate("new A(1, 2, undefined).Result").AsString());
Assert.Equal("5", engine.Evaluate("new A(1, 2, null, undefined).Result").AsString());
Assert.Equal("9", engine.Evaluate("new A(1, 2, ...'packed').Result").AsString());
Assert.Equal("3", engine.Evaluate("new A(1, 2, []).Result").AsString());
Assert.Equal("7", engine.Evaluate("new A(1, 2, [...'abcd']).Result").AsString());

// Optional parameter tests
Assert.Equal("3", engine.Evaluate("new A(1, 2).Result").AsString());
Assert.Equal("6", engine.Evaluate("new A(1).Result").AsString());
Assert.Equal("7", engine.Evaluate("new A(2, undefined).Result").AsString());
Assert.Equal("8", engine.Evaluate("new A(3, undefined).Result").AsString());
Assert.Equal("ab", engine.Evaluate("new A('a').Result").AsString());
Assert.Equal("ab", engine.Evaluate("new A('a', undefined).Result").AsString());
Assert.Equal("ac", engine.Evaluate("new A('a', 'c').Result").AsString());
Assert.Equal("adc", engine.Evaluate("new A('a', 'd', undefined).Result").AsString());
Assert.Equal("ade", engine.Evaluate("new A('a', 'd', 'e').Result").AsString());
}

public class A
{
public A(int param1, int param2 = 5) => Result = (param1 + param2).ToString();
public A(string param1, string param2 = "b") => Result = string.Concat(param1, param2);
public A(string param1, string param2 = "b", string param3 = "c") => Result = string.Concat(param1, param2, param3);
public A(int param1, int param2, params object[] param3) => Result = (param1 + param2 + param3?.Length).ToString();

public string Result { get; }
}
}
}
88 changes: 86 additions & 2 deletions Jint/Runtime/Interop/TypeReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,93 @@ static ObjectInstance ObjectCreator(Engine engine, Realm realm, ObjectCreateStat
referenceType,
t => MethodDescriptor.Build(t.GetConstructors(BindingFlags.Public | BindingFlags.Instance)));

foreach (var (method, _, _) in TypeConverter.FindBestMatch(engine, constructors, _ => arguments))
var argumentProvider = new Func<MethodDescriptor, JsValue[]>(method =>
{
var retVal = method.Call(engine, null, arguments);
var parameters = method.Parameters;

if (parameters.Length == 0)
{
return arguments;
}

var newArguments = new JsValue[parameters.Length];
var currentParameter = parameters[parameters.Length - 1];
var isParamArray = currentParameter.ParameterType.IsArray &&
currentParameter.GetCustomAttribute(typeof(ParamArrayAttribute)) is not null;

// last parameter is a ParamArray
if (isParamArray && arguments.Length >= parameters.Length - 1)
{
var currentArgument = JsValue.Undefined;

if (arguments.Length > parameters.Length - 1)
{
currentArgument = arguments[parameters.Length - 1];
}

// nothing to do, is an array as expected
if (currentArgument.IsArray())
{
return arguments;
}

Array.Copy(arguments, 0, newArguments, 0, parameters.Length - 1);

// the last argument is null or undefined and there are exactly the same arguments and parameters
if (currentArgument.IsNullOrUndefined() && parameters.Length == arguments.Length)
{
// this fix the issue with CLR that receives a null ParamArray instead of an empty one
newArguments[parameters.Length - 1] = new JsArray(engine, 0);
return newArguments;
}

// pack the rest of the arguments into an array, as CLR expects
var paramArray = new JsValue[Math.Max(0, arguments.Length - (parameters.Length - 1))];
if (paramArray.Length > 0)
{
Array.Copy(arguments, parameters.Length - 1, paramArray, 0, paramArray.Length);
}
newArguments[parameters.Length - 1] = new JsArray(engine, paramArray);

return newArguments;
}
// TODO: edge case, last parameter is ParamArray with optional parameter before?
else if (isParamArray && arguments.Length < parameters.Length - 1)
{
return arguments;
}
// optional parameters
else if (parameters.Length >= arguments.Length)
{
Array.Copy(arguments, 0, newArguments, 0, arguments.Length);

for (var i = parameters.Length - 1; i >= 0; i--)
{
currentParameter = parameters[i];

if (i >= arguments.Length - 1)
{
if (!currentParameter.IsOptional)
{
break;
}

if (arguments.Length - 1 < i || arguments[i].IsUndefined())
{
newArguments[i] = JsValue.FromObject(engine, currentParameter.DefaultValue);
}
}
}

return newArguments;
}

return arguments;
});

foreach (var (method, methodArguments, _) in TypeConverter.FindBestMatch(engine, constructors, argumentProvider))
{
var retVal = method.Call(engine, null, methodArguments);
result = TypeConverter.ToObject(realm, retVal);

// todo: cache method info
Expand Down

0 comments on commit 54e1a47

Please sign in to comment.