🧩

RecastでASTを探索してコードを動的に変換する

2024/02/09に公開

Recastを使ってみたら便利だったので紹介です。

🛠️ Recastとは?

https://github.com/benjamn/recast

Recastは、JavaScriptのAST(抽象構文木)を探索・操作するためのライブラリです。ASTを扱うための便利な関数が豊富に提供されています。

大規模なコードベースでのリファクタリングや、特定のコーディングパターンの自動修正ツールを作成する際に利用できます。

基本的な使い方

コードを探索・操作する流れは以下の通りです。

  1. parseでコードをASTに変換する
  2. visitでASTを探索して、特定のNodeを見つける
  3. Nodeを改変する
  4. printでASTをコードに変換する

簡単な例として、fooという変数名をbarに変更するスクリプトは以下のようになります。

import { parse, visit, print } from "recast";

const code = `const foo = 'hello world';`;

// コード文字列をASTに変換
const ast = parse(code);

// ASTを探索
visit(ast, {
  // 探索中にIdentifierを見つけたら行う処理を登録
  visitIdentifier(path) {
    const { node } = path;
    // 変数名がfooの場合はbarに変更
    if (node.name === "foo") {
        node.name = "bar";
    }
    return false;
  },
});

// const bar = 'hello world';
console.log(print(ast).code);

便利なヘルパー関数

Nodeのチェックや、改変に便利なヘルパー関数も提供されています。

namedTypes

namedTypes ではASTのNodeの型を表すオブジェクトが用意されているので、型をチェックする際に利用できます。

import { types } from "recast";

const n = types.namedTypes;

// nodeがCallExpressionかどうかをチェック
if (n.CallExpression.check(node)) {
  // ...
}

builders

buildersにはASTのNodeを生成するための関数が用意されているので、新しいNodeを生成する際に利用できます。

import { types } from "recast";

const b = types.builders;

// 新しいCallExpressionを生成
const newCallExpression = b.callExpression(
  b.identifier("add"),
  [b.literal("1"), b.literal("2")]
);

// add(1, 2)
print(newCallExpression).code;

prettyPrint

もし、出力時にコードをいい感じに整形したい場合は、prettyPrint が利用できます。

import { prettyPrint } from "recast";

const code = `
const foo = 1 +
2
`;

const ast = parse(code);

// const foo = 1 +
// 2
console.log(print(ast).code);

// const foo = 1 + 2;
console.log(prettyPrint(ast).code);

複数のパーサーに対応

Recastはデフォルトだと、Esprima JavaScript parserに対応していますが、parseの第2引数にパーサーを指定することで、他のパーサーにも変更できます。

TypeScriptやJSXのコードを使う場合などに変更すると便利です。

import { parse } from "recast";
import TSParser from "recast/parsers/typescript";

const code = `
  function hello(name: string): string {
    return 'hello ' + name;
  }
`;

// TypeScriptのASTに変換
const ast = parse(code, {
  parser: TSParser
});

🧑‍💻 サンプル

以下簡単なユースケースごとのサンプルです。

特定のコードを置換する

letconstに置換する場合。

const replaceLetToConst = (code: string) => {
  const ast = parse(code);
  visit(ast, {
    visitVariableDeclaration(path) {
      const { node } = path;
      if (node.kind === "let") {
        node.kind = "const";
      }
      return false;
    },
  });
  return print(ast).code;
}

🧪 テスト

test("replace let to const", () => {
  const code = `
    let hello = 'hello';
    let world = 'world';
  `;
  const result = replaceLetToConst(code);
  expect(result).toBe(`
    const hello = 'hello';
    const world = 'world';
  `);
})

特定のコードを削除する

コード上のconsole.logを削除する場合。

const removeConsoleLog = (code: string) => {
  const ast = parse(code);
  visit(ast, {
    visitExpressionStatement(path) {
      const { node } = path;
      if (
        n.CallExpression.check(node.expression) &&
        n.MemberExpression.check(node.expression.callee) &&
        n.Identifier.check(node.expression.callee.object) &&
        node.expression.callee.object.name === "console" &&
        n.Identifier.check(node.expression.callee.property) &&
        node.expression.callee.property.name === "log"
      ) {
        path.prune();
      }
      return false;
    },
  });
  return print(ast).code;
}

🧪 テスト

test("remove console.log", () => {
  const code = `
    console.log('hello world');

    function helloWorld() {
      console.log('hello world');
      return 'hello world';
    }
  `;
  const result = removeConsoleLog(code);
  expect(result).toBe(`
    function helloWorld() {
      return 'hello world';
    }
  `);
});

特定のコードを追加する

use strictを追加する場合。

const addUseStrict = (code: string) => {
  const ast = parse(code);
  visit(ast, {
    visitProgram(path) {
      const { node } = path;
      const useStrictStatement = b.expressionStatement(b.literal("use strict"));
      node.body.unshift(useStrictStatement);
      return false;
    },
  });
  return print(ast).code;
}

🧪 テスト

test("add use strict", () => {
  const code = `
    function helloWorld() {
      return 'hello world';
    }
  `;
  const result = addUseStrict(code);
  expect(result).toBe(`
    "use strict";
    function helloWorld() {
      return 'hello world';
    }
  `);
})

型アノテーションを追加する

hello関数に型アノテーションを追加する場合。

const addTypeAnnotation = (code: string) => {
  const ast = parse(code, {
    parser: TSParser,
  });
  visit(ast, {
    visitFunctionDeclaration(path) {
      const { node } = path;
      if (!(n.Identifier.check(node.id) && node.id.name === "hello")) return false;
      if(n.Identifier.check(node.params[0])) { node.params[0].typeAnnotation = b.tsTypeAnnotation(
          b.tsStringKeyword()
        );
      }
      node.returnType = b.tsTypeAnnotation(
        b.tsStringKeyword()
      );
      return false
    },
  });
  return print(ast).code;
}

🧪 テスト

test("add type annotation", () => {
  const code = `
    function hello(name) {
      return 'hello ' + name;
    }
  `;
  const result = addTypeAnnotation(code);
  expect(result).toBe(`
    function hello(name: string): string {
      return 'hello ' + name;
    }
  `);
})

おわりに

Recastの紹介でした。これを使って便利なスクリプトでも作れたなと思っています💪

Discussion