Skip to content

Commit 246fcc8

Browse files
committed
Fix space option not being applied to function bodies (#195)
1 parent bb0048c commit 246fcc8

File tree

2 files changed

+324
-26
lines changed

2 files changed

+324
-26
lines changed

index.js

Lines changed: 260 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -138,44 +138,278 @@ module.exports = function serialize(obj, options) {
138138
return value;
139139
}
140140

141-
function serializeFunc(fn) {
141+
function serializeFunc(fn, options) {
142142
var serializedFn = fn.toString();
143143
if (IS_NATIVE_CODE_REGEXP.test(serializedFn)) {
144144
throw new TypeError('Serializing native function: ' + fn.name);
145145
}
146146

147-
// pure functions, example: {key: function() {}}
148-
if(IS_PURE_FUNCTION.test(serializedFn)) {
149-
return serializedFn;
147+
// If no space option, return original behavior
148+
if (!options || !options.space) {
149+
// pure functions, example: {key: function() {}}
150+
if(IS_PURE_FUNCTION.test(serializedFn)) {
151+
return serializedFn;
152+
}
153+
154+
// arrow functions, example: arg1 => arg1+5
155+
if(IS_ARROW_FUNCTION.test(serializedFn)) {
156+
return serializedFn;
157+
}
158+
159+
var argsStartsAt = serializedFn.indexOf('(');
160+
var def = serializedFn.substr(0, argsStartsAt)
161+
.trim()
162+
.split(' ')
163+
.filter(function(val) { return val.length > 0 });
164+
165+
var nonReservedSymbols = def.filter(function(val) {
166+
return RESERVED_SYMBOLS.indexOf(val) === -1
167+
});
168+
169+
// enhanced literal objects, example: {key() {}}
170+
if(nonReservedSymbols.length > 0) {
171+
return (def.indexOf('async') > -1 ? 'async ' : '') + 'function'
172+
+ (def.join('').indexOf('*') > -1 ? '*' : '')
173+
+ serializedFn.substr(argsStartsAt);
174+
}
175+
176+
// arrow functions
177+
return serializedFn;
178+
}
179+
180+
// Format function body with space option
181+
return formatFunctionWithSpace(serializedFn, options.space);
182+
}
183+
184+
function formatFunctionWithSpace(serializedFn, space) {
185+
// Determine indentation unit
186+
var indentUnit;
187+
if (typeof space === 'number') {
188+
indentUnit = ' '.repeat(space);
189+
} else if (typeof space === 'string') {
190+
indentUnit = space;
191+
} else {
192+
return serializedFn; // fallback to original
150193
}
151194

152-
// arrow functions, example: arg1 => arg1+5
153-
if(IS_ARROW_FUNCTION.test(serializedFn)) {
154-
return serializedFn;
195+
// Find the function body opening brace (not parameter destructuring braces)
196+
var bodyStartBraceIndex = -1;
197+
var parenDepth = 0;
198+
var braceDepth = 0;
199+
200+
for (var i = 0; i < serializedFn.length; i++) {
201+
var char = serializedFn[i];
202+
if (char === '(') {
203+
parenDepth++;
204+
} else if (char === ')') {
205+
parenDepth--;
206+
// After closing the parameter list, the next { is the function body
207+
if (parenDepth === 0) {
208+
for (var j = i + 1; j < serializedFn.length; j++) {
209+
if (serializedFn[j] === '{') {
210+
bodyStartBraceIndex = j;
211+
break;
212+
} else if (serializedFn[j] !== ' ' && serializedFn[j] !== '=' && serializedFn[j] !== '>') {
213+
// Non-space/arrow character before brace, not a function body brace
214+
break;
215+
}
216+
}
217+
break;
218+
}
219+
}
220+
}
221+
222+
var closeBraceIndex = serializedFn.lastIndexOf('}');
223+
224+
if (bodyStartBraceIndex === -1 || closeBraceIndex === -1 || bodyStartBraceIndex >= closeBraceIndex) {
225+
return serializedFn; // No function body braces found, return original
155226
}
156227

157-
var argsStartsAt = serializedFn.indexOf('(');
158-
var def = serializedFn.substr(0, argsStartsAt)
159-
.trim()
160-
.split(' ')
161-
.filter(function(val) { return val.length > 0 });
162-
163-
var nonReservedSymbols = def.filter(function(val) {
164-
return RESERVED_SYMBOLS.indexOf(val) === -1
165-
});
166-
167-
// enhanced literal objects, example: {key() {}}
168-
if(nonReservedSymbols.length > 0) {
169-
return (def.indexOf('async') > -1 ? 'async ' : '') + 'function'
170-
+ (def.join('').indexOf('*') > -1 ? '*' : '')
171-
+ serializedFn.substr(argsStartsAt);
228+
var signature = serializedFn.substring(0, bodyStartBraceIndex).trim();
229+
var body = serializedFn.substring(bodyStartBraceIndex + 1, closeBraceIndex).trim();
230+
231+
// Clean up signature: ensure proper spacing
232+
// For arrow functions, add space around =>
233+
if (signature.includes('=>')) {
234+
signature = signature.replace(/\s*=>\s*/, ' => ');
235+
}
236+
237+
// Ensure space before opening brace
238+
if (!signature.endsWith(' ')) {
239+
signature += ' ';
240+
}
241+
242+
// If body is empty, format minimally
243+
if (!body) {
244+
return signature + '{\n' + indentUnit.repeat(2) + '}';
172245
}
173246

174-
// arrow functions
175-
return serializedFn;
247+
// Format the function body with proper indentation and spacing
248+
var formattedBody = formatSimpleFunctionBody(body, indentUnit);
249+
250+
// Ensure we don't double-add closing braces
251+
var lines = formattedBody.split('\n');
252+
var lastNonEmptyIndex = lines.length - 1;
253+
while (lastNonEmptyIndex >= 0 && !lines[lastNonEmptyIndex].trim()) {
254+
lastNonEmptyIndex--;
255+
}
256+
257+
if (lastNonEmptyIndex >= 0 && lines[lastNonEmptyIndex].trim() === '}') {
258+
// Remove the last closing brace line
259+
lines.splice(lastNonEmptyIndex, 1);
260+
formattedBody = lines.join('\n');
261+
}
262+
263+
return signature + '{\n' + formattedBody + '\n' + indentUnit + '}';
264+
}
265+
266+
function formatSimpleFunctionBody(body, indentUnit) {
267+
// Enhanced function body formatter that handles nested structures
268+
var baseIndent = indentUnit.repeat(2); // Functions are already inside objects, so depth 2
269+
270+
// First, add spaces around operators and keywords, being careful about arrow functions
271+
var formatted = body
272+
// Protect arrow functions from being split
273+
.replace(/=>/g, '___ARROW___')
274+
// Clean up multiple spaces first
275+
.replace(/\s+/g, ' ')
276+
// Add spaces around operators (but not === or !==)
277+
.replace(/([^=!<>])\s*=\s*([^=])/g, '$1 = $2')
278+
.replace(/([^=])\s*===\s*([^=])/g, '$1 === $2')
279+
.replace(/([^!])\s*!==\s*([^=])/g, '$1 !== $2')
280+
.replace(/([^|])\s*\|\|\s*([^|])/g, '$1 || $2')
281+
.replace(/([^&])\s*&&\s*([^&])/g, '$1 && $2')
282+
// Add spaces around arithmetic operators
283+
.replace(/([^\s*])\s*\*\s*([^\s*])/g, '$1 * $2')
284+
.replace(/([^\s+])\s*\+\s*([^\s+])/g, '$1 + $2')
285+
.replace(/([^\s-])\s*-\s*([^\s-])/g, '$1 - $2')
286+
.replace(/([^\s/])\s*\/\s*([^\s/])/g, '$1 / $2')
287+
// Add spaces around comparison operators
288+
.replace(/([^\s>])\s*>\s*([^\s>=])/g, '$1 > $2')
289+
.replace(/([^\s<])\s*<\s*([^\s<=])/g, '$1 < $2')
290+
.replace(/\s*>=\s*(?![>])/g, ' >= ')
291+
.replace(/\s*<=\s*(?![<])/g, ' <= ')
292+
// Add spaces after commas
293+
.replace(/,(?!\s)/g, ', ')
294+
// Add space after control keywords and before braces
295+
.replace(/\b(if|for|while)\s*\(/g, '$1 (')
296+
.replace(/\)\s*\{/g, ') {')
297+
.replace(/\belse\s*\{/g, 'else {')
298+
.replace(/\breturn\s+([^\s])/g, 'return $1')
299+
// Restore arrow functions
300+
.replace(/___ARROW___/g, ' => ');
301+
302+
// Parse and format the statements with proper line breaks and nesting
303+
return formatCodeWithNesting(formatted, baseIndent, indentUnit);
176304
}
177305

178-
// Check if the parameter is function
306+
function formatCodeWithNesting(code, baseIndent, indentUnit) {
307+
var result = '';
308+
var lines = [];
309+
var current = '';
310+
var braceDepth = 0;
311+
var inString = false;
312+
var stringChar = '';
313+
314+
// First pass: break into logical lines, handling } else { pattern
315+
for (var i = 0; i < code.length; i++) {
316+
var char = code[i];
317+
318+
// Handle strings
319+
if (!inString && (char === '"' || char === "'" || char === '`')) {
320+
inString = true;
321+
stringChar = char;
322+
} else if (inString && char === stringChar && code[i-1] !== '\\') {
323+
inString = false;
324+
stringChar = '';
325+
}
326+
327+
if (!inString) {
328+
if (char === '{') {
329+
current += char;
330+
lines.push(current.trim());
331+
current = '';
332+
braceDepth++;
333+
continue;
334+
} else if (char === '}') {
335+
if (current.trim()) {
336+
lines.push(current.trim());
337+
}
338+
braceDepth--;
339+
340+
// Check for } else { pattern
341+
var nextNonWhitespace = '';
342+
var j = i + 1;
343+
while (j < code.length && /\s/.test(code[j])) {
344+
j++;
345+
}
346+
if (j < code.length - 4 && code.substring(j, j + 4) === 'else') {
347+
// Skip to after 'else'
348+
j += 4;
349+
while (j < code.length && /\s/.test(code[j])) {
350+
j++;
351+
}
352+
if (j < code.length && code[j] === '{') {
353+
// This is } else {
354+
lines.push('} else {');
355+
i = j; // Skip to the {
356+
braceDepth++;
357+
current = '';
358+
continue;
359+
}
360+
}
361+
362+
lines.push('}');
363+
current = '';
364+
continue;
365+
} else if (char === ';') {
366+
current += char;
367+
lines.push(current.trim());
368+
current = '';
369+
continue;
370+
}
371+
}
372+
373+
current += char;
374+
}
375+
376+
// Add any remaining content
377+
if (current.trim()) {
378+
lines.push(current.trim());
379+
}
380+
381+
// Second pass: apply proper indentation
382+
var currentDepth = 2; // Start at depth 2 for function bodies (object has 1, function has 2)
383+
for (var k = 0; k < lines.length; k++) {
384+
var line = lines[k].trim();
385+
if (!line) continue;
386+
387+
// Adjust depth for closing braces
388+
if (line === '}' || line.startsWith('}')) {
389+
currentDepth--;
390+
}
391+
392+
// Apply indentation
393+
result += indentUnit.repeat(currentDepth) + line;
394+
395+
// Add newline except for last line
396+
if (k < lines.length - 1) {
397+
result += '\n';
398+
}
399+
400+
// Adjust depth for opening braces
401+
if (line.endsWith('{')) {
402+
currentDepth++;
403+
}
404+
405+
// Add semicolon if missing (except for braces)
406+
if (!line.endsWith(';') && !line.endsWith('{') && line !== '}' && !line.startsWith('}')) {
407+
result = result.replace(/([^;}])$/, '$1;');
408+
}
409+
}
410+
411+
return result;
412+
} // Check if the parameter is function
179413
if (options.ignoreFunction && typeof obj === "function") {
180414
obj = undefined;
181415
}
@@ -261,6 +495,6 @@ module.exports = function serialize(obj, options) {
261495

262496
var fn = functions[valueIndex];
263497

264-
return serializeFunc(fn);
498+
return serializeFunc(fn, options);
265499
});
266500
}

test/unit/serialize.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,70 @@ describe('serialize( obj )', function () {
567567
'{"num":123,"str":"str"}'
568568
);
569569
});
570+
571+
it('should apply `space` option to function bodies (Issue #195)', function () {
572+
// Test compact function without space option
573+
var objWithFunction = {
574+
isSupported: function({filepath}){const basename=require('path').basename(filepath);return basename===".env"||basename.startsWith(".env.")}
575+
};
576+
577+
var withoutSpace = serialize(objWithFunction);
578+
strictEqual(withoutSpace, '{"isSupported":function({filepath}){const basename=require(\'path\').basename(filepath);return basename===".env"||basename.startsWith(".env.")}}');
579+
580+
// Test function body should be formatted with space: 2
581+
var withSpace2 = serialize(objWithFunction, { space: 2 });
582+
var expected = '{\n "isSupported": function({filepath}) {\n const basename = require(\'path\').basename(filepath);\n return basename === ".env" || basename.startsWith(".env.");\n }\n}';
583+
strictEqual(withSpace2, expected);
584+
585+
// Test function body should be formatted with space: 4
586+
var withSpace4 = serialize(objWithFunction, { space: 4 });
587+
var expectedSpace4 = '{\n "isSupported": function({filepath}) {\n const basename = require(\'path\').basename(filepath);\n return basename === ".env" || basename.startsWith(".env.");\n }\n}';
588+
strictEqual(withSpace4, expectedSpace4);
589+
});
590+
591+
it('should apply `space` option to named function bodies', function () {
592+
var objWithNamedFunction = {
593+
process: function processData(data) {const result=data.map(x=>x*2);if(result.length>0){return result.filter(x=>x>10);}return [];}
594+
};
595+
596+
var withSpace2 = serialize(objWithNamedFunction, { space: 2 });
597+
var expected = '{\n "process": function processData(data) {\n const result = data.map(x => x * 2);\n if (result.length > 0) {\n return result.filter(x => x > 10);\n }\n return [];\n }\n}';
598+
strictEqual(withSpace2, expected);
599+
});
600+
601+
it('should apply `space` option to arrow function bodies', function () {
602+
var objWithArrowFunction = {
603+
transform: (x)=>{const doubled=x*2;if(doubled>10){return doubled;}return 0;}
604+
};
605+
606+
var withSpace2 = serialize(objWithArrowFunction, { space: 2 });
607+
var expected = '{\n "transform": (x) => {\n const doubled = x * 2;\n if (doubled > 10) {\n return doubled;\n }\n return 0;\n }\n}';
608+
strictEqual(withSpace2, expected);
609+
});
610+
611+
it('should apply `space` option to multiple functions in same object', function () {
612+
var objWithMultipleFunctions = {
613+
fn1: function(){return 1;},
614+
fn2: ()=>{return 2;},
615+
fn3: function named(){return 3;}
616+
};
617+
618+
var withSpace2 = serialize(objWithMultipleFunctions, { space: 2 });
619+
var expected = '{\n "fn1": function() {\n return 1;\n },\n "fn2": () => {\n return 2;\n },\n "fn3": function named() {\n return 3;\n }\n}';
620+
strictEqual(withSpace2, expected);
621+
});
622+
623+
it('should handle edge cases with space option and functions', function () {
624+
// Test with string space option
625+
var objWithFunction = { fn: function(){return true;} };
626+
var withStringSpace = serialize(objWithFunction, { space: ' ' });
627+
var expected = '{\n "fn": function() {\n return true;\n }\n}';
628+
strictEqual(withStringSpace, expected);
629+
630+
// Test with no space (should not format function bodies)
631+
var withoutSpaceOption = serialize(objWithFunction);
632+
strictEqual(withoutSpaceOption, '{"fn":function(){return true;}}');
633+
});
570634
});
571635

572636
describe('backwards-compatability', function () {

0 commit comments

Comments
 (0)