Skip to content

Commit

Permalink
Wrap .toString() (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky authored and sindresorhus committed Jun 13, 2019
1 parent 1b1d2e5 commit 39f96c4
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 3 deletions.
2 changes: 2 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. 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.
Expand Down
19 changes: 19 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,31 @@ 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});
};

const mimicFn = (to, from, {ignoreNonConfigurable = false} = {}) => {
const {name} = to;

for (const property of Reflect.ownKeys(from)) {
copyProperty(to, from, property, ignoreNonConfigurable);
}

changePrototype(to, from);
changeToString(to, from, name);

return to;
};
Expand Down
5 changes: 5 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ console.log(wrapper.name);

console.log(wrapper.unicorn);
//=> '🦄'

console.log(String(wrapper));
//=> '/* Wrapped with wrapper() */\nfunction foo() {}'
```


Expand All @@ -48,6 +51,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. 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`
Expand Down
86 changes: 83 additions & 3 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,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 => {
Expand Down Expand Up @@ -92,6 +93,85 @@ 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');
});

test('should work when toString() was patched by original function', t => {
const wrapper = function () {};
const bar = function () {};
bar.toString = () => 'bar.toString()';
mimicFn(wrapper, bar);

t.is(wrapper.toString(), `/* Wrapped with wrapper() */\n${bar.toString()}`);
});

// eslint-disable-next-line max-params
const configurableTest = (t, shouldThrow, ignoreNonConfigurable, toDescriptor, fromDescriptor) => {
const wrapper = function () {};
Expand Down

0 comments on commit 39f96c4

Please sign in to comment.