diff --git a/JSTests/stress/iterator-helpers-close-for-invalid-argument.js b/JSTests/stress/iterator-helpers-close-for-invalid-argument.js new file mode 100644 index 0000000000000..9f5b0d0ca8605 --- /dev/null +++ b/JSTests/stress/iterator-helpers-close-for-invalid-argument.js @@ -0,0 +1,296 @@ +function shouldThrow(errorType, func) { + let error; + try { + func(); + } catch (e) { + error = e; + } + if (!(error instanceof errorType)) { + print(error.message); + throw new Error(`Expected ${errorType.name}! got ${error.name}`); + } +} + +function shouldBe(a, b) { + if (a !== b) + throw new Error(`Expected ${b} but got ${a}`); +} + +{ + // Iterator.prototype.map + let closed = false; + let closable = { + __proto__: Iterator.prototype, + get next() { + throw new Error('next should not be read'); + }, + return() { + closed = true; + return {}; + }, + }; + shouldThrow(TypeError, function() { + closable.map(); + }); + shouldBe(closed, true); + + closed = false; + shouldThrow(TypeError, function() { + closable.map({}); + }); + shouldBe(closed, true); +} + +{ + // Iterator.prototype.filter + let closed = false; + let closable = { + __proto__: Iterator.prototype, + get next() { + throw new Error('next should not be read'); + }, + return() { + closed = true; + return {}; + }, + }; + shouldThrow(TypeError, function() { + closable.filter(); + }); + shouldBe(closed, true); + + closed = false; + shouldThrow(TypeError, function() { + closable.filter({}); + }); + shouldBe(closed, true); +} + +{ + // Iterator.prototype.take + let closed = false; + let closable = { + __proto__: Iterator.prototype, + get next() { + throw new Error('next should not be read'); + }, + return() { + closed = true; + return {}; + }, + }; + + shouldThrow(RangeError, function() { + closable.take(); + }); + shouldBe(closed, true); + + closed = false; + shouldThrow(RangeError, function() { + closable.take(NaN); + }); + shouldBe(closed, true); + + closed = false; + shouldThrow(RangeError, function() { + closable.take(-1); + }); + shouldBe(closed, true); + + closed = false; + function OurError() {} + shouldThrow(OurError, function() { + closable.take({ get valueOf() { throw new OurError(); }}); + }); + shouldBe(closed, true); +} + +{ + // Iterator.prototype.drop + let closed = false; + let closable = { + __proto__: Iterator.prototype, + get next() { + throw new Error('next should not be read'); + }, + return() { + closed = true; + return {}; + }, + }; + + shouldThrow(RangeError, function() { + closable.drop(); + }); + shouldBe(closed, true); + + closed = false; + shouldThrow(RangeError, function() { + closable.drop(NaN); + }); + shouldBe(closed, true); + + closed = false; + shouldThrow(RangeError, function() { + closable.drop(-1); + }); + shouldBe(closed, true); + + closed = false; + function OurError() {} + shouldThrow(OurError, function() { + closable.drop({ get valueOf() { throw new OurError(); }}); + }); + shouldBe(closed, true); +} + +{ + // Iterator.prototype.flatMap + let closed = false; + let closable = { + __proto__: Iterator.prototype, + get next() { + throw new Error('next should not be read'); + }, + return() { + closed = true; + return {}; + }, + }; + shouldThrow(TypeError, function() { + closable.flatMap(); + }); + shouldBe(closed, true); + + closed = false; + shouldThrow(TypeError, function() { + closable.flatMap({}); + }); + shouldBe(closed, true); +} + +{ + // Iterator.prototype.some + let closed = false; + let closable = { + __proto__: Iterator.prototype, + get next() { + throw new Error('next should not be read'); + }, + return() { + closed = true; + return {}; + }, + }; + shouldThrow(TypeError, function() { + closable.some(); + }); + shouldBe(closed, true); + + closed = false; + shouldThrow(TypeError, function() { + closable.some({}); + }); + shouldBe(closed, true); +} + +{ + // Iterator.prototype.every + let closed = false; + let closable = { + __proto__: Iterator.prototype, + get next() { + throw new Error('next should not be read'); + }, + return() { + closed = true; + return {}; + }, + }; + shouldThrow(TypeError, function() { + closable.every(); + }); + shouldBe(closed, true); + + closed = false; + shouldThrow(TypeError, function() { + closable.every({}); + }); + shouldBe(closed, true); +} + +{ + // Iterator.prototype.find + let closed = false; + let closable = { + __proto__: Iterator.prototype, + get next() { + throw new Error('next should not be read'); + }, + return() { + closed = true; + return {}; + }, + }; + shouldThrow(TypeError, function() { + closable.find(); + }); + shouldBe(closed, true); + + closed = false; + shouldThrow(TypeError, function() { + closable.find({}); + }); + shouldBe(closed, true); +} + +{ + // Iterator.prototype.reduce + let closed = false; + let closable = { + __proto__: Iterator.prototype, + get next() { + throw new Error('next should not be read'); + }, + return() { + closed = true; + return {}; + }, + }; + shouldThrow(TypeError, function() { + closable.reduce(); + }); + shouldBe(closed, true); + + closed = false; + shouldThrow(TypeError, function() { + closable.reduce({}); + }); + shouldBe(closed, true); +} + +{ + // Iterator.prototype.forEach + let closed = false; + let closable = { + __proto__: Iterator.prototype, + get next() { + throw new Error('next should not be read'); + }, + return() { + closed = true; + return {}; + }, + }; + shouldThrow(TypeError, function() { + closable.forEach(); + }); + shouldBe(closed, true); + + closed = false; + shouldThrow(TypeError, function() { + closable.forEach({}); + }); + shouldBe(closed, true); +} + diff --git a/JSTests/test262/expectations.yaml b/JSTests/test262/expectations.yaml index cc3b310653991..9fa3050af5f96 100644 --- a/JSTests/test262/expectations.yaml +++ b/JSTests/test262/expectations.yaml @@ -25,36 +25,6 @@ test/built-ins/Iterator/concat/fresh-iterator-result.js: test/built-ins/Iterator/concat/next-method-returns-throwing-value.js: default: 'Test262Error: ' strict mode: 'Test262Error: ' -test/built-ins/Iterator/prototype/drop/argument-validation-failure-closes-underlying.js: - default: 'Test262Error: Expected SameValue(«false», «true») to be true' - strict mode: 'Test262Error: Expected SameValue(«false», «true») to be true' -test/built-ins/Iterator/prototype/every/argument-validation-failure-closes-underlying.js: - default: 'Test262Error: Expected SameValue(«false», «true») to be true' - strict mode: 'Test262Error: Expected SameValue(«false», «true») to be true' -test/built-ins/Iterator/prototype/filter/argument-validation-failure-closes-underlying.js: - default: 'Test262Error: Expected SameValue(«false», «true») to be true' - strict mode: 'Test262Error: Expected SameValue(«false», «true») to be true' -test/built-ins/Iterator/prototype/find/argument-validation-failure-closes-underlying.js: - default: 'Test262Error: Expected SameValue(«false», «true») to be true' - strict mode: 'Test262Error: Expected SameValue(«false», «true») to be true' -test/built-ins/Iterator/prototype/flatMap/argument-validation-failure-closes-underlying.js: - default: 'Test262Error: Expected SameValue(«false», «true») to be true' - strict mode: 'Test262Error: Expected SameValue(«false», «true») to be true' -test/built-ins/Iterator/prototype/forEach/argument-validation-failure-closes-underlying.js: - default: 'Test262Error: Expected SameValue(«false», «true») to be true' - strict mode: 'Test262Error: Expected SameValue(«false», «true») to be true' -test/built-ins/Iterator/prototype/map/argument-validation-failure-closes-underlying.js: - default: 'Test262Error: Expected SameValue(«false», «true») to be true' - strict mode: 'Test262Error: Expected SameValue(«false», «true») to be true' -test/built-ins/Iterator/prototype/reduce/argument-validation-failure-closes-underlying.js: - default: 'Test262Error: Expected SameValue(«false», «true») to be true' - strict mode: 'Test262Error: Expected SameValue(«false», «true») to be true' -test/built-ins/Iterator/prototype/some/argument-validation-failure-closes-underlying.js: - default: 'Test262Error: Expected SameValue(«false», «true») to be true' - strict mode: 'Test262Error: Expected SameValue(«false», «true») to be true' -test/built-ins/Iterator/prototype/take/argument-validation-failure-closes-underlying.js: - default: 'Test262Error: Expected SameValue(«false», «true») to be true' - strict mode: 'Test262Error: Expected SameValue(«false», «true») to be true' test/built-ins/Object/entries/order-after-define-property-with-function.js: default: 'Test262Error: Actual [a, name] and expected [name, a] should have the same contents. ' strict mode: 'Test262Error: Actual [a, name] and expected [name, a] should have the same contents. ' diff --git a/Source/JavaScriptCore/builtins/JSIteratorPrototype.js b/Source/JavaScriptCore/builtins/JSIteratorPrototype.js index d3bb67d5657e8..113612a26be25 100644 --- a/Source/JavaScriptCore/builtins/JSIteratorPrototype.js +++ b/Source/JavaScriptCore/builtins/JSIteratorPrototype.js @@ -32,8 +32,10 @@ function map(mapper) if (!@isObject(this)) @throwTypeError("Iterator.prototype.map requires that |this| be an Object."); - if (!@isCallable(mapper)) + if (!@isCallable(mapper)) { + @iteratorGenericClose(this); @throwTypeError("Iterator.prototype.map callback must be a function."); + } var iterated = this; var iteratedNextMethod = iterated.next; @@ -63,8 +65,10 @@ function filter(predicate) if (!@isObject(this)) @throwTypeError("Iterator.prototype.filter requires that |this| be an Object."); - if (!@isCallable(predicate)) + if (!@isCallable(predicate)) { + @iteratorGenericClose(this); @throwTypeError("Iterator.prototype.filter callback must be a function."); + } var iterated = this; var iteratedNextMethod = iterated.next; @@ -94,13 +98,18 @@ function take(limit) if (!@isObject(this)) @throwTypeError("Iterator.prototype.take requires that |this| be an Object."); - var numLimit = @toNumber(limit); - if (numLimit !== numLimit) + var numLimit; + @ifAbruptCloseIterator(this, numLimit = @toNumber(limit)); + if (numLimit !== numLimit) { + @iteratorGenericClose(this); @throwRangeError("Iterator.prototype.take argument must not be NaN."); + } var intLimit = @toIntegerOrInfinity(numLimit); - if (intLimit < 0) + if (intLimit < 0) { + @iteratorGenericClose(this); @throwRangeError("Iterator.prototype.take argument must be non-negative."); + } var iterated = this; var iteratedNextMethod = iterated.next; @@ -138,13 +147,18 @@ function drop(limit) if (!@isObject(this)) @throwTypeError("Iterator.prototype.drop requires that |this| be an Object."); - var numLimit = @toNumber(limit); - if (numLimit !== numLimit) + var numLimit; + @ifAbruptCloseIterator(this, (numLimit = @toNumber(limit))); + if (numLimit !== numLimit) { + @iteratorGenericClose(this); @throwRangeError("Iterator.prototype.drop argument must not be NaN."); + } var intLimit = @toIntegerOrInfinity(numLimit); - if (intLimit < 0) + if (intLimit < 0) { + @iteratorGenericClose(this); @throwRangeError("Iterator.prototype.drop argument must be non-negative."); + } var iterated = this; var iteratedNextMethod = iterated.next; @@ -179,8 +193,10 @@ function flatMap(mapper) if (!@isObject(this)) @throwTypeError("Iterator.prototype.flatMap requires that |this| be an Object."); - if (!@isCallable(mapper)) + if (!@isCallable(mapper)) { + @iteratorGenericClose(this); @throwTypeError("Iterator.prototype.flatMap callback must be a function."); + } var iterated = this; var iteratedNextMethod = iterated.next; @@ -212,12 +228,14 @@ function some(predicate) if (!@isObject(this)) @throwTypeError("Iterator.prototype.some requires that |this| be an Object."); - if (!@isCallable(predicate)) + if (!@isCallable(predicate)) { + @iteratorGenericClose(this); @throwTypeError("Iterator.prototype.some callback must be a function."); + } + var iterated = this; var count = 0; - var iterator = this; - var wrapper = { @@iterator: function () { return iterator; }}; + var wrapper = { @@iterator: function () { return iterated; }}; for (var item of wrapper) { if (predicate(item, count++)) return true; @@ -234,12 +252,14 @@ function every(predicate) if (!@isObject(this)) @throwTypeError("Iterator.prototype.every requires that |this| be an Object."); - if (!@isCallable(predicate)) + if (!@isCallable(predicate)) { + @iteratorGenericClose(this); @throwTypeError("Iterator.prototype.every callback must be a function."); + } + var iterated = this; var count = 0; - var iterator = this; - var wrapper = { @@iterator: function () { return iterator; }}; + var wrapper = { @@iterator: function () { return iterated; }}; for (var item of wrapper) { if (!predicate(item, count++)) return false; @@ -256,12 +276,14 @@ function find(predicate) if (!@isObject(this)) @throwTypeError("Iterator.prototype.find requires that |this| be an Object."); - if (!@isCallable(predicate)) + if (!@isCallable(predicate)) { + @iteratorGenericClose(this); @throwTypeError("Iterator.prototype.find callback must be a function."); + } + var iterated = this; var count = 0; - var iterator = this; - var wrapper = { @@iterator: function () { return iterator; }}; + var wrapper = { @@iterator: function () { return iterated; }}; for (var item of wrapper) { if (predicate(item, count++)) return item; @@ -278,12 +300,14 @@ function reduce(reducer /*, initialValue */) if (!@isObject(this)) @throwTypeError("Iterator.prototype.reduce requires that |this| be an Object."); - if (!@isCallable(reducer)) + if (!@isCallable(reducer)) { + @iteratorGenericClose(this); @throwTypeError("Iterator.prototype.reduce reducer argument must be a function."); + } + var iterated = this; var initialValue = @argument(1); - var iterated = this; var iteratedNextMethod = this.next; var accumulator; diff --git a/Source/JavaScriptCore/runtime/JSIteratorPrototype.cpp b/Source/JavaScriptCore/runtime/JSIteratorPrototype.cpp index 26a2501bbfb76..c4d1a2d5820c2 100644 --- a/Source/JavaScriptCore/runtime/JSIteratorPrototype.cpp +++ b/Source/JavaScriptCore/runtime/JSIteratorPrototype.cpp @@ -162,8 +162,11 @@ JSC_DEFINE_HOST_FUNCTION(iteratorProtoFuncForEach, (JSGlobalObject* globalObject return throwVMTypeError(globalObject, scope, "Iterator.prototype.forEach requires that |this| be an Object."_s); JSValue callbackArg = callFrame->argument(0); - if (!callbackArg.isCallable()) + if (!callbackArg.isCallable()) { + iteratorClose(globalObject, thisValue); + RETURN_IF_EXCEPTION(scope, { }); return throwVMTypeError(globalObject, scope, "Iterator.prototype.forEach requires the callback argument to be callable."_s); + } auto callData = JSC::getCallData(callbackArg); ASSERT(callData.type != CallData::Type::None);