using Jint.Native; using Jint.Runtime; using Jint.Runtime.Interop; namespace Jint.Tests.Runtime; public class NullPropagation { public class NullPropagationReferenceResolver : IReferenceResolver { public bool TryUnresolvableReference(Engine engine, Reference reference, out JsValue value) { value = reference.Base; return true; } public bool TryPropertyReference(Engine engine, Reference reference, ref JsValue value) { return value.IsNull() || value.IsUndefined(); } public bool TryGetCallable(Engine engine, object callee, out JsValue value) { if (callee is Reference reference) { var name = reference.ReferencedName.AsString(); if (name == "filter") { value = new ClrFunction(engine, "map", (thisObj, values) => engine.Realm.Intrinsics.Array.ArrayCreate(0)); return true; } } value = new ClrFunction(engine, "anonymous", (thisObj, values) => thisObj); return true; } public bool CheckCoercible(JsValue value) { return true; } } [Fact] public void CanCallFilterOnNull() { var engine = new Engine(cfg => cfg.SetReferencesResolver(new NullPropagationReferenceResolver())); const string Script = @" var input = {}; var output = { Tags : input.Tags.filter(x=>x!=null) }; "; engine.Execute(Script); var output = engine.GetValue("output").AsObject(); Assert.True(output.Get("Tags").IsArray()); } [Fact] public void NullPropagationTest() { var engine = new Engine(cfg => cfg.SetReferencesResolver(new NullPropagationReferenceResolver())); 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(JsValue.Null, address); Assert.Equal(JsValue.Null, city); Assert.Equal(JsValue.Null, length); Assert.Equal(JsValue.Null, output.Get("Count1")); Assert.Equal(JsValue.Undefined, output.Get("Count2")); } [Fact] public void NullPropagationFromArg() { var engine = new Engine(cfg => cfg.SetReferencesResolver(new NullPropagationReferenceResolver())); const string Script = @" function test(arg) { return arg.Name; } function test2(arg) { return arg.Name.toUpperCase(); } "; engine.Execute(Script); var result = engine.Invoke("test", JsValue.Null); Assert.Equal(JsValue.Null, result); result = engine.Invoke("test2", JsValue.Null); Assert.Equal(JsValue.Null, result); } [Fact] public void NullPropagationShouldNotAffectOperators() { var engine = new Engine(cfg => cfg.SetReferencesResolver(new NullPropagationReferenceResolver())); var jsObject = engine.Realm.Intrinsics.Object.Construct(Arguments.Empty); jsObject.Set("NullField", JsValue.Null); 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 = $@"function ExecutePatchScript(docInner){{ (function(doc){{ {script} }}).apply(docInner); }};"; engine.Execute(wrapperScript, "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()); } }