So, I've seen several other solutions to this question and also on these:
And other methods, besides the ones that rely on external dependencies and parsers, when tested with the following functions break, or return a messy result.
Test cases:
// 1
const testFn1 = (
nonEmpty=' ',
end=')',
y=end,
x,
z=x,
someCalc = 2*2.3-(2/2),
comment='//',
template='`,',
{destructuring, test = '2', ...rest},
{defDestruct, defDestruct_a, defDestruct_b} = {defDestruct: '1', defDestruct_a: 2, defDestruct_b: '3'},
a = {1: '2', 'hi': [3,4,'==)']},
anArr = [z,y],
brackets = (1.23+')'),
math = Math.round(1.34*10),
h={a:'ok \"', b:{c:'now=2,tricky=2', d:'rlly'}},
[test2, destructuring2 = 3, two],
[destructuring3,,
notIgnored],
[destructuring4, ...rest2],
[defDestruct2, defDestruct2_a] = [4,5],
{a: destrct5, c: destrct5_a, e : destrct5_b = 5},
lastArg
) => {}
//2
const testFn2 = (a,
b=2, c={a:2, b:'lol'}, dee, e=[2,3,"a"],/*lol joke=2,2*/ foook=e, g=`test, ok`, h={a:'ok \"', b:{c:'lol=2,tricky=2', d:'lol'}}, i=[2,5,['x']], lastArg = "done" //ajaim, noMatch = 2)
) => {}
These are probably unusual cases, but I wanted to propose a stand-alone solution, that covers more edge cases and also returns an object with arguments as keys paired with their default values (evaluated not strings).
const delimiters = {
brackets: {
open: ["{","[","("],
close: ["}","]",")"]
},
str: ["'",'"',"`"],
destructuring: {
obj: ["{","}"],
arr: ["[","]"]
},
cmmt: {
open: ["/*","//"],
close: ["*/", null]
}
}
/**
* Parses the parameters of a function.toString() and returns them as an object
* @param {*} fn The function to get the parameters from
* @param {*} args arguments to pass to the function
* @returns An object with the parameters and their default values
* @author [bye-csavier](https://github.com/bye-csavier)
*/
function parseFnParams(fn, ...args){
if(typeof fn != 'function') return null;
let [subStr, parsedArgs, openDelimiters, argList] = ['', "", ['('], ''];
fn = fn.toString().split('') // function to "char" array
let len = fn.length, READ_ARG = true, INSIDE_STR = false, INSIDE_DESTRUCT = {obj: false, arr: false}, INSIDE_CMMT = {block: false, line: false};
const addArg = () =>{
subStr = subStr.replace(/\s|\./g, '');
if(subStr.length == 0) return;
parsedArgs += `${subStr}:${subStr},\n`;
subStr = '';
}
for(let i=fn.indexOf('(')+1, char; i<len && openDelimiters.length > 0; i++){
char = fn[i];
let idx = -1;
if(!INSIDE_STR && !INSIDE_CMMT.block && !INSIDE_CMMT.line){
if(char == "/" && fn[i+1] == "/") INSIDE_CMMT.line = true;
else if(char == "/" && fn[i+1] == "*") INSIDE_CMMT.block = true;
else if(READ_ARG && char == delimiters.destructuring.obj[0]){ INSIDE_DESTRUCT.obj = true; openDelimiters.push(char); idx=0; }
else if(READ_ARG && char == delimiters.destructuring.arr[0]){ INSIDE_DESTRUCT.arr = true; openDelimiters.push(char); idx=0; }
else if((INSIDE_DESTRUCT.obj && char == delimiters.destructuring.obj[1]) || (INSIDE_DESTRUCT.arr && char == delimiters.destructuring.arr[1])){
INSIDE_DESTRUCT.obj = false; INSIDE_DESTRUCT.arr = false;
if(READ_ARG) addArg();
openDelimiters.pop(); idx=0;
}
else if(delimiters.brackets.open.indexOf(char) > -1)
{
openDelimiters.push(char);
idx = 0;
}
else{
idx = delimiters.brackets.close.indexOf(char);
if(idx > -1 && delimiters.brackets.open.indexOf(openDelimiters[openDelimiters.length-1]) == idx) openDelimiters.pop();
}
}
if(INSIDE_CMMT.line){ if(char == "\n") INSIDE_CMMT.line = false; }
else if(INSIDE_CMMT.block){ if(char == "/" && fn[i-1] == "*") INSIDE_CMMT.block = false; }
else if(READ_ARG && !INSIDE_STR && idx < 0){
if(char == ',') addArg();
else if(char == "="){addArg(); READ_ARG = false;}
else if(INSIDE_DESTRUCT.obj && char == ':') subStr = "";
else subStr += char;
}
else if((openDelimiters.length == 1 || (INSIDE_DESTRUCT.obj || INSIDE_DESTRUCT.arr)) && char == ',') READ_ARG = true;
else if(delimiters.str.indexOf(char) > -1){
if(!INSIDE_STR) INSIDE_STR = char;
else if(INSIDE_STR == char) INSIDE_STR = false;
}
argList += char;
}
addArg(); // to also push the last arg
fn = eval(`(${argList}=>{return {${parsedArgs}}}`);
parsedArgs = fn(...args);
return parsedArgs;
}
The function is nothing fancy or elegant, and neither is it fast. It does around 12 Ops/sec for the 1° test and 60 Ops/sec for the 2° test. But it's more robust and actually returns the function arguments in an organized way. In the current state, it should be used to precompute the default value of the parameters.
To summarize, this function should handle:
- comments
- non-constant defaults like
(x,y=x)=>{}
- whitespace
- passing a list of arguments
- anonymous functions
- arrow functions
The support for destructuring assignment is limited. It supports only simple desturing assignment like [x,,y=2]
, {x, y=2}
, {a: x, b: y=2}
, [x, ...rest]
, and {x, ...rest}
. It doesn't support (meaning that it breaks with): further destructuring like {a, b: {c:d}}
; binding pattern as the rest property (e.g [a, b, ...{ length }]
or [a, b, ...[c, d]]
);
And it also assumes parameters to be wrapped inside round brackets, otherwise it will not work.
Thanks to @Bergi for pointing out the issues in my previous solutions. As @Bergi (and others) said, using an already existing JS parser would be a more reliable solution.
Lastly, I don't know anything about parsing, so any improvement is welcomed.