序章

昔から残っている大きなJavaScriptファイルがあるとしましょう。 70,000行の長さで、webpackまたはコンソートを使用して必死に分割する必要があります。 次に、グローバルスコープに公開する関数または定数を知る必要があります。

コンピューターにコードを読み取らせ、そこから必要なものを抽出します。

それは抽象構文木(AST)の仕事です。

Logo declaring AST in a design that resembles a comic book superhero.

次の例は小さいです。 私たちの使命は、あなたがそれを受け入れることを選択した場合、グローバルスコープで公開されているすべての関数の名前を抽出することです。

test.js
// test the code
function decrementAndAdd(a, b) {
   function add(c, d) {
      return c + d;
   }
   a--;
   b = b - 1;
   return add(a,b)
}

// test the code
function incrementAndMultiply(a, b) {
    function multiply(c, d) {
      return c * d;
    }
    a++;
    b = b + 1;
    return multiply(a, b)
}

結果は["decrementAndAdd", "incrementAndMultiply"]になります。

コードの解析

ASTは、コードを解析した結果です。 JavaScriptの場合、ASTはソースのツリー表現を含むJavaScriptオブジェクトです。 使用する前に、作成する必要があります。 解析するコードに応じて、適切なパーサーを選択します。

ここでは、コードがES5互換であるため、acornパーサーを選択できます。

最もよく知られているオープンソースECMAScriptパーサーのいくつかを次に示します。

パーサー サポートされている言語 GitHub
どんぐり esnext&JSX(acorn-jsxを使用) https://github.com/acornjs/acorn
エスプリマ esnext&older https://github.com/jquery/esprima
cherow esnext&older https://github.com/cherow/cherow
espree esnext&older https://github.com/eslint/espree
シフト esnext&older https://github.com/shapesecurity/shift-parser-js
バベル esnext、JSX、typescript https://github.com/babel/babel
TypeScript esnext&typescript https://github.com/Microsoft/TypeScript

すべてのパーサーは同じように機能します。 コードを入力して、ASTを取得します。

const { Parser } = require('acorn')

const ast = Parser.parse(readFileSync(fileName).toString())

TypeScriptパーサーの構文は少し異なります。 しかし、それはここに文書化されています

これは、@babel/parser解析で取得されたツリーです。

Tree graph from @babel/parser

test-2.js
// test the code
function decrementAndAdd(a, b) {
  return add(a, b)
}

トラバース

抽出しようとしているものを見つけるために、AST全体を一度に処理しない方がよい場合がよくあります。 小さなコードスニペットの場合でも、数千のノードを持つ大きなオブジェクトになります。 したがって、必要な情報を抽出する前に、検索を絞り込みます。

そのための最善の方法は、気になるトークンのみをフィルタリングすることです。

繰り返しになりますが、このトラバース部分を実行するための多くのツールが利用可能です。 この例では、recastを使用します。 これは非常に高速で、コードのバージョンを変更しないという利点があります。 このようにして、元のフォーマットで必要なコードの部分を返すことができます。

トラバース中に、すべてのfunctionトークンが見つかります。 これが、visitFunctionDeclarationメソッドを使用する理由です。

変数の割り当てを確認したい場合は、visitAssignmentExpressionを使用します。

recast-acorn-example.js
const recast = require('recast');
const { Parser } = require('acorn');

const ast = Parser.parse(readFileSync(fileName).toString());

recast.visit(
  ast,
  {
    visitFunctionDeclaration: (path) => {
      // the navigation code here...

      // return false to stop at this depth
      return false;
    }
  }
)

ASTノードタイプ

通常、トークンタイプの名前は明確ではありません。 ast-explorer を使用して、調査したタイプを検索できます。 左側のパネルにコードを貼り付け、使用しているパーサーを選択して、「voilà!」と入力するだけです。 右側の解析されたコードを参照して、探しているNode Typeを見つけます。

浅いまたは深い

ツリーのすべてのレベルを常に見たいとは限りません。 詳細な検索を実行したい場合もあれば、最上位のレイヤーを確認したい場合もあります。 フレームワークによって、構文は異なります。 幸いなことに、それは通常十分に文書化されています。

recastを使用すると、現在の深度での検索を停止したい場合は、完了したらreturn falseだけにします。 これは私たちが以前にしたことです。 トラバース(深く)したい場合は、以下に示すようにthis.traverse(path)を使用できます。

@babel/traverseを使用すると、babelに続行する場所を指示する必要がありません。 return falseステートメントで停止する場所を指定するだけで済みます。

recast-acorn-example.js
recast.visit(
  ast,
  {
    visitFunctionDeclaration: (path) => {
      // deal with the path here...

      // run the visit on every child node
      this.traverse(path);
    }
  }
)

非常に幅広い検索から、より小さなサンプルに移行しました。 これで、必要なデータを抽出できます。

visitFunctionDeclarationに渡されるpathオブジェクトは、NodePathです。 このオブジェクトは、親と子のASTノード間の接続を表します。 このpathは、関数宣言と関数の本体の間のリンクを表すため、それ自体では役に立ちません。

ast-explorerを使用して、探しているパスの内容を見つけます。

古典的なこと:path.node。 親子関係の子ノードを取得します。 関数の検索を選択した場合、path.nodeのノードのタイプはFunctionになります。

recast-acorn-example.js
const functionNames = [];

recast.visit(
  ast,
  {
    visitFunctionDeclaration: (path) => {
      console.log(path.node.type); // will print "FunctionDeclaration"
      functionNames.push(path.node.id.name); // will add the name of the function to the array

      // return false to avoid looking inside of the functions body
      // we stop our search at this level
      return false;
    }
  }
)

サブツリーを確認するために、トラバース関数を相互にラップしてみてください。 以下のコードは、正確に2番目のレベルにあるすべての関数を返します。 関数内の関数内の関数を認識しません。

recast-acorn-example.js
const functionNames = [];

recast.visit(
  ast,
  {
    visitFunctionDeclaration: (path) => {
      let newPath = path.get('body');

      // subtraversing
      recast.visit(
        newPath,
        {
          visitFunctionDeclaration: (path) => {
            functionNames.push(path.node.id.name);
            return false;
          }
        }
      )

      // return false to not look at other functions contained in this function
      // leave this role to the sub-traversing
      return false;
    }
  }
)

任務完了!!

プログラムですべての関数名を見つけました。 引数または公開された変数の名前を簡単に見つけることができます。

用語集

ASTノードツリー内の1つのオブジェクト。 例:関数宣言、変数代入、オブジェクト式

ツリー内の親ノードと子ノード間のNodePathリンク

ノードの定義のNodeProperty部分。 ノードに応じて、名前だけまたはより多くの情報を持つことができます