如何在节点 js 中读取文件,找到函数的所有实例,然后提取每个函数的参数?

How can I read a file in node js, find all instances of a function and then extract each function's argument?

我正在尝试编写一个节点脚本来识别我的 React 项目中未使用的翻译字符串。

首先,我想获得 使用的所有翻译的列表。为此,我在我的 /src/components 文件夹中获取每个 JS 文件的列表,然后读取该文件。

我的翻译字符串如下所示:t('some.translation.key'),所以基本上,我想使用 RegEx 识别 t('...') 的每个实例,然后获取这些括号之间的键(即“some.translation.key”)。从那里,我应该能够将密钥与我的翻译 JSON 文件中的密钥进行比较,并删除未使用的密钥。

unused.js

const path = require('path');
const fs = require('fs');
let files = [];

// 
function getFiles(dir) {
  fs.readdirSync(dir).forEach(file => {
    const absolute = path.join(dir, file);

    if (fs.statSync(absolute).isDirectory()) {
      getFiles(absolute);
    } else {
      if (absolute.includes('.js')) {
        files.push(absolute);
      }
    }
  });
  return files;
}

function getTranslations() {
  const pathComponents = path.join(__dirname, '../../src/components');

  // get all js files in components directory
  const files = getFiles(pathComponents);

  const translationKeys = [];

  // for each js file
  for(let i = 0; i < files.length; i++) {
    // read contents of file
    const contents = fs.readFileSync(files[i]).toString();

    // search contents for all instances of t('...')
    // and get the key between the parentheses

  }
}

getTranslations();

如何使用正则表达式在 contents 中查找 t('...') 的所有实例,然后提取括号之间的 ... 字符串?

是的,您可以使用正则表达式:

for (const [, str] of contents.matchAll(/\bt\(['"](.*?)['"]\)/g)) {
  console.log('t called with string argument:', str)
}

但是,对于正则表达式,问题在于它们 理解 代码,并且会在匹配包含 ( ) 或 [=13 的字符串时出现问题=] 他们自己,在连接字符串或额外空格等方面存在问题,然后您还可以从字面上获取内容,包括可能的转义序列。

一种更可靠的方法是从代码创建一个 AST (abstract syntax tree) 并在其中查找对 t 的调用。

一个流行的 AST 解析器是 acorn. There is also the supplementary module acorn-walk,它可以帮助遍历整个语法树,而无需构建您自己的递归算法。

import acorn from 'acorn'
import walk from 'acorn-walk'

// Example
const contents = "function a () { if (123) { t('hello') } return t('world') }"

// The arguments to acorn.parse would have to be adjusted based
// on what kind of syntax your files can use.
const result = acorn.parse(contents, {ecmaVersion: 2020})

walk.full(result, node => {
  if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 't') {
    if (node.arguments.length === 1 && node.arguments[0].type === 'Literal' && typeof node.arguments[0].value === 'string') {
      // This is for the case `t` is called with a single string
      // literal as argument.
      console.log('t called with string argument:', node.arguments[0].value)
    } else {
      // In case you have things like template literals as well,
      // or multiple arguments, you'd need to handle them here too.
      console.log('t called with unknown arguments:', node.arguments)
    }
  }
})

// Will output:
//   t called with string argument: hello
//   t called with string argument: world