Skip to content

Commit

Permalink
Wrap toString()
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed Jun 13, 2019
1 parent 64ebaf2 commit d5a19d7
Show file tree
Hide file tree
Showing 4 changed files with 101 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. 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.
Expand Down
20 changes: 20 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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);
}
Expand All @@ -66,6 +84,8 @@ const mimicFn = (to, from, {ignoreNonConfigurable = false} = {}) => {
removeProperty(to, from, property, ignoreNonConfigurable);
}

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 @@ -34,6 +34,9 @@ console.log(wrapper.name);

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

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


Expand All @@ -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`
Expand Down
77 changes: 74 additions & 3 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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 () {};
Expand Down

0 comments on commit d5a19d7

Please sign in to comment.