diff --git a/lib/Runtime/Library/JavascriptBuiltInFunctionList.h b/lib/Runtime/Library/JavascriptBuiltInFunctionList.h index f2690171248..c794e5d1e8a 100644 --- a/lib/Runtime/Library/JavascriptBuiltInFunctionList.h +++ b/lib/Runtime/Library/JavascriptBuiltInFunctionList.h @@ -177,6 +177,8 @@ BUILTIN(JavascriptObject, LookupGetter, EntryLookupGetter, FunctionInfo::ErrorOn BUILTIN(JavascriptObject, LookupSetter, EntryLookupSetter, FunctionInfo::ErrorOnNew) BUILTIN(JavascriptObject, Is, EntryIs, FunctionInfo::ErrorOnNew) BUILTIN(JavascriptObject, Assign, EntryAssign, FunctionInfo::ErrorOnNew) +BUILTIN(JavascriptObject, Values, EntryValues, FunctionInfo::ErrorOnNew) +BUILTIN(JavascriptObject, Entries, EntryEntries, FunctionInfo::ErrorOnNew) BUILTIN(ObjectPrototypeObject, __proto__getter, Entry__proto__getter, FunctionInfo::ErrorOnNew | FunctionInfo::DoNotProfile) BUILTIN(ObjectPrototypeObject, __proto__setter, Entry__proto__setter, FunctionInfo::ErrorOnNew | FunctionInfo::DoNotProfile) BUILTIN(JavascriptRegExp, NewInstance, NewInstance, FunctionInfo::SkipDefaultNewObject) diff --git a/lib/Runtime/Library/JavascriptLibrary.cpp b/lib/Runtime/Library/JavascriptLibrary.cpp index 46ebc174d23..f0d89fe59d1 100644 --- a/lib/Runtime/Library/JavascriptLibrary.cpp +++ b/lib/Runtime/Library/JavascriptLibrary.cpp @@ -3602,6 +3602,14 @@ namespace Js library->AddFunctionToLibraryObject(objectConstructor, PropertyIds::assign, &JavascriptObject::EntryInfo::Assign, 2)); } + if (scriptContext->GetConfig()->IsES7BuiltinsEnabled()) + { + scriptContext->SetBuiltInLibraryFunction(JavascriptObject::EntryInfo::Values.GetOriginalEntryPoint(), + library->AddFunctionToLibraryObject(objectConstructor, PropertyIds::values, &JavascriptObject::EntryInfo::Values, 1)); + scriptContext->SetBuiltInLibraryFunction(JavascriptObject::EntryInfo::Entries.GetOriginalEntryPoint(), + library->AddFunctionToLibraryObject(objectConstructor, PropertyIds::entries, &JavascriptObject::EntryInfo::Entries, 1)); + } + objectConstructor->SetHasNoEnumerableProperties(true); } @@ -6183,6 +6191,12 @@ namespace Js REG_OBJECTS_LIB_FUNC(assign, JavascriptObject::EntryAssign); } + if (config.IsES7BuiltinsEnabled()) + { + REG_OBJECTS_LIB_FUNC(values, JavascriptObject::EntryValues); + REG_OBJECTS_LIB_FUNC(entries, JavascriptObject::EntryEntries); + } + return hr; } diff --git a/lib/Runtime/Library/JavascriptObject.cpp b/lib/Runtime/Library/JavascriptObject.cpp index e23423385b9..35b590bf3fd 100644 --- a/lib/Runtime/Library/JavascriptObject.cpp +++ b/lib/Runtime/Library/JavascriptObject.cpp @@ -1096,6 +1096,92 @@ namespace Js return JavascriptOperators::GetOwnEnumerablePropertyNames(object, scriptContext); } + Var JavascriptObject::GetValuesOrEntries(RecyclableObject* object, bool valuesToReturn, ScriptContext* scriptContext) + { + Assert(object != nullptr); + Assert(scriptContext != nullptr); + JavascriptArray* valuesArray = scriptContext->GetLibrary()->CreateArray(0); + + Var ownKeysVar = JavascriptOperators::GetOwnPropertyNames(object, scriptContext); + JavascriptArray* ownKeysResult = nullptr; + if (JavascriptArray::Is(ownKeysVar)) + { + ownKeysResult = JavascriptArray::FromVar(ownKeysVar); + } + else + { + return valuesArray; + } + + uint32 length = ownKeysResult->GetLength(); + + Var nextKey; + const PropertyRecord* propertyRecord = nullptr; + PropertyId propertyId; + for (uint32 i = 0, index = 0; i < length; i++) + { + nextKey = ownKeysResult->DirectGetItem(i); + Assert(JavascriptString::Is(nextKey)); + + PropertyDescriptor propertyDescriptor; + + BOOL propertyKeyResult = JavascriptConversion::ToPropertyKey(nextKey, scriptContext, &propertyRecord); + Assert(propertyKeyResult); + propertyId = propertyRecord->GetPropertyId(); + Assert(propertyId != Constants::NoProperty); + if (JavascriptOperators::GetOwnPropertyDescriptor(object, propertyId, scriptContext, &propertyDescriptor)) + { + if (propertyDescriptor.IsEnumerable()) + { + Var value = JavascriptOperators::GetProperty(object, propertyId, scriptContext); + if (!valuesToReturn) + { + // For Object.entries each entry is key, value pair + JavascriptArray* entry = scriptContext->GetLibrary()->CreateArray(2); + entry->DirectSetItemAt(0, CrossSite::MarshalVar(scriptContext, nextKey)); + entry->DirectSetItemAt(1, CrossSite::MarshalVar(scriptContext, value)); + value = entry; + } + valuesArray->DirectSetItemAt(index++, CrossSite::MarshalVar(scriptContext, value)); + } + } + } + + return valuesArray; + } + + Var JavascriptObject::EntryValues(RecyclableObject* function, CallInfo callInfo, ...) + { + PROBE_STACK(function->GetScriptContext(), Js::Constants::MinStackDefault); + + ARGUMENTS(args, callInfo); + ScriptContext* scriptContext = function->GetScriptContext(); + + Assert(!(callInfo.Flags & CallFlags_New)); + CHAKRATEL_LANGSTATS_INC_BUILTINCOUNT(ObjectValuesCount); + + Var tempVar = args.Info.Count < 2 ? scriptContext->GetLibrary()->GetUndefined() : args[1]; + RecyclableObject *object = RecyclableObject::FromVar(JavascriptOperators::ToObject(tempVar, scriptContext)); + + return GetValuesOrEntries(object, true /*valuesToReturn*/, scriptContext); + } + + Var JavascriptObject::EntryEntries(RecyclableObject* function, CallInfo callInfo, ...) + { + PROBE_STACK(function->GetScriptContext(), Js::Constants::MinStackDefault); + + ARGUMENTS(args, callInfo); + ScriptContext* scriptContext = function->GetScriptContext(); + + Assert(!(callInfo.Flags & CallFlags_New)); + CHAKRATEL_LANGSTATS_INC_BUILTINCOUNT(ObjectEntriesCount); + + Var tempVar = args.Info.Count < 2 ? scriptContext->GetLibrary()->GetUndefined() : args[1]; + RecyclableObject *object = RecyclableObject::FromVar(JavascriptOperators::ToObject(tempVar, scriptContext)); + + return GetValuesOrEntries(object, false /*valuesToReturn*/, scriptContext); + } + Var JavascriptObject::CreateOwnSymbolPropertiesHelper(RecyclableObject* object, ScriptContext* scriptContext) { return CreateKeysHelper(object, scriptContext, TRUE, true /*includeSymbolsOnly */, false, true /*includeSpecialProperties*/); diff --git a/lib/Runtime/Library/JavascriptObject.h b/lib/Runtime/Library/JavascriptObject.h index 8757863cb92..c7abddc8520 100644 --- a/lib/Runtime/Library/JavascriptObject.h +++ b/lib/Runtime/Library/JavascriptObject.h @@ -47,6 +47,8 @@ namespace Js static FunctionInfo LookupSetter; static FunctionInfo Is; static FunctionInfo Assign; + static FunctionInfo Values; + static FunctionInfo Entries; }; static Var NewInstance(RecyclableObject* function, CallInfo callInfo, ...); @@ -77,7 +79,8 @@ namespace Js static Var EntryLookupSetter(RecyclableObject* function, CallInfo callInfo, ...); static Var EntryIs(RecyclableObject* function, CallInfo callInfo, ...); static Var EntryAssign(RecyclableObject* function, CallInfo callInfo, ...); - + static Var EntryValues(RecyclableObject* function, CallInfo callInfo, ...); + static Var EntryEntries(RecyclableObject* function, CallInfo callInfo, ...); static Var GetPrototypeOf(RecyclableObject* obj, ScriptContext* scriptContext); static BOOL ChangePrototype(RecyclableObject* object, RecyclableObject* newPrototype, bool validate, ScriptContext* scriptContext); @@ -91,6 +94,9 @@ namespace Js static Var GetOwnPropertyDescriptorHelper(RecyclableObject* obj, Var propertyKey, ScriptContext* scriptContext); static BOOL GetOwnPropertyDescriptorHelper(RecyclableObject* obj, PropertyId propertyId, ScriptContext* scriptContext, PropertyDescriptor& propertyDescriptor); + // Param valuesToReturn should be set to true when we are looking for values from an object otherwise entries will be returned + static Var GetValuesOrEntries(RecyclableObject* object, bool valuesToReturn, ScriptContext* scriptContext); + // Presently used in the projection as a mechanism of calling the general object prototype toString. static JavascriptString* ToStringInternal(Var thisArg, ScriptContext* scriptContext) { diff --git a/test/es7/rlexe.xml b/test/es7/rlexe.xml index d69918d067d..1f206e6da01 100644 --- a/test/es7/rlexe.xml +++ b/test/es7/rlexe.xml @@ -38,4 +38,11 @@ -es7asyncawait -es6tostringtag -args summary -endargs + + + valuesAndEntries.js + -ES7Builtins -args summary -endargs + BugFix + + diff --git a/test/es7/valuesAndEntries.js b/test/es7/valuesAndEntries.js new file mode 100644 index 00000000000..5dd28bff24a --- /dev/null +++ b/test/es7/valuesAndEntries.js @@ -0,0 +1,226 @@ +//------------------------------------------------------------------------------------------------------- +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. +//------------------------------------------------------------------------------------------------------- + +WScript.LoadScriptFile("..\\UnitTestFramework\\UnitTestFramework.js"); + +var tests = [ + { + name: "Object.values/entries should exist and constructed properly", + body: function () { + assert.isTrue(Object.hasOwnProperty('values'), "Object should have a values method"); + assert.isTrue(Object.hasOwnProperty('entries'), "Object should have a entries method"); + assert.areEqual(1, Object.values.length, "values method takes one argument"); + assert.areEqual(1, Object.entries.length, "entries method takes one argument"); + assert.areEqual("values", Object.values.name, "values.name is 'values'"); + assert.areEqual("entries", Object.entries.name, "entries.name is 'entries'"); + + var descriptor = Object.getOwnPropertyDescriptor(Object, 'values'); + assert.isTrue(descriptor.writable, "writable(values) must be true"); + assert.isFalse(descriptor.enumerable, "enumerable(values) must be false"); + assert.isTrue(descriptor.configurable, "configurable(values) must be true"); + + descriptor = Object.getOwnPropertyDescriptor(Object, 'entries'); + assert.isTrue(descriptor.writable, "writable(entries) must be true"); + assert.isFalse(descriptor.enumerable, "enumerable(entries) must be false"); + assert.isTrue(descriptor.configurable, "configurable(entries) must be true"); + } + }, + { + name: "Object.values/entries basic syntax", + body: function () { + assert.throws(function () { eval("Object.values();"); }, TypeError, "Calling values method without any arguments throws an error", "Object expected"); + assert.throws(function () { eval("Object.values(undefined);"); }, TypeError, "Calling values method on undefined throws an error", "Object expected"); + assert.throws(function () { eval("Object.values(null);"); }, TypeError, "Calling values method on null throws an error", "Object expected"); + assert.isTrue(Array.isArray(Object.values({})), "calling values method on an object returns an array"); + assert.areEqual(0, Object.values({}).length, "calling values method on an empty object returns an empty array"); + + assert.throws(function () { eval("Object.entries();"); }, TypeError, "Calling entries method without any arguments throws an error", "Object expected"); + assert.throws(function () { eval("Object.entries(undefined);"); }, TypeError, "Calling entries method on undefined throws an error", "Object expected"); + assert.throws(function () { eval("Object.entries(null);"); }, TypeError, "Calling entries method on null throws an error", "Object expected"); + assert.isTrue(Array.isArray(Object.entries({})), "calling entries method on an object returns an array"); + assert.areEqual(0, Object.entries({}).length, "calling entries method on an empty object returns an empty array"); + } + }, + { + name: "Object.values/entries functionality", + body: function () { + var a1 = {prop1:10, prop2:20}; + var values = Object.values(a1); + assert.areEqual(2, values.length, "calling values on an object with two properties will returned an array of 2 elements"); + assert.areEqual(10, values[0], "First element of the returned values array should be 10"); + assert.areEqual(20, values[1], "Second element of the returned values array should be 20"); + + var entries = Object.entries(a1); + assert.areEqual(2, entries.length, "calling entries on an object with two properties will returned an array of 2 elements"); + assert.isTrue(Array.isArray(entries[0]) && Array.isArray(entries[1]), "each element itself an array of key, value pair"); + assert.areEqual(["prop1", 10], entries[0], "First element of the returned entry array should be ['prop1', 10]"); + assert.areEqual(["prop2", 20], entries[1], "Second element of the returned entry array should be ['prop2', 20]"); + + var a2 = {prop3 : 30}; + a2[2] = 40; + a2["prop4"] = 50; + Object.defineProperty(a2, "prop5", { value: 60, enumerable: true}); + Object.defineProperty(a2, "prop6", { value: 70, enumerable: false}); + Object.defineProperty(a2, 'prop7', { enumerable: true, get: function () { return 80;}}); + var sym = Symbol('prop8'); + a2[sym] = 90; + + values = Object.values(a2); + assert.areEqual(5, values.length, "values method returns an array of 5 elements, symbol and non-enumerable should be excluded"); + assert.areEqual([40,30,50,60,80], values, "values method returns an array and matches correctly"); + + entries = Object.entries(a2); + assert.areEqual(5, entries.length, "entries method returns an array of 5 elements, symbol and non-enumerable should be excluded"); + assert.areEqual("2,40,prop3,30,prop4,50,prop5,60,prop7,80", entries.toString(), "entries method returns an array and matches correctly"); + } + }, + { + name: "Object.values/entries with proxy", + body: function () { + var obj1 = {prop1:10}; + var proxy1 = new Proxy(obj1, { }); + var values = Object.values(proxy1); + assert.areEqual(1, values.length, "values - Proxy object on an object with one property returns an array of 1 element"); + assert.areEqual(10, values[0], "values - Proxy object on an object with one property returns correct element"); + + var entries = Object.entries(proxy1); + assert.areEqual(1, entries.length, "entries - Proxy object on an object with one property returns an array of 1 element"); + assert.areEqual(["prop1", 10], entries[0], "entries - Proxy object on an object with one property returns correct element"); + + var obj2 = {}; + Object.defineProperty(obj2, "prop2", { value: 20, enumerable: true }); + Object.defineProperty(obj2, "prop3", { get: function () { return 30; }, enumerable: true }); + var proxy2 = new Proxy(obj2, { + getOwnPropertyDescriptor: function (target, propert) { + return Reflect.getOwnPropertyDescriptor(target, propert); + } + }); + + values = Object.values(proxy2); + assert.areEqual(2, values.length, "values - exhibiting a Proxy trapping getOwnPropertyDescriptor returns an aray to 2 elements"); + assert.areEqual(20, values[0], "values - a Proxy trapping getOwnPropertyDescriptor matching the first element"); + assert.areEqual(30, values[1], "values - a Proxy trapping getOwnPropertyDescriptor matching the second element"); + + entries = Object.entries(proxy2); + assert.areEqual(2, entries.length, "values - exhibiting a Proxy trapping getOwnPropertyDescriptor returns an aray to 2 elements"); + assert.areEqual(["prop2", 20], entries[0], "entries - a Proxy trapping getOwnPropertyDescriptor matching the first element"); + assert.areEqual(["prop3", 30], entries[1], "entries - a Proxy trapping getOwnPropertyDescriptor matching the second element"); + + var obj3 = {}; + var count = 0; + var proxy3 = new Proxy(obj3, { + get: function (target, property, receiver) { + return count++ * 5; + }, + getOwnPropertyDescriptor: function (target, property) { + return {configurable: true, enumerable: true}; + }, + + ownKeys: function (target) { + return ["prop0", "prop1", Symbol("prop2"), Symbol("prop5")]; + } + }); + + values = Object.values(proxy3); + assert.areEqual(2, values.length, "values - exhibiting a Proxy with get and ownKeys traps - returns 2 elements"); + assert.areEqual(0, values[0], "values - exhibiting a Proxy with get and ownKeys traps - matching first element"); + assert.areEqual(5, values[1], "values - exhibiting a Proxy with get and ownKeys traps - matching second element"); + + entries = Object.entries(proxy3); + assert.areEqual(2, entries.length, "entries - exhibiting a Proxy with get and ownKeys trap - returns 2 elements"); + assert.areEqual(["prop0", 10], entries[0], "entries - exhibiting a Proxy with get and ownKeys trap - matching first element"); + assert.areEqual(["prop1", 15], entries[1], "entries - exhibiting a Proxy with get and ownKeys trap - matching second element"); + + } + }, + { + name: "Object.values - deleting or making non-enumerable during enumeration", + body: function () { + var aDeletesB = { + get a() { + delete this.b; + return 1; + }, + b: 2 + }; + + var values = Object.values(aDeletesB); + assert.areEqual(1, values.length, "deleting next key during enumeration is excluded in the result"); + assert.areEqual(1, values[0], "deleting next key - first element is 1"); + + var aRemovesB = { + get a() { + Object.defineProperty(this, 'b', { + enumerable: false + }); + return 1; + }, + b: 2 + }; + + values = Object.values(aRemovesB); + assert.areEqual(1, values.length, "making the next key non-enumerable is excluded in the result"); + assert.areEqual(1, values[0], "making next nonenumerable - first element is 1"); + } + }, + { + name: "Object.entries - deleting or making non-enumerable during enumeration", + body: function () { + var aDeletesB = { + get a() { + delete this.b; + return 1; + }, + b: 2 + }; + + var entries = Object.entries(aDeletesB); + assert.areEqual(1, entries.length, "deleting next key during enumeration is excluded in the result"); + assert.areEqual(['a', 1], entries[0], "deleting next key - first element is 1"); + + var aRemovesB = { + get a() { + Object.defineProperty(this, 'b', { + enumerable: false + }); + return 1; + }, + b: 2 + }; + + entries = Object.entries(aRemovesB); + assert.areEqual(1, entries.length, "making the next key non-enumerable is excluded in the result"); + assert.areEqual(['a', 1], entries[0], "making next nonenumerable - first element is 1"); + } + }, + { + name: "Object.values - making non-enumerable to enumerable during enumeration", + body: function () { + var aAddsB = {}; + Object.defineProperty(aAddsB, "a", { get: function () { Object.defineProperty(this, 'b', { enumerable: true }); return 1; }, enumerable: true }); + Object.defineProperty(aAddsB, "b", { value: 2, configurable:true, enumerable: false}); + + var values = Object.values(aAddsB); + assert.areEqual(2, values.length, "making the second non-enumerable key to enumerable is included in the result"); + assert.areEqual(1, values[0], "non-enumerable to enumerable - first element is 1"); + assert.areEqual(2, values[1], "non-enumerable to enumerable - second element is 2"); + } + }, + { + name: "Object.entries - making non-enumerable to enumerable during enumeration", + body: function () { + var aAddsB = {}; + Object.defineProperty(aAddsB, "a", { get: function () { Object.defineProperty(this, 'b', { enumerable:true }); return 1; }, enumerable: true }); + Object.defineProperty(aAddsB, "b", { value: 2, configurable:true, enumerable: false}); + + var entries = Object.entries(aAddsB); + assert.areEqual(2, entries.length, "making the second non-enumerable key to enumerable is included in the result"); + assert.areEqual(['a', 1], entries[0], "non-enumerable to enumerable - first element is 1"); + assert.areEqual(['b', 2], entries[1], "non-enumerable to enumerable - second element is 2"); + } + } +]; + +testRunner.runTests(tests, { verbose: WScript.Arguments[0] != "summary" });