From a31f49940576711646389bff32f42b57b71ef901 Mon Sep 17 00:00:00 2001 From: Xotic750 Date: Sun, 15 Nov 2015 20:44:10 +0100 Subject: [PATCH] Patch `Function#call` and `Function#apply` together, more robust than single fix. Ref: https://github.com/es-shims/es5-shim/issues/304 Changed some code back as mentioned in comments. Changed more code as per comments Changed more code as per comments. Changed some variable names to better reflect comments. Fixed missed invocation. Added some comments and code changes as discussed. [tests] Remove unneeded jshint comment. [Tests] use the preferred it/skip pattern for this strict mode test. https://github.com/es-shims/es5-shim/pull/345#discussion_r44878834 And some other cleanup Added `arguments` expectations to tests. Add tests for `Object#toString` of typed arrays and Symbols, if they exist. Added note about typed array tests. Fix `hasToStringTagRegExpBug` Removed RegExp and Array bug detection as can not test, possible Opera 9. Fixed missing `force` on `defineProperties` that caused the patch to not be applied on IE<9. Fixed `Uint8ClampedArray` tests for Opera 11 and IE10 that don't have it. Removed offending test that was moved to detection, but forgotten. Avoid all possibilities of `call` calling `call`. Do not pass `undefined` argument, we know IE<9 has unfixable bug. Final cleanup (hopeully) Port over work from `apply` fix Move code so that it is specific to the fix. Robustness, move before bind. Remove `Array#slice` tests. Add notes about `eval` and `apply` avoidance. --- es5-shim.js | 138 ++++++++++++- tests/spec/s-function.js | 406 ++++++++++++++++++++++++++++++++++++++- tests/spec/s-object.js | 68 ++++++- 3 files changed, 596 insertions(+), 16 deletions(-) diff --git a/es5-shim.js b/es5-shim.js index fc6778fd..975a3cd9 100644 --- a/es5-shim.js +++ b/es5-shim.js @@ -55,6 +55,7 @@ var array_splice = ArrayPrototype.splice; var array_push = ArrayPrototype.push; var array_unshift = ArrayPrototype.unshift; var array_concat = ArrayPrototype.concat; +var str_split = StringPrototype.split; var call = FunctionPrototype.call; var apply = FunctionPrototype.apply; var max = Math.max; @@ -175,11 +176,143 @@ var ES = { } }; +// Check failure of by-index access of string characters (IE < 9) +// and failure of `0 in boxedString` (Rhino) +var boxedString = $Object('a'); +var splitString = boxedString[0] !== 'a' || !(0 in boxedString); + // // Function // ======== // +// Tests for inconsistent or buggy `[[Class]]` strings. +/* eslint-disable no-useless-call */ +var hasToStringTagBasicBug = to_string.call() !== '[object Undefined]' || to_string.call(null) !== '[object Null]'; +/* eslint-enable no-useless-call */ +var hasToStringTagLegacyArguments = to_string.call(arguments) !== '[object Arguments]'; +var hasToStringTagInconsistency = hasToStringTagBasicBug || hasToStringTagLegacyArguments; +// Others that could be fixed: +// Older ES3 native functions like `alert` return `[object Object]`. +// Inconsistent `[[Class]]` strings for `window` or `global`. + +var hasApplyArrayLikeDeficiency = (function () { + var arrayLike = { length: 4, 0: 1, 2: 4, 3: true }; + var expectedArray = [1, undefined, 4, true]; + var actualArray; + try { + actualArray = (function () { + // `array_slice` is safe to use here, no known issue at present. + return array_slice.apply(arguments); + }.apply(null, arrayLike)); + } catch (e) { + if (to_string.call(actualArray) !== '[object Array]' || actualArray.length !== arrayLike.length) { + return true; + } + while (expectedArray.length) { + if (actualArray.pop() !== expectedArray.pop()) { + return true; + } + } + } + return false; +}()); + +var shouldPatchCallApply = hasToStringTagInconsistency || hasApplyArrayLikeDeficiency; + +if (shouldPatchCallApply) { + // To prevent recursion when `call` and `apply` are patched. Robustness. + call.call = call; + call.apply = apply; + apply.call = call; + apply.apply = apply; +} + +// This function is for use within `call` and `apply` only. +// To avoid any possibility of `call` recursion we use original `hasOwnProperty`. +var isDuckTypeArguments = hasToStringTagLegacyArguments && (function (hasOwnProperty) { + return function (value) { + if (value != null) { // Checks `null` or `undefined`. + if (typeof value === 'object' && call.call(hasOwnProperty, value, 'length')) { + var length = value.length; + // Constant. ES3 maximum array length. 2^32-1 + if (length > -1 && length % 1 === 0 && length <= 4294967295) { + return !call.call(hasOwnProperty, value, 'arguments') && call.call(hasOwnProperty, value, 'callee'); + } + } + } + return false; + }; +}(ObjectPrototype.hasOwnProperty)); + +// For use with `call` and `apply` fixes. +var toStringTag = shouldPatchCallApply && function (value) { + // Add whatever fixes for getting `[[Class]]` strings here. + if (value === null) { + return '[object Null]'; + } + if (typeof value === 'undefined') { + return '[object Undefined]'; + } + if (hasToStringTagLegacyArguments && isDuckTypeArguments(value)) { + return '[object Arguments]'; + } + // `to_string` is safe to use here, no known issue at present. + return call.call(to_string, value); +}; + +defineProperties(FunctionPrototype, { + // ES-5 15.3.4.3 + // http://es5.github.io/#x15.3.4.3 + // The apply() method calls a function with a given this value and arguments + // provided as an array (or an array-like object). + apply: function (thisArg) { + var argsArray = arguments[1]; + var type = typeof argsArray; + if (arguments.length > 1) { + // IE9 (though fix not needed) has a problem here for some reason!!! + // Pretty much any function here causes error `SCRIPT5007: Object expected`. + if (type !== 'undefined' && type !== 'object' && type !== 'function') { + throw new TypeError('Function.prototype.apply: Arguments list has wrong type'); + } + } + // If `this` is `Object#toString`, captured or modified. + if (this === to_string || this === Object.prototype.toString) { + return toStringTag(thisArg); + } + // All other applys. + if (arguments.length > 1 && type === 'object' && argsArray && argsArray.length > 0) { + // Boxed string access bug fix. + if (splitString && to_string.call(argsArray) === '[object String]') { + // `str_split` is safe to use here, no known issue at present. + argsArray = call.call(str_split, argsArray, ''); + } else { + // `array_slice` is safe to use here, no known issue at present. + argsArray = call.call(array_slice, argsArray); + } + } else { + // `argsArray` was `undefined` (not present), `== null` or not an object. + argsArray = []; + } + + return apply.call(this, thisArg, argsArray); + }, + + // ES-5 15.3.4.4 + // http://es5.github.io/#x15.3.4.4 + // The call() method calls a function with a given this value and arguments + // provided individually. + call: function (thisArg) { + // If `this` is `Object#toString`, captured or modified. + if (this === to_string || this === Object.prototype.toString) { + return toStringTag(thisArg); + } + // All other calls. + // `array_slice` is safe to use here, no known issue at present. + return apply.call(this, thisArg, call.call(array_slice, arguments, 1)); + } +}, shouldPatchCallApply); + // ES-5 15.3.4.5 // http://es5.github.com/#x15.3.4.5 @@ -375,11 +508,6 @@ defineProperties($Array, { isArray: isArray }); // http://es5.github.com/#x15.4.4.18 // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/forEach -// Check failure of by-index access of string characters (IE < 9) -// and failure of `0 in boxedString` (Rhino) -var boxedString = $Object('a'); -var splitString = boxedString[0] !== 'a' || !(0 in boxedString); - var properlyBoxesContext = function properlyBoxed(method) { // Check node 0.6.21 bug where third parameter is not boxed var properlyBoxesNonStrict = true; diff --git a/tests/spec/s-function.js b/tests/spec/s-function.js index 97237adb..69e50702 100644 --- a/tests/spec/s-function.js +++ b/tests/spec/s-function.js @@ -1,16 +1,404 @@ -/* global describe, it, expect, beforeEach */ +/* global describe, it, xit, expect, beforeEach */ +var hasStrictMode = (function () { + 'use strict'; + + return !this; +}()); +var ifHasStrictIt = hasStrictMode ? it : xit; +var global = Function('return this')(); describe('Function', function () { - 'use strict'; + describe('#call()', function () { + describe('should pass correct arguments', function () { + var testFn; + var expected; + beforeEach(function () { + testFn = function () { + return Array.prototype.slice.call(arguments); + }; + expected = [null, '1', 1, true, testFn]; + }); + + it('`this` is `undefined`', function () { + /* eslint-disable no-useless-call */ + var actual = testFn.call(undefined, null, '1', 1, true, testFn); + expect(actual).toEqual(expected); + /* eslint-enable no-useless-call */ + }); + + it('`this` is `null`', function () { + /* eslint-disable no-useless-call */ + var actual = testFn.call(null, null, '1', 1, true, testFn); + expect(actual).toEqual(expected); + /* eslint-enable no-useless-call */ + }); + + it('`this` is `\'a\'`', function () { + var actual = testFn.call('a', null, '1', 1, true, testFn); + expect(actual).toEqual(expected); + }); + + it('`this` is `1`', function () { + var actual = testFn.call(1, null, '1', 1, true, testFn); + expect(actual).toEqual(expected); + }); + + it('`this` is `true`', function () { + var actual = testFn.call(true, null, '1', 1, true, testFn); + expect(actual).toEqual(expected); + }); + + it('`this` is `testFn`', function () { + var actual = testFn.call(testFn, null, '1', 1, true, testFn); + expect(actual).toEqual(expected); + }); + + it('`this` is `date`', function () { + var actual = testFn.call(new Date(), null, '1', 1, true, testFn); + expect(actual).toEqual(expected); + }); + }); + + describe('context in strict mode', function () { + 'use strict'; + + var testFn; + beforeEach(function () { + testFn = function () { + return this; + }; + }); + + ifHasStrictIt('`this` is not defined', function () { + expect(testFn.call()).toBe(undefined); + }); + + ifHasStrictIt('`this` is `undefined`', function () { + /* eslint-disable no-useless-call */ + expect(testFn.call(undefined)).toBe(undefined); + /* eslint-enable no-useless-call */ + }); + + ifHasStrictIt('`this` is `null`', function () { + /* eslint-disable no-useless-call */ + expect(testFn.call(null)).toBe(null); + /* eslint-enable no-useless-call */ + }); + + ifHasStrictIt('`this` is `a`', function () { + expect(testFn.call('a')).toBe('a'); + }); + + ifHasStrictIt('`this` is `1`', function () { + expect(testFn.call(1)).toBe(1); + }); + + ifHasStrictIt('`this` is `true`', function () { + expect(testFn.call(true)).toBe(true); + }); + + ifHasStrictIt('`this` is `testFn`', function () { + expect(testFn.call(testFn)).toBe(testFn); + }); + + ifHasStrictIt('`this` is `date`', function () { + var subject = new Date(); + expect(testFn.call(subject)).toBe(subject); + }); + }); + + describe('context in non-strict mode', function () { + var testFn; + beforeEach(function () { + testFn = function () { + return this; + }; + }); + + it('`this` is not defined', function () { + expect(testFn.call()).toBe(global); + }); + + it('`this` is `undefined`', function () { + /* eslint-disable no-useless-call */ + expect(testFn.call(undefined)).toBe(global); + /* eslint-enable no-useless-call */ + }); + + it('`this` is `null`', function () { + /* eslint-disable no-useless-call */ + expect(testFn.call(null)).toBe(global); + /* eslint-enable no-useless-call */ + }); + + it('`this` is `\'a\'`', function () { + var result = testFn.call('a'); + expect(typeof result).toBe('object'); + expect(String(result)).toBe('a'); + }); + + it('`this` is `1`', function () { + var result = testFn.call(1); + expect(typeof result).toBe('object'); + expect(Number(result)).toBe(1); + }); + + it('`this` is `true`', function () { + var result = testFn.call(true); + expect(typeof result).toBe('object'); + expect(Boolean(result)).toBe(true); + }); + + it('`this` is `testFn`', function () { + expect(testFn.call(testFn)).toBe(testFn); + }); + + it('`this` is `date`', function () { + var subject = new Date(); + expect(testFn.call(subject)).toBe(subject); + }); + }); + }); describe('#apply()', function () { - it('works with arraylike objects', function () { - var arrayLike = { length: 4, 0: 1, 2: 4, 3: true }; - var expectedArray = [1, undefined, 4, true]; - var actualArray = (function () { - return Array.prototype.slice.apply(arguments); - }.apply(null, arrayLike)); - expect(actualArray).toEqual(expectedArray); + var expected; + var testFn; + + beforeEach(function () { + expected = []; + testFn = function () { + return Array.prototype.slice.call(arguments); + }; + }); + + describe('should not throw if argument is `undefined`, `null` or `object`', function () { + it('`argArray` is `null`', function () { + var actual = testFn.apply(undefined, null); + expect(actual).toEqual(expected, 'null'); + }); + + it('`argArray` is `undefined`', function () { + var actual = testFn.apply(undefined, undefined); + expect(actual).toEqual(expected); + }); + + it('`argArray` is `function`', function () { + var actual = testFn.apply(undefined, function () {}); + expect(actual).toEqual(expected); + }); + + it('`argArray` is `{}`', function () { + var actual = testFn.apply(undefined, {}); + expect(actual).toEqual(expected); + }); + + it('`argArray` is `date`', function () { + var actual = testFn.apply(undefined, new Date()); + expect(actual).toEqual(expected); + }); + + it('`argArray` is `regexp`', function () { + var actual = testFn.apply(undefined, /pattern/); + expect(actual).toEqual(expected); + }); + }); + + describe('should throw if argument is any other primitive', function () { + var testFn; + beforeEach(function () { + testFn = function () {}; + }); + + it('argument is `1`', function () { + expect(function () { + testFn.apply(undefined, 1); + }).toThrow(); + }); + + it('argument is `true`', function () { + expect(function () { + testFn.apply(undefined, true); + }).toThrow(); + }); + + it('argument is `\'123\'`', function () { + expect(function () { + testFn.apply(undefined, '123'); + }).toThrow(); + }); + }); + + describe('should pass correct arguments', function () { + var testFn; + var expected; + var args; + beforeEach(function () { + testFn = function () { + return Array.prototype.slice.call(arguments); + }; + expected = [null, '1', 1, true, testFn]; + args = [null, '1', 1, true, testFn]; + }); + + it('`this` is `undefined`', function () { + /* eslint-disable no-useless-call */ + var actual = testFn.apply(undefined, args); + expect(actual).toEqual(expected); + /* eslint-enable no-useless-call */ + }); + + it('`this` is `null`', function () { + /* eslint-disable no-useless-call */ + var actual = testFn.apply(null, args); + expect(actual).toEqual(expected); + /* eslint-enable no-useless-call */ + }); + + it('`this` is `\'a\'`', function () { + var actual = testFn.apply('a', args); + expect(actual).toEqual(expected); + }); + + it('`this` is `1`', function () { + var actual = testFn.apply(1, args); + expect(actual).toEqual(expected); + }); + + it('`this` is `true`', function () { + var actual = testFn.apply(true, args); + expect(actual).toEqual(expected); + }); + + it('`this` is `testFn`', function () { + var actual = testFn.apply(testFn, args); + expect(actual).toEqual(expected); + }); + + it('`this` is `date`', function () { + var actual = testFn.apply(new Date(), args); + expect(actual).toEqual(expected); + }); + }); + + describe('context in strict mode', function () { + 'use strict'; + + var testFn; + beforeEach(function () { + testFn = function () { + return this; + }; + }); + + ifHasStrictIt('`this` is not defined', function () { + expect(testFn.apply()).toBe(undefined); + }); + + ifHasStrictIt('`this` is `undefined`', function () { + /* eslint-disable no-useless-call */ + expect(testFn.apply(undefined)).toBe(undefined); + /* eslint-enable no-useless-call */ + }); + + ifHasStrictIt('`this` is `null`', function () { + /* eslint-disable no-useless-call */ + expect(testFn.apply(null)).toBe(null); + /* eslint-enable no-useless-call */ + }); + + ifHasStrictIt('`this` is `a`', function () { + expect(testFn.apply('a')).toBe('a'); + }); + + ifHasStrictIt('`this` is `1`', function () { + expect(testFn.apply(1)).toBe(1); + }); + + ifHasStrictIt('`this` is `true`', function () { + expect(testFn.apply(true)).toBe(true); + }); + + ifHasStrictIt('`this` is `testFn`', function () { + expect(testFn.apply(testFn)).toBe(testFn); + }); + + ifHasStrictIt('`this` is `date`', function () { + var subject = new Date(); + expect(testFn.apply(subject)).toBe(subject); + }); + }); + + describe('context in non-strict mode', function () { + var testFn; + beforeEach(function () { + testFn = function () { + return this; + }; + }); + + it('`this` is not defined', function () { + expect(testFn.apply()).toBe(global); + }); + + it('`this` is `undefined`', function () { + /* eslint-disable no-useless-call */ + expect(testFn.apply(undefined)).toBe(global); + /* eslint-enable no-useless-call */ + }); + + it('`this` is `null`', function () { + /* eslint-disable no-useless-call */ + expect(testFn.apply(null)).toBe(global); + /* eslint-enable no-useless-call */ + }); + + it('`this` is `\'a\'`', function () { + var result = testFn.apply('a'); + expect(typeof result).toBe('object'); + expect(String(result)).toBe('a'); + }); + + it('`this` is `1`', function () { + var result = testFn.apply(1); + expect(typeof result).toBe('object'); + expect(Number(result)).toBe(1); + }); + + it('`this` is `true`', function () { + var result = testFn.apply(true); + expect(typeof result).toBe('object'); + expect(Boolean(result)).toBe(true); + }); + + it('`this` is `testFn`', function () { + expect(testFn.apply(testFn)).toBe(testFn); + }); + + it('`this` is `date`', function () { + var subject = new Date(); + expect(testFn.apply(subject)).toBe(subject); + }); + }); + + describe('works with arraylike objects', function () { + it('object like sparse array', function () { + var arrayLike = { length: 4, 0: 1, 2: 4, 3: true }; + var expectedArray = [1, undefined, 4, true]; + var actualArray = (function () { + return Array.prototype.slice.apply(arguments); + }.apply(null, arrayLike)); + expect(actualArray).toEqual(expectedArray); + }); + + it('arguments object', function () { + var args = function () { + return arguments; + }; + var testFn = function () { + return Array.prototype.slice.call(arguments); + }; + expect(testFn.apply(undefined, args(1, 2, 3, 4))).toEqual([1, 2, 3, 4]); + }); }); }); diff --git a/tests/spec/s-object.js b/tests/spec/s-object.js index 3065c669..f7d9a5ed 100644 --- a/tests/spec/s-object.js +++ b/tests/spec/s-object.js @@ -1,4 +1,6 @@ -/* global describe, it, xit, expect, beforeEach, jasmine, window */ +/* global describe, it, xit, expect, beforeEach, jasmine, window, + ArrayBuffer, Float32Array, Float64Array, Int8Array, Int16Array, + Int32Array, Uint8Array, Uint8ClampedArray, Uint16Array, Uint32Array */ var supportsDescriptors = Object.defineProperty && (function () { try { @@ -32,7 +34,14 @@ var canFreeze = typeof Object.freeze === 'function' && (function () { return obj.a !== 3; }()); var ifCanFreezeIt = canFreeze ? it : xit; - +var toStr = Object.prototype.toString; +var noop = function () {}; +var hasIteratorTag = typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol'; +var ifHasIteratorTag = hasIteratorTag ? it : xit; +var hasArrayBuffer = typeof ArrayBuffer === 'function'; +var ifHasArrayBuffer = hasArrayBuffer ? it : xit; +var hasUint8ClampedArray = typeof Uint8ClampedArray === 'function'; +var ifHasUint8ClampedArray = hasUint8ClampedArray ? it : xit; describe('Object', function () { 'use strict'; @@ -365,4 +374,59 @@ describe('Object', function () { expect(obj instanceof Object).toBe(false); }); }); + + describe('#toString', function () { + it('basic', function () { + expect(toStr.call()).toBe('[object Undefined]'); + /* eslint-disable no-useless-call */ + expect(toStr.call(undefined)).toBe('[object Undefined]'); + expect(toStr.call(null)).toBe('[object Null]'); + /* eslint-enable no-useless-call */ + expect(toStr.call(1)).toBe('[object Number]'); + expect(toStr.call(true)).toBe('[object Boolean]'); + expect(toStr.call('x')).toBe('[object String]'); + expect(toStr.call([1, 2, 3])).toBe('[object Array]'); + expect(toStr.call(arguments)).toBe('[object Arguments]'); + expect(toStr.call({})).toBe('[object Object]'); + expect(toStr.call(noop)).toBe('[object Function]'); + expect(toStr.call(new RegExp('c'))).toBe('[object RegExp]'); + expect(toStr.call(new Date())).toBe('[object Date]'); + expect(toStr.call(new Error('x'))).toBe('[object Error]'); + }); + ifHasArrayBuffer('Typed Arrays', function () { + var buffer = new ArrayBuffer(8); + expect(toStr.call(buffer)).toBe('[object ArrayBuffer]'); + expect(toStr.call(new Float32Array(buffer))).toBe('[object Float32Array]'); + expect(toStr.call(new Float64Array(buffer))).toBe('[object Float64Array]'); + expect(toStr.call(new Int8Array(buffer))).toBe('[object Int8Array]'); + expect(toStr.call(new Int16Array(buffer))).toBe('[object Int16Array]'); + expect(toStr.call(new Int32Array(buffer))).toBe('[object Int32Array]'); + expect(toStr.call(new Uint8Array(buffer))).toBe('[object Uint8Array]'); + expect(toStr.call(new Uint16Array(buffer))).toBe('[object Uint16Array]'); + expect(toStr.call(new Uint32Array(buffer))).toBe('[object Uint32Array]'); + }); + ifHasUint8ClampedArray('Uint8ClampedArray', function () { + var buffer = new ArrayBuffer(8); + expect(toStr.call(new Uint32Array(buffer))).toBe('[object Uint32Array]'); + }); + ifHasIteratorTag('Symbol.iterator', function () { + expect(toStr.call(Symbol.iterator)).toBe('[object Symbol]'); + }); + // https://github.com/es-shims/es5-shim/pull/345#discussion_r44878834 + it('prototypes', function () { + expect(toStr.call(Object.prototype)).toBe('[object Object]'); + expect(toStr.call(Array.prototype)).toBe('[object Array]'); + expect(toStr.call(Boolean.prototype)).toBe('[object Boolean]'); + expect(toStr.call(Function.prototype)).toBe('[object Function]'); + }); + // In ES6, many prototype objects stop being instances of themselves, + // and instead would return '[object Object]'. + xit('prototypes', function () { + expect(toStr.call(Number.prototype)).toBe('[object Number]'); + expect(toStr.call(String.prototype)).toBe('[object String]'); + expect(toStr.call(Error.prototype)).toBe('[object Error]'); + expect(toStr.call(Date.prototype)).toBe('[object Date]'); + expect(toStr.call(RegExp.prototype)).toBe('[object RegExp]'); + }); + }); });