5

There is a test.js:

const add = (x,y) => {
    return x+y;
}

const multiply = (x,y,z) => {
    return x*y*z;
}

I want to read this test.js from an index.js and print all its function name and arguments;

const fs = require("fs");

let file = fs.readFileSync("./test.js", "utf8");

let functionArg = "Do some operations"

console.log(functionArg)

//Result:
//  add : [x,y]
//  multiply : [x,y,z]

Without module.exports.
Is it possible to read js file and return all its function and their arguments.

Shubham Chadokar
  • 2,520
  • 1
  • 24
  • 45
  • Since you don't want to import the functions, you can read every line of the file and check if the line is a function definition or declaration. – DEVCNN Mar 13 '19 at 09:38
  • How to do that? For functions I can read each line and split it by a space and then can check if it is of type function or not. What about arguments? Can you please explain your approach? thanks – Shubham Chadokar Mar 13 '19 at 09:54
  • You don't have to read each line for functions. see my answer below – wang Mar 13 '19 at 09:58
  • I can confirm that remix23's answer works. Just tried it. – DEVCNN Mar 13 '19 at 11:06

4 Answers4

3

You can get functions and their arguments with the help of a JavaScript parser like esprima.

const fs = require("fs");
const esprima = require('esprima');

let file = fs.readFileSync("./test.js", "utf8");

let functionArg = esprima.parseScript(file);

functionArg.body.forEach(el => {
  let variableDeclarator = el.declarations[0]
  let params = []
  variableDeclarator.init.params.forEach(arg => {
    params.push(arg.name)
  })
  console.log(variableDeclarator.id.name, ' : ', [params.join()])
})

//Result:
// add  :  [ 'x,y' ]
// multiply  :  [ 'x,y,z' ]
TGrif
  • 5,725
  • 9
  • 31
  • 52
  • Thanks for sharing the answer. I had heard about ESPrima before, but never used it. After your answer I read more about it, went through its code, and it helped improve my JS knowledge. – Niraj Kumar Mar 13 '19 at 13:06
  • 1
    @NirajKumar good reflex. Note that the project is 68% in TypeScript so it will help to improve your TS too ;) – TGrif Mar 13 '19 at 16:19
2

When you need to read a code file, the better is to directly use a compiler when available.

It turns out acorn is a well known parser and you're probably already using it without knowing because you're probably using babel.

With acorn you can parse a source file into an abstract source tree which in turn can be walked with acorn-walk to find what you need.

example:

testfiletoparse.js


    export function factorialR(n) {
        if (n <= 1) {
            return 1;
        }
        return factorialR(n - 1) * n;
    }

    export function bound(v, min, max) {
        return Math.max(Math.min(v, min), max);
    }

    export function isNullOrEmpty(str) {
        return !str || str.length === 0;
    }

    export const arrowFunction = v => v;

basicparser.js


    import fs from 'fs';

    const acorn = require('acorn');
    const walk = require('acorn-walk');

    require('acorn-object-rest-spread/inject')(acorn);
    require('acorn-static-class-property-initializer/inject')(acorn);
    require('acorn-class-fields/inject')(acorn);

    const filePath = 'src/testfiletoparse.js';

    const sourceCode = String(fs.readFileSync(filePath));
    const program = acorn.parse(
        sourceCode,
        {
            ranges: true,
            locations: true,
            sourceType: 'module',
            plugins: {
                objectRestSpread: true,
                // es7: true,
                staticClassPropertyInitializer: true,
                classFields: true,
            }
        }
    );

    walk.full(
        program,
        /**
         * @param {}
         */
        (node) => {
            if (node.type === 'FunctionDeclaration') {
                console.log(`There's a FunctionDeclaration node at ${JSON.stringify(node.loc.start)}`);
                console.log(`Function name is ${node.id.name}`);
                const params = node.params.map((param) => {
                    return param.name;
                }).join();
                console.log(`Function params are ${params}`);
            }
            // it is a little tricky to catch arrow functions but trial and error will get you through it
            if (node.type === 'VariableDeclarator' && node.init.type === 'ArrowFunctionExpression') {
                console.log(`There's an arrow function expression declaration node at ${JSON.stringify(node.loc.start)}`);
                console.log(`Its name is ${node.id.name}`);
                const params = node.init.params.map((param) => {
                    return param.name;
                }).join();
                console.log(`Function params are ${params}`);
            }
        }
    );

output


    There's a FunctionDeclaration node at {"line":1,"column":7}
    Function name is factorialR
    Function params are n
    There's a FunctionDeclaration node at {"line":8,"column":7}
    Function name is bound
    Function params are v,min,max
    There's a FunctionDeclaration node at {"line":12,"column":7}
    Function name is isNullOrEmpty
    Function params are str
    There's an arrow function expression declaration node at {"line":16,"column":13}
    Its name is arrowFunction
    Function params are v

Starting with this it should be quite straightforward to find a solution to your problem.

remix23
  • 2,632
  • 2
  • 11
  • 21
1
  • read a js file
  • replace const to this.
  • wrap it inside constructor function and evaluate it.
  • create a instance of it
  • since you replaced const to this., all variables in test.js became the member of the instance. now you can interate this instance to get the members.
  • to get the function signature, you will have to convert a function object to string and get the arguments manually.

Here is the code:

const fs = require("fs");

let file = fs.readFileSync("./test.js", "utf8");

const getFuncInfo = function(file) {
  file = file.replace(new RegExp('const ', 'g'), 'this.');
  eval(`function Container(){
    ${file}}
  `)
  const instance = new Container()
  const names = Object.keys(instance)
  return names.reduce((res, cur) => {
    if(typeof instance[cur] == 'function') {
      let args = instance[cur].toString()
      res[cur] = args.split('(')[1].split(')')[0].split(',')
    }
    return res;
  }, {})
}

let functionArg = getFuncInfo(file)

console.log(functionArg)

The result is:

{ add: [ 'x', 'y' ], multiply: [ 'x', 'y', 'z' ] }

Edit: Regarding the question about what eval does, it is same as below:

const getFuncInfo = function(file) {
  file = file.replace(new RegExp('const ', 'g'), 'this.');
  // think like eval replace the content with below one
  function Container(){
    // content of test.js except `const ` is replaced with `this.`
    this.add = (x,y) => {
      return x+y;
    }

    this.multiply = (x,y,z) => {
      return x*y*z;
    }
  }
  // end of replacement
  const instance = new Container()
  const names = Object.keys(instance)
  return names.reduce((res, cur) => {
    if(typeof instance[cur] == 'function') {
      let args = instance[cur].toString()
      res[cur] = args.split('(')[1].split(')')[0].split(',')
    }
    return res;
  }, {})
}
wang
  • 1,660
  • 9
  • 20
  • Can you please explain what actually eval is doing here? code is working. thanks – Shubham Chadokar Mar 13 '19 at 10:46
  • eval is executing the code. look at the parameter of it. it's not just the content of test.js. it's wrapped by another function. think of it as compiler. – wang Mar 13 '19 at 11:28
  • yeah I print the result to check what it is doing. My ques. is as eval just evaluate on runtime, so what that actually doing inside the eval? It should work if i put the Container function as a new function, but it is not working. – Shubham Chadokar Mar 13 '19 at 11:33
  • `It should work if i put the Container function as a new function, but it is not working.` what do you mean by this? – wang Mar 13 '19 at 11:51
  • I edited my answer. please check again and let me know if you still have question – wang Mar 13 '19 at 11:55
  • const instance = new Function(file); I was doing this but when I am printing it is printing complete file, but when I was printing the eval's evaluated instance it was printing "Container { add: [Function], multiply: [Function] }" – Shubham Chadokar Mar 13 '19 at 11:58
  • 1
    `const instance = new Function(file); this is what running inside the eval`. no it's not :). please check my edited answer. – wang Mar 13 '19 at 11:59
  • 1
    Okay now I understood, when we put function Container() {file}; it takes file as a variable. but when we use eval it first write the complete function with $file and then it runs, thanks for the explanation it helped – Shubham Chadokar Mar 13 '19 at 12:07
  • glad to help you :) – wang Mar 13 '19 at 12:08
  • 1
    For a lot of reasons, using eval is a bad option. https://stackoverflow.com/questions/86513/why-is-using-the-javascript-eval-function-a-bad-idea – DEVCNN Mar 13 '19 at 12:31
  • @DEVCNN Agreed with your point. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#Do_not_ever_use_eval! While, I will never recommend anyone to use EVAL, the code above was an interesting read. – Niraj Kumar Mar 13 '19 at 12:52
-3

I think you are on the right path already but you never used the file you read, it should actually be(I hope I got your question right):

let file = fs.readFileSync("./test.js", "utf8");
console.log(file);

// Result:
//const add = (x,y) => {
//   return x+y;
//}

//const multiply = (x,y,z) => {
//  return x*y*z;
// }
Steve S.
  • 1
  • 2
  • thanks for the answer, actually i don't want to print the content of js file, I want only the function name and their arguments inside the js file. – Shubham Chadokar Mar 13 '19 at 10:45