Skip to content

Commit fdb853d

Browse files
Fix methods containing nested functions and arrows (#231)
* fix: detect outer functions before serializing method bodies * test: preserve commented async function signatures * fix: preserve class expressions during serialization
1 parent f7dfeae commit fdb853d

2 files changed

Lines changed: 93 additions & 4 deletions

File tree

‎index.js‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ var PLACE_HOLDER_REGEXP = new RegExp('(\\\\)?"@__(F|R|D|M|S|A|U|I|B|L)-' + UID +
1515
// call could start past a `[native code]` match and wrongly report a native
1616
// function as safe to serialize.
1717
var IS_NATIVE_CODE_REGEXP = /\{\s*\[native code\]\s*\}/;
18-
var IS_PURE_FUNCTION = /function.*?\(/;
19-
var IS_ARROW_FUNCTION = /.*?=>.*?/;
18+
// Classify the outer function, not a declaration or arrow inside its body.
19+
var IS_PURE_FUNCTION = /^(?:async(?:\s|\/\*[\s\S]*?\*\/|\/\/[^\n]*\n)+)?function\b/;
20+
// A concise method named `class` is followed by `(`, unlike a class expression.
21+
var IS_CLASS = /^class\b(?:\s|\/\*[\s\S]*?\*\/|\/\/[^\n]*\n)*(?:\{|[^\s(/])/;
22+
var IS_ARROW_FUNCTION = /^(?:async(?:\s|\/\*[\s\S]*?\*\/|\/\/[^\n]*\n)+)?(?:\([\s\S]*?\)|[^\s()]+)\s*=>/;
2023
var UNSAFE_CHARS_REGEXP = /[<>\/\u2028\u2029]/g;
2124
// Matches a script end tag (case-insensitive) for XSS protection: either a
2225
// literal `</script>`, or a bare `</script` followed by one of the other
@@ -228,8 +231,8 @@ module.exports = function serialize(obj, options) {
228231
serializedFn = escapeFunctionBody(serializedFn);
229232
}
230233

231-
// pure functions, example: {key: function() {}}
232-
if(IS_PURE_FUNCTION.test(serializedFn)) {
234+
// Function and class expressions can be emitted without method conversion.
235+
if(IS_PURE_FUNCTION.test(serializedFn) || IS_CLASS.test(serializedFn)) {
233236
return serializedFn;
234237
}
235238

‎test/unit/method-bodies.js‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
const { describe, it } = require('node:test');
2+
const { strictEqual } = require('node:assert');
3+
const serialize = require('../../');
4+
5+
describe('method bodies containing other functions', function () {
6+
it('round trips a method containing an arrow with destructured parameters', function () {
7+
const input = {
8+
method() {
9+
const nested = ({ value }) => value;
10+
return nested({ value: 42 });
11+
}
12+
};
13+
const output = eval('(' + serialize(input) + ')');
14+
strictEqual(output.method(), 42);
15+
});
16+
17+
it('round trips a method containing a function declaration', function () {
18+
const input = {
19+
method() {
20+
function nested() { return 42; }
21+
return nested();
22+
}
23+
};
24+
const output = eval('(' + serialize(input) + ')');
25+
strictEqual(output.method(), 42);
26+
});
27+
28+
it('preserves async methods containing arrows', async function () {
29+
const input = {
30+
async method() {
31+
const nested = () => 42;
32+
return nested();
33+
}
34+
};
35+
const output = eval('(' + serialize(input) + ')');
36+
strictEqual(await output.method(), 42);
37+
});
38+
39+
it('preserves generator methods containing arrows', function () {
40+
const input = {
41+
*method() {
42+
const nested = () => 42;
43+
yield nested();
44+
}
45+
};
46+
const output = eval('(' + serialize(input) + ')');
47+
strictEqual(output.method().next().value, 42);
48+
});
49+
});
50+
51+
52+
describe('outer function signatures', function () {
53+
it('round trips a class with a nested arrow', function () {
54+
class A { method() { return value => value; } }
55+
const output = eval('(' + serialize({ A }) + ')');
56+
strictEqual(new output.A().method()(42), 42);
57+
strictEqual(output.A.toString(), A.toString());
58+
});
59+
60+
it('round trips a class without nested functions', function () {
61+
class A { method() { return 42; } }
62+
const output = eval('(' + serialize({ A }) + ')');
63+
strictEqual(new output.A().method(), 42);
64+
strictEqual(output.A.toString(), A.toString());
65+
});
66+
67+
it('still converts concise methods named class', function () {
68+
const input = { class () { return 42; } };
69+
const output = eval('(' + serialize(input) + ')');
70+
strictEqual(output.class(), 42);
71+
});
72+
73+
it('preserves async signatures with comments instead of spaces', async function () {
74+
for (const fn of [async/**/function () { return 42; }, async/**/() => 42]) {
75+
const output = eval('(' + serialize(fn) + ')');
76+
strictEqual(await output(), 42);
77+
strictEqual(output.constructor.name, 'AsyncFunction');
78+
}
79+
});
80+
81+
it('preserves arrows whose default arguments contain functions', function () {
82+
const fn = (callback = function () { return 42; }) => callback();
83+
const output = eval('(' + serialize(fn) + ')');
84+
strictEqual(output(), 42);
85+
});
86+
});

0 commit comments

Comments
 (0)