🧩
RecastでASTを探索してコードを動的に変換する
Recastを使ってみたら便利だったので紹介です。
🛠️ Recastとは?
Recastは、JavaScriptのAST(抽象構文木)を探索・操作するためのライブラリです。ASTを扱うための便利な関数が豊富に提供されています。
大規模なコードベースでのリファクタリングや、特定のコーディングパターンの自動修正ツールを作成する際に利用できます。
基本的な使い方
コードを探索・操作する流れは以下の通りです。
-
parse
でコードをASTに変換する -
visit
でASTを探索して、特定のNodeを見つける - Nodeを改変する
-
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
});
🧑💻 サンプル
以下簡単なユースケースごとのサンプルです。
特定のコードを置換する
let
をconst
に置換する場合。
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