瀏覽代碼

Explicit/implicit cast operator search should check parameter type (#1098)

Marko Lahma 3 年之前
父節點
當前提交
0231800a02

+ 13 - 0
Jint.Tests/Runtime/InteropTests.cs

@@ -10,6 +10,7 @@ using Jint.Runtime;
 using Jint.Runtime.Interop;
 using Jint.Tests.Runtime.Converters;
 using Jint.Tests.Runtime.Domain;
+using Jint.Tests.Runtime.TestClasses;
 using MongoDB.Bson;
 using Shapes;
 using Xunit;
@@ -2766,6 +2767,18 @@ namespace Jint.Tests.Runtime
             Assert.Equal("NOT SUPPORTED", ex.Message);
         }
 
+        [Fact]
+        public void ShouldBeAbleToUseConvertibleStructAsMethodParameter()
+        {
+            var engine = new Engine(options => options.AllowOperatorOverloading());
+            engine.SetValue("test", new DiscordTestClass());
+            engine.SetValue("id", new DiscordId("12345"));
+
+            Assert.Equal("12345", engine.Evaluate("String(id)").AsString());
+            Assert.Equal("12345", engine.Evaluate("test.echo('12345')").AsString());
+            Assert.Equal("12345", engine.Evaluate("test.create(12345)").AsString());
+        }
+
         private class Profile
         {
             public int AnyProperty => throw new NotSupportedException("NOT SUPPORTED");

+ 72 - 0
Jint.Tests/Runtime/TestClasses/DiscordId.cs

@@ -0,0 +1,72 @@
+#nullable enable
+
+using System;
+using Jint.Native.Date;
+
+namespace Jint.Tests.Runtime.TestClasses;
+
+public class DiscordTestClass
+{
+    public string Echo(DiscordId id)
+    {
+        return id.ToString();
+    }
+
+    public DiscordId Create(uint value)
+    {
+        return new DiscordId(value);
+    }
+}
+
+public readonly struct DiscordId : IConvertible, IEquatable<DiscordId>
+{
+    private readonly string? _value;
+
+    private string Value => _value ?? "0";
+
+    public override string ToString() => Value;
+
+    public DiscordId(string s)
+    {
+        if (ulong.TryParse(s, out _))
+            _value = s;
+        else
+            throw new FormatException($"{nameof(s)} must consist of decimal digits and cannot be too large");
+    }
+
+    public DiscordId(ulong u)
+    {
+        _value = u.ToString();
+    }
+
+    public override bool Equals(object? obj) => obj is DiscordId id && Equals(id);
+    public bool Equals(DiscordId id) => _value == id._value;
+
+    public override int GetHashCode()
+    {
+        return Value.GetHashCode();
+    }
+
+    public DateTimeOffset CreatedAt => DateTimeOffset.FromUnixTimeMilliseconds((long)((ulong.Parse(Value) >> 22) + (ulong) (DateConstructor.Epoch.Ticks / 1000)));
+
+    TypeCode IConvertible.GetTypeCode() => TypeCode.String;
+    bool IConvertible.ToBoolean(IFormatProvider? provider) => Convert.ToBoolean(Value, provider);
+    byte IConvertible.ToByte(IFormatProvider? provider) => Convert.ToByte(Value, provider);
+    char IConvertible.ToChar(IFormatProvider? provider) => Convert.ToChar(Value, provider);
+    DateTime IConvertible.ToDateTime(IFormatProvider? provider) => CreatedAt.UtcDateTime;
+    decimal IConvertible.ToDecimal(IFormatProvider? provider) => Convert.ToDecimal(Value, provider);
+    double IConvertible.ToDouble(IFormatProvider? provider) => Convert.ToDouble(Value, provider);
+    short IConvertible.ToInt16(IFormatProvider? provider) => Convert.ToInt16(Value, provider);
+    int IConvertible.ToInt32(IFormatProvider? provider) => Convert.ToInt32(Value, provider);
+    long IConvertible.ToInt64(IFormatProvider? provider) => Convert.ToInt64(Value, provider);
+    sbyte IConvertible.ToSByte(IFormatProvider? provider) => Convert.ToSByte(Value, provider);
+    float IConvertible.ToSingle(IFormatProvider? provider) => Convert.ToSingle(Value, provider);
+    string IConvertible.ToString(IFormatProvider? provider) => Value;
+    object IConvertible.ToType(Type conversionType, IFormatProvider? provider) => Convert.ChangeType(Value, conversionType, provider);
+    ushort IConvertible.ToUInt16(IFormatProvider? provider) => Convert.ToUInt16(Value, provider);
+    uint IConvertible.ToUInt32(IFormatProvider? provider) => Convert.ToUInt32(Value, provider);
+    ulong IConvertible.ToUInt64(IFormatProvider? provider) => Convert.ToUInt64(Value, provider);
+
+    public static implicit operator DiscordId(ulong u) => new(u);
+    public static explicit operator DiscordId(string s) => new(s);
+}

+ 24 - 4
Jint/Runtime/Interop/DefaultTypeConverter.cs

@@ -211,10 +211,30 @@ namespace Jint.Runtime.Interop
             {
                 var key = new TypeConversionKey(valueType, type);
 
-                var castOperator = _knownCastOperators.GetOrAdd(key, _ =>
-                    valueType.GetOperatorOverloadMethods()
-                    .Concat(type.GetOperatorOverloadMethods())
-                    .FirstOrDefault(m => type.IsAssignableFrom(m.ReturnType) && m.Name is "op_Implicit" or "op_Explicit"));
+                static MethodInfo CreateValueFactory(TypeConversionKey k)
+                {
+                    foreach (var m in k.Source.GetOperatorOverloadMethods().Concat(k.Target.GetOperatorOverloadMethods()))
+                    {
+                        var parameters = m.GetParameters();
+                        if (parameters.Length != 1)
+                        {
+                            continue;
+                        }
+
+                        if (!parameters[0].ParameterType.IsAssignableFrom(k.Source))
+                        {
+                            continue;
+                        }
+
+                        if (k.Target.IsAssignableFrom(m.ReturnType) && m.Name is "op_Implicit" or "op_Explicit")
+                        {
+                            return m;
+                        }
+                    }
+                    return null;
+                }
+
+                var castOperator = _knownCastOperators.GetOrAdd(key, CreateValueFactory);
 
                 if (castOperator != null)
                 {

+ 1 - 1
Jint/Runtime/TypeConverter.cs

@@ -1193,7 +1193,7 @@ namespace Jint.Runtime
 
             if (CanChangeType(objectValue, paramType))
             {
-                // forcing conversion isn't ideal not ideal, but works, especially for int -> double for example
+                // forcing conversion isn't ideal, but works, especially for int -> double for example
                 return 1;
             }