From d5a19d734623d0ebcb44bf4424c8a59f94a2bab8 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Wed, 12 Jun 2019 10:00:00 +0200 Subject: [PATCH] Wrap toString() --- index.d.ts | 2 ++ index.js | 20 ++++++++++++++ readme.md | 5 ++++ test.js | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 101 insertions(+), 3 deletions(-) diff --git a/index.d.ts b/index.d.ts index 8565d88..c1b749e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -14,6 +14,8 @@ Modifies the `to` function to mimic the `from` function. Returns the `to` functi `name`, `displayName`, and any other properties of `from` are copied. The `length` property is not copied. Properties present in `to` but not in `from` are deleted. Prototype, class, and inherited properties are copied. +`to.toString()` will return the same as `from.toString()` but prepended with a `Wrapped with to()` comment. + @param to - Mimicking function. @param from - Function to mimic. @returns The modified `to` function. diff --git a/index.js b/index.js index a7d9c41..b2f7cb8 100644 --- a/index.js +++ b/index.js @@ -40,6 +40,22 @@ const changePrototype = (to, from) => { Object.setPrototypeOf(to, fromPrototype); }; +const wrappedToString = (withName, fromBody) => `/* Wrapped ${withName}*/\n${fromBody}`; + +const toStringDescriptor = Object.getOwnPropertyDescriptor(Function.prototype, 'toString'); +const toStringName = Object.getOwnPropertyDescriptor(Function.prototype.toString, 'name'); + +// We call `from.toString()` early (not lazily) to ensure `from` can be garbage collected. +// We use `bind()` instead of a closure for the same reason. +// Calling `from.toString()` early also allows caching it in case `to.toString()` is called several times. +const changeToString = (to, from, name) => { + const withName = name === '' ? '' : `with ${name.trim()}() `; + const newToString = wrappedToString.bind(null, withName, from.toString()); + // Ensure `to.toString.toString` is non-enumerable and has the same `same` + Object.defineProperty(newToString, 'name', toStringName); + Object.defineProperty(to, 'toString', {...toStringDescriptor, value: newToString}); +}; + // If `to` has properties that `from` does not have, remove them const removeProperty = (to, from, property, ignoreNonConfigurable) => { if (hasOwnProperty.call(from, property)) { @@ -56,6 +72,8 @@ const removeProperty = (to, from, property, ignoreNonConfigurable) => { }; const mimicFn = (to, from, {ignoreNonConfigurable = false} = {}) => { + const {name} = to; + for (const property of Reflect.ownKeys(from)) { copyProperty(to, from, property, ignoreNonConfigurable); } @@ -66,6 +84,8 @@ const mimicFn = (to, from, {ignoreNonConfigurable = false} = {}) => { removeProperty(to, from, property, ignoreNonConfigurable); } + changeToString(to, from, name); + return to; }; diff --git a/readme.md b/readme.md index 0ff28ba..3e44701 100644 --- a/readme.md +++ b/readme.md @@ -34,6 +34,9 @@ console.log(wrapper.name); console.log(wrapper.unicorn); //=> '🦄' + +console.log(String(wrapper)); +//=> '/* Wrapped with wrapper() */\nfunction foo() {}' ``` @@ -45,6 +48,8 @@ Modifies the `to` function to mimic the `from` function. Returns the `to` functi `name`, `displayName`, and any other properties of `from` are copied. The `length` property is not copied. Properties present in `to` but not in `from` are deleted. Prototype, class, and inherited properties are copied. +`to.toString()` will return the same as `from.toString()` but prepended with a `Wrapped with to()` comment. + #### to Type: `Function` diff --git a/test.js b/test.js index 51b72fd..d3349a2 100644 --- a/test.js +++ b/test.js @@ -55,10 +55,11 @@ test('should keep descriptors', t => { const wrapper = function () {}; mimicFn(wrapper, foo); - const {length: fooLength, ...fooProperties} = Object.getOwnPropertyDescriptors(foo); - const {length: wrapperLength, ...wrapperProperties} = Object.getOwnPropertyDescriptors(wrapper); + const {length: fooLength, toString: fooToString, ...fooProperties} = Object.getOwnPropertyDescriptors(foo); + const {length: wrapperLength, toString: wrapperToString, ...wrapperProperties} = Object.getOwnPropertyDescriptors(wrapper); t.deepEqual(fooProperties, wrapperProperties); - t.notDeepEqual(fooLength, wrapperLength); + t.not(fooLength, wrapperLength); + t.not(fooToString, wrapperToString); }); test('should copy inherited properties', t => { @@ -112,6 +113,76 @@ test('should allow classes to be copied', t => { t.not(wrapperClass.prototype, fooClass.prototype); }); +test('should patch toString()', t => { + const wrapper = function () {}; + mimicFn(wrapper, foo); + + t.is(wrapper.toString(), `/* Wrapped with wrapper() */\n${foo.toString()}`); +}); + +test('should patch toString() with arrow functions', t => { + const wrapper = function () {}; + const arrowFn = value => value; + mimicFn(wrapper, arrowFn); + + t.is(wrapper.toString(), `/* Wrapped with wrapper() */\n${arrowFn.toString()}`); +}); + +test('should patch toString() with bound functions', t => { + const wrapper = function () {}; + const boundFn = (() => {}).bind(); + mimicFn(wrapper, boundFn); + + t.is(wrapper.toString(), `/* Wrapped with wrapper() */\n${boundFn.toString()}`); +}); + +test('should patch toString() with new Function()', t => { + const wrapper = function () {}; + // eslint-disable-next-line no-new-func + const newFn = new Function(''); + mimicFn(wrapper, newFn); + + t.is(wrapper.toString(), `/* Wrapped with wrapper() */\n${newFn.toString()}`); +}); + +test('should patch toString() several times', t => { + const wrapper = function () {}; + const wrapperTwo = function () {}; + mimicFn(wrapper, foo); + mimicFn(wrapperTwo, wrapper); + + t.is(wrapperTwo.toString(), `/* Wrapped with wrapperTwo() */\n/* Wrapped with wrapper() */\n${foo.toString()}`); +}); + +test('should keep toString() non-enumerable', t => { + const wrapper = function () {}; + mimicFn(wrapper, foo); + + const {enumerable} = Object.getOwnPropertyDescriptor(wrapper, 'toString'); + t.false(enumerable); +}); + +test('should print original function with Function.prototype.toString.call()', t => { + const wrapper = function () {}; + mimicFn(wrapper, foo); + + t.is(Function.prototype.toString.call(wrapper), 'function () {}'); +}); + +test('should work with String()', t => { + const wrapper = function () {}; + mimicFn(wrapper, foo); + + t.is(String(wrapper), `/* Wrapped with wrapper() */\n${foo.toString()}`); +}); + +test('should not modify toString.name', t => { + const wrapper = function () {}; + mimicFn(wrapper, foo); + + t.is(wrapper.toString.name, 'toString'); +}); + // eslint-disable-next-line max-params const configurableTest = (t, shouldThrow, ignoreNonConfigurable, toDescriptor, fromDescriptor) => { const wrapper = function () {};