🖖

node.jsでASTの簡易CRUD操作

2022/12/21に公開
この記事はオプティマインド 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を可視化


ここまで確認すると、どの木構造でも計算式を表しているようです。可視化にする流れは以下のようになります。

  1. まずは計算式を分解する
[5 * 4 / 2] + [3 * 6]
  1. 規則を定義する

    1. 5 * 4 は計算式なので、名前をBinaryExpressionで表す。
    2. この計算中では、5,4,*の三つ要素があるので、54Identifierで表す。
    3. *はオペレーターなので、Operatorで表す。
  2. 5 * 4コードで可視化してみる

BinaryExpression:
 type: BinaryExpression
 left: Identifier
  type: Indentifier
  value: 5
 operator: *
 right: Identifier
  type: Identifier
  value: 4
  1. ASTの確認できるツールがあります。AST Explorer ↓こんな感じです。

jscodeshiftでASTのfindをやってみる


  1. jscodeshiftをインストール
npm install jscodeshift
  1. 以下の依存packageを取得する
import * as express from 'express';
import chalk from 'chalk';
  1. 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);
 });
  1. 実行する
node index.js
  1. 結果
express
chalk
  1. 配列の値を取得したい場合はどうする?
const value = ['foo', 'boo', 'hoge']
  1. 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));
 });
  1. 結果
foo
boo
hoge

ASTのfindはできたので、editもやってみる


  1. 修正元を用意する
const data = `
 import * as express from 'express';
 import { deepClone } from 'lodash';

 const value = ['foo', 'boo', 'hoge'];
`;
  1. 編集用のソースを用意する
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');
  1. 出力した結果 editedIndex.js
import * as express from 'express';
import { chunk } from 'lodash'; // deepCloneがchunkに変更されました!

const value = ['foo', 'boo', 'hoge'];

ついでにASTのcreateをやってみる


  1. 修正元を用意する
// momentというパッケージもimportしたい
// deepClone の後ろに、chunkを追加したい

const data = `
 import * as express from 'express';
 import { deepClone } from 'lodash'; 

 const value = ['foo', 'boo', 'hoge'];
`;
  1. 編集用のソースを用意する
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');
  1. 出力した結果 editedIndex.js
import * as express from 'express';
import { moment } from "moment";
import { deepClone, chunk } from 'lodash';

const value = ['foo', 'boo', 'hoge'];

最後にASTのdeleteもやってみる


  1. 修正元を用意する
// lodashの依存を消したい
const data = `
 import * as express from 'express';
 import { deepClone } from 'lodash';

 const value = ['foo', 'boo', 'hoge'];
`;
  1. 編集用のソースを用意する
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');
  1. 出力した結果 editedIndex.js
import * as express from 'express';
// lodashの依存が削除されました!

const value = ['foo', 'boo', 'hoge'];

おわりに


ここまで読んでいただいてありがとうございます!

弊社オプティマインドでは一緒に働く仲間を募集していますので、ご興味がある方はぜひ下記リンクをご覧ください!

https://recruit.optimind.tech/

Discussion