🖖
node.jsでASTの簡易CRUD操作
この記事はオプティマインド Location Tech Advent Calendar 2022の23日目の記事となります。
はじめに
テンプレートコードを自動生成のやり方を確認する時に、コンパイラ関連のAST
について、勉強しました。
ASTって何?
AST(abstract syntax tree)は抽象構文木と言われて、意味に関係ある情報のみを抽象した木構造の木である。wikiの定義はこちら
ASTの例
以下の計算式があるとして、
5 * 4 / 2 + 3 * 6
ASTに変換すると↓こんな感じになります。
ここで構造を確認すると、以下のもの分けられいます。
- 数値
- オペレーション
つまり、children treeが独立できる、かつ、多くのchildren treeが結合できます。
ASTを可視化
ここまで確認すると、どの木構造でも計算式を表しているようです。可視化にする流れは以下のようになります。
- まずは計算式を分解する
[5 * 4 / 2] + [3 * 6]
-
規則を定義する
- 5 * 4 は計算式なので、名前を
BinaryExpression
で表す。 - この計算中では、
5
,4
,*
の三つ要素があるので、5
と4
はIdentifier
で表す。 -
*
はオペレーターなので、Operator
で表す。
- 5 * 4 は計算式なので、名前を
-
5 * 4
コードで可視化してみる
BinaryExpression:
type: BinaryExpression
left: Identifier
type: Indentifier
value: 5
operator: *
right: Identifier
type: Identifier
value: 4
- ASTの確認できるツールがあります。AST Explorer ↓こんな感じです。
jscodeshift
でASTのfindをやってみる
- jscodeshiftをインストール
npm install jscodeshift
- 以下の依存packageを取得する
import * as express from 'express';
import chalk from 'chalk';
- index.jsを作成し、AST searchの処理を実装してみる
const shift = require('jscodeshift');
const data = `
import * as express from 'express';
import chalk from 'chalk';
`;
shift(data)
.find(shift.ImportDeclaration)
.forEach((path) => {
console.log(path.node.source.value);
});
- 実行する
node index.js
- 結果
express
chalk
- 配列の値を取得したい場合はどうする?
const value = ['foo', 'boo', 'hoge']
- 3番の手順と同じように
const shift = require('jscodeshift');
const data = `
const value = ['foo', 'boo', 'hoge'];
`;
shift(data)
.find(shift.ArrayExpression)
.forEach((path) => {
path.node.elements.forEach((element) => console.log(element.value));
});
- 結果
foo
boo
hoge
ASTのfindはできたので、editもやってみる
- 修正元を用意する
const data = `
import * as express from 'express';
import { deepClone } from 'lodash';
const value = ['foo', 'boo', 'hoge'];
`;
- 編集用のソースを用意する
const shift = require('jscodeshift');
const fs = require('fs');
const data = `
import * as express from 'express';
import { deepClone } from 'lodash';
const value = ['foo', 'boo', 'hoge'];
`;
const base = shift(data);
base.find(shift.ImportDeclaration, { source: { value: 'lodash' } })
.forEach((path) => {
const { specifiers } = path.node;
specifiers.forEach((specifier) => {
if (specifier.imported.name === 'deepClone') {
specifier.imported.name = 'chunk';
}
});
});
// ファイルを新たに出力するため、./sampleのフォルダを事前用意する
fs.writeFileSync('./sample/editedIndex.js', base.toSource(), 'utf-8');
- 出力した結果 editedIndex.js
import * as express from 'express';
import { chunk } from 'lodash'; // deepCloneがchunkに変更されました!
const value = ['foo', 'boo', 'hoge'];
ついでにASTのcreateをやってみる
- 修正元を用意する
// momentというパッケージもimportしたい
// deepClone の後ろに、chunkを追加したい
const data = `
import * as express from 'express';
import { deepClone } from 'lodash';
const value = ['foo', 'boo', 'hoge'];
`;
- 編集用のソースを用意する
const shift = require('jscodeshift');
const fs = require('fs');
const data = `
import * as express from 'express';
import { deepClone } from 'lodash';
const value = ['foo', 'boo', 'hoge'];
`;
const base = shift(data);
const momentImport = shift.importDeclaration(
[shift.importSpecifier(shift.identifier('moment'))],
shift.stringLiteral('moment')
);
base.find(shift.ImportDeclaration, { source: { value: 'lodash' } })
.insertBefore(momentImport);
.forEach((path) => {
const chunkImporter = shift.importSpecifier(shift.identifier('chunk'));
path.node.specifiers.push(chunkImporter);
});
fs.writeFileSync('./sample/editedIndex.js', base.toSource(), 'utf-8');
- 出力した結果 editedIndex.js
import * as express from 'express';
import { moment } from "moment";
import { deepClone, chunk } from 'lodash';
const value = ['foo', 'boo', 'hoge'];
最後にASTのdeleteもやってみる
- 修正元を用意する
// lodashの依存を消したい
const data = `
import * as express from 'express';
import { deepClone } from 'lodash';
const value = ['foo', 'boo', 'hoge'];
`;
- 編集用のソースを用意する
const shift = require('jscodeshift');
const fs = require('fs');
const data = `
import * as express from 'express';
import { deepClone } from 'lodash';
const value = ['foo', 'boo', 'hoge'];
`;
const base = shift(data);
base.find(shift.ImportDeclaration, { source: { value: 'lodash' } })
.forEach((path) => {
shift(path).replaceWith("");
});
// ファイルを新たに出力するため、./sampleのフォルダを事前用意する
fs.writeFileSync('./sample/editedIndex.js', base.toSource(), 'utf-8');
- 出力した結果 editedIndex.js
import * as express from 'express';
// lodashの依存が削除されました!
const value = ['foo', 'boo', 'hoge'];
おわりに
ここまで読んでいただいてありがとうございます!
弊社オプティマインドでは一緒に働く仲間を募集していますので、ご興味がある方はぜひ下記リンクをご覧ください!
Discussion