Jelajahi Sumber

Allow host to handle unknown references (#410)

Fixes #409
Ayende Rahien 8 tahun lalu
induk
melakukan
4223c30051

+ 128 - 0
Jint.Tests/Runtime/NullPropogation.cs

@@ -0,0 +1,128 @@
+using Jint.Native;
+using Jint.Native.Object;
+using Jint.Parser;
+using Jint.Runtime;
+using Jint.Runtime.Interop;
+using Jint.Runtime.References;
+using Xunit;
+
+namespace Jint.Tests.Runtime
+{
+    public class NullPropogation
+    {
+        public class NullPropgationReferenceResolver : IReferenceResolver
+        {
+            public bool TryUnresolvableReference(Engine engine, Reference reference, out JsValue value)
+            {
+                value = reference.GetBase();
+                return true;
+            }
+
+            public bool TryPropertyReference(Engine engine, Reference reference, ref JsValue value)
+            {
+                return value.IsNull() || value.IsUndefined();
+            }
+
+            public bool TryGetCallable(Engine engine, object reference, out JsValue value)
+            {
+                value = new JsValue(
+                    new ClrFunctionInstance(engine, (thisObj, values) => thisObj)
+                );
+                return true;
+            }
+            
+            public bool CheckCoercible(JsValue value)
+            {
+                return true;
+            }
+        }
+
+        [Fact]
+        public void NullPropagation()
+        {
+            var engine = new Engine(cfg => cfg.SetReferencesResolver(new NullPropgationReferenceResolver()));
+
+            const string Script = @"
+var input = { 
+	Address : null 
+};
+
+var address = input.Address;
+var city = input.Address.City;
+var length = input.Address.City.length;
+
+var output = {
+	Count1 : input.Address.City.length,
+	Count2 : this.XYZ.length
+};
+";
+
+            engine.Execute(Script);
+
+            var address = engine.GetValue("address");
+            var city = engine.GetValue("city");
+            var length = engine.GetValue("length");
+            var output = engine.GetValue("output").AsObject();
+
+            Assert.Equal(Null.Instance, address);
+            Assert.Equal(Null.Instance, city);
+            Assert.Equal(Null.Instance, length);
+
+            Assert.Equal(Null.Instance, output.Get("Count1"));
+            Assert.Equal(Undefined.Instance, output.Get("Count2"));
+        }
+
+        [Fact]
+        public void NullPropagationFromArg()
+        {
+            var engine = new Engine(cfg => cfg.SetReferencesResolver(new NullPropgationReferenceResolver()));
+
+
+            const string Script = @"
+function test(arg) {
+    return arg.Name;
+}
+
+function test2(arg) {
+    return arg.Name.toUpperCase();
+}
+";
+            engine.Execute(Script);
+            var result = engine.Invoke("test", Null.Instance);
+
+            Assert.Equal(Null.Instance, result);
+
+            result = engine.Invoke("test2", Null.Instance);
+
+            Assert.Equal(Null.Instance, result);
+        }
+
+        [Fact]
+        public void NullPropagationShouldNotAffectOperators()
+        {
+            var engine = new Engine(cfg => cfg.SetReferencesResolver(new NullPropgationReferenceResolver()));
+
+            var jsObject = engine.Object.Construct(Arguments.Empty);
+            jsObject.Put("NullField", JsValue.Null, true);
+
+            var script = @"
+this.is_nullfield_not_null = this.NullField !== null;
+this.is_notnullfield_not_null = this.NotNullField !== null;
+this.has_emptyfield_not_null = this.EmptyField !== null;
+";
+
+            var wrapperScript = string.Format(@"function ExecutePatchScript(docInner){{ (function(doc){{ {0} }}).apply(docInner); }};", script);
+
+            engine.Execute(wrapperScript, new ParserOptions
+            {
+                Source = "main.js"
+            });
+
+            engine.Invoke("ExecutePatchScript", jsObject);
+
+            Assert.False(jsObject.Get("is_nullfield_not_null").AsBoolean());
+            Assert.True(jsObject.Get("is_notnullfield_not_null").AsBoolean());
+            Assert.True(jsObject.Get("has_emptyfield_not_null").AsBoolean());
+        }
+    }
+}

+ 11 - 0
Jint/Engine.cs

@@ -511,6 +511,11 @@ namespace Jint
 
             if (reference.IsUnresolvableReference())
             {
+                if (Options._ReferenceResolver != null &&
+                    Options._ReferenceResolver.TryUnresolvableReference(this, reference, out JsValue val))
+                {
+                    return val;
+                }
                 throw new JavaScriptException(ReferenceError, reference.GetReferencedName() + " is not defined");
             }
 
@@ -518,6 +523,12 @@ namespace Jint
 
             if (reference.IsPropertyReference())
             {
+                if (Options._ReferenceResolver != null &&
+                    Options._ReferenceResolver.TryPropertyReference(this, reference, ref baseValue))
+                {
+                    return baseValue;
+                }
+                
                 if (reference.HasPrimitiveBase() == false)
                 {
                     var o = TypeConverter.ToObject(this, baseValue);

+ 10 - 0
Jint/Options.cs

@@ -22,6 +22,7 @@ namespace Jint
         private TimeZoneInfo _localTimeZone = TimeZoneInfo.Local;
         private List<Assembly> _lookupAssemblies = new List<Assembly>();
         private Predicate<Exception> _clrExceptionsHandler;
+        private IReferenceResolver _referenceResolver;
 
         /// <summary>
         /// When called, doesn't initialize the global scope.
@@ -145,6 +146,12 @@ namespace Jint
             return this;
         }
 
+        public Options SetReferencesResolver(IReferenceResolver resolver)
+        {
+            _referenceResolver = resolver;
+            return this;
+        }
+
         internal bool _IsGlobalDiscarded => _discardGlobal;
 
         internal bool _IsStrict => _strict;
@@ -170,5 +177,8 @@ namespace Jint
         internal CultureInfo _Culture => _culture;
 
         internal TimeZoneInfo _LocalTimeZone => _localTimeZone;
+
+        internal IReferenceResolver  _ReferenceResolver => _referenceResolver;
+
     }
 }

+ 9 - 2
Jint/Runtime/ExpressionIntepreter.cs

@@ -3,6 +3,7 @@ using System.Linq;
 using Jint.Native;
 using Jint.Native.Function;
 using Jint.Native.Number;
+using Jint.Native.Object;
 using Jint.Parser.Ast;
 using Jint.Runtime.Descriptors;
 using Jint.Runtime.Environments;
@@ -846,12 +847,18 @@ namespace Jint.Runtime
 
             if (func == Undefined.Instance)
             {
-                throw new JavaScriptException(_engine.TypeError, r == null ? "" : string.Format("Object has no method '{0}'", (callee as Reference).GetReferencedName()));
+                throw new JavaScriptException(_engine.TypeError, r == null ? "" : string.Format("Object has no method '{0}'", r.GetReferencedName()));
             }
 
             if (!func.IsObject())
             {
-                throw new JavaScriptException(_engine.TypeError, r == null ? "" : string.Format("Property '{0}' of object is not a function", (callee as Reference).GetReferencedName()));
+
+                if (_engine.Options._ReferenceResolver == null ||
+                    !_engine.Options._ReferenceResolver.TryGetCallable(_engine, callee, out func))
+                {
+                    throw new JavaScriptException(_engine.TypeError,
+                        r == null ? "" : string.Format("Property '{0}' of object is not a function", r.GetReferencedName()));
+                }
             }
 
             var callable = func.TryCast<ICallable>();

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

@@ -0,0 +1,13 @@
+using Jint.Native;
+using Jint.Runtime.References;
+
+namespace Jint.Runtime.Interop
+{
+    public interface IReferenceResolver
+    {
+        bool TryUnresolvableReference(Engine engine, Reference reference, out JsValue value);
+        bool TryPropertyReference(Engine engine, Reference reference, ref JsValue value);
+        bool TryGetCallable(Engine engine, object callee, out JsValue value);
+        bool CheckCoercible(JsValue value);
+    }
+}

+ 4 - 0
Jint/Runtime/TypeConverter.cs

@@ -347,6 +347,10 @@ namespace Jint.Runtime
             if (o != Undefined.Instance && o != Null.Instance)
                 return;
 
+            if (engine.Options._ReferenceResolver != null && 
+                engine.Options._ReferenceResolver.CheckCoercible(o))
+                return;
+
             var message = string.Empty;
             var reference = baseReference as Reference;
             if (reference != null)