OpenRewrite・ast-grep・LibCSTで実践する大規模自動リファクタリング
レガシーコードの改善といえば手作業でのリファクタリングを想像しがちですが、数百ファイル・数万行規模のコードベースでは手作業は現実的ではありません。本記事では、AST(抽象構文木)ベースの自動リファクタリングツールを言語別に比較し、実際のコードモディフィケーション(codemod)を書いて適用するまでの手順を解説します。
この記事でわかること
- AST(抽象構文木)ベースの自動リファクタリングが手作業と比べてどう優れているか
- Java向けOpenRewrite、多言語対応ast-grep、Python向けLibCSTの使い分け
- 各ツールで実際にcodemodを書き、大規模コードベースに適用する手順
- Martin Fowlerが提唱するcodemod合成パターンによるテスト駆動リファクタリング
- AI支援リファクタリングツールとASTベースツールを組み合わせる実践的な戦略
対象読者
- 想定読者: レガシーコードの改善に取り組む中級者のソフトウェアエンジニア
-
必要な前提知識:
- Python・Java・JavaScript/TypeScriptのいずれかの基礎文法
- Gitの基本操作(ブランチ、差分確認)
- リファクタリングの基本概念(関数の抽出、変数のリネームなど)
MLEの方へ: ASTは自然言語処理における構文解析木(parse tree)と同じ概念です。コードを木構造に変換し、パターンマッチで変換対象を特定する仕組みは、NLPの構文変換と同じ発想で理解できます。本記事のPythonコード例はMLパイプラインのリファクタリングにもそのまま応用できます。
結論・成果
ASTベースの自動リファクタリングツールを導入することで、大規模コードベースの改善を効率化できます。cronn社の報告では、OpenRewriteを用いたJavaプロジェクトのSpring Bootマイグレーションで手動3ヶ月の作業を3日に短縮した事例があります(cronn社ブログ)。また、Rector(PHP向け)の公式サイトによると、PHP 8.0→8.5へのアップグレードで同様に3ヶ月→3日の効率化が報告されています(Rector公式)。
ただし、これらのツールは「正規表現による置換」の上位互換であり、ビジネスロジックの理解を伴うリファクタリング(責務の分離、アーキテクチャ変更など)には向きません。構文的に定義可能な変換に適用範囲が限定される点を理解した上で活用することが重要です。
ASTベース自動リファクタリングの基本を理解する
手作業のリファクタリングや正規表現による置換と、ASTベースの変換はどう違うのでしょうか。まずその基本的な仕組みを見ていきます。
なぜ正規表現では不十分なのか
テキストベースの検索置換は、コードの構造を理解しません。たとえば、setTimeout の第2引数を変更したい場合、正規表現では文字列中の setTimeout やコメント内のものまで誤って置換してしまう危険があります。
# bad_example.py - 正規表現ベースの置換の問題
import re
code = '''
setTimeout(callback, 1000) # 変換したい
x = "setTimeout(callback, 1000)" # 文字列中 - 変換してはいけない
# setTimeout(callback, 1000) # コメント中 - 変換してはいけない
'''
# 正規表現は構造を理解しない
result = re.sub(r'setTimeout\((\w+), 1000\)', r'setTimeout(\1, 2000)', code)
# → 文字列中やコメント中も誤って置換される!
ASTベースのツールはコードを木構造に変換してから操作するため、「関数呼び出し」「文字列リテラル」「コメント」を正確に区別できます。
AST変換の3ステップ
ASTベースの自動リファクタリングは、どのツールでも以下の3ステップで動作します。
- Parse(解析): ソースコードをAST(木構造)に変換
- Transform(変換): ASTノードにパターンマッチし、対象ノードを書き換え
- Print(出力): 変換後のASTをソースコードに書き戻し(空白・コメント保持)
この「Lossless(非破壊的)」な変換が重要です。通常のASTではコメントや空白が失われますが、自動リファクタリングツールはCST(Concrete Syntax Tree)やLST(Lossless Semantic Tree)を使い、元のフォーマットを維持したまま変換します。
主要ツールの比較
2026年現在、言語ごとに成熟したツールが揃っています。
| ツール | 対象言語 | AST基盤 | 特徴 | GitHubスター |
|---|---|---|---|---|
| OpenRewrite | Java, Kotlin, XML, YAML | LST(Lossless Semantic Tree) | 型情報を含むセマンティック解析、レシピエコシステム | 2,400+ |
| ast-grep | 多言語(30+) | tree-sitter | Rust製で高速、宣言的YAML定義 | 8,000+ |
| jscodeshift | JavaScript, TypeScript | recast + Babel | Facebook開発、豊富なコミュニティレシピ | 9,400+ |
| LibCST | Python | 独自CST | Instagram開発、Python 3.0〜3.14対応 | 1,600+ |
| Rope | Python | 独自AST | IDE統合(VS Code、Vim)、セマンティック解析 | 1,900+ |
| Rector | PHP | PHP-Parser | PHP 5.3→8.5対応、フレームワークマイグレーション | 9,600+ |
なぜこの分類か: ASTベースのツールは、解析対象の言語に依存するため、言語ごとに専用のエコシステムが発展してきました。多言語対応のast-grepは比較的新しく(2023年登場)、tree-sitterの普及とともに急速に採用が広がっています。
注意: 上記のスター数は2026年3月時点の概算です。ツール選定ではスター数だけでなく、レシピの充実度・コミュニティの活発さ・自社の言語スタックとの適合性で判断してください。
Java/KotlinプロジェクトをOpenRewriteで自動変換する
OpenRewriteはJavaエコシステムで最も成熟した自動リファクタリングツールです。Spring Bootのメジャーバージョンマイグレーションやフレームワーク移行で広く使われています。
OpenRewriteの仕組み
OpenRewriteの特徴は**LST(Lossless Semantic Tree)**を使うことです。通常のASTよりも多くの情報を保持しています。
-
型情報: 変数やメソッドの完全修飾型名(
java.util.Listなど) - フォーマット: 空白、インデント、コメントの完全な保持
- セマンティクス: メソッドのオーバーロード解決、ジェネリクスの型推論
この情報があるため、「javax.persistence パッケージのインポートを jakarta.persistence に変更する」といった意味を理解した変換が可能です。
Spring Boot 3.x → 4.0 マイグレーションの実行
実際にOpenRewriteを使って、Spring Bootのマイグレーションを実行してみましょう。
<!-- pom.xml にOpenRewriteプラグインを追加 -->
<build>
<plugins>
<plugin>
<groupId>org.openrewrite.maven</groupId>
<artifactId>rewrite-maven-plugin</artifactId>
<version>5.50.0</version>
<configuration>
<exportDatatables>true</exportDatatables>
<activeRecipes>
<recipe>org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0</recipe>
</activeRecipes>
</configuration>
<dependencies>
<dependency>
<groupId>org.openrewrite.recipe</groupId>
<artifactId>rewrite-spring</artifactId>
<version>5.28.0</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
# レシピの実行(ドライラン)
mvn rewrite:dryRun
# 変換の実行
mvn rewrite:run
# 変更差分の確認
git diff
mvn rewrite:run を実行すると、OpenRewriteが以下の変換を自動で行います。
-
javax.*→jakarta.*パッケージの移行 - 非推奨APIの置き換え
-
application.propertiesの設定キー更新 - モジュラースターターへの移行
カスタムレシピの作成
既存レシピで対応できない社内固有のパターンには、カスタムレシピを作成します。たとえば、非推奨の社内ユーティリティメソッドを新しいAPIに置き換えるレシピです。
// ReplaceDeprecatedUtilRecipe.java
package com.example.recipes;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.tree.J;
public class ReplaceDeprecatedUtilRecipe extends Recipe {
@Override
public String getDisplayName() {
return "Replace deprecated StringUtil.isEmpty with Strings.isBlank";
}
@Override
public String getDescription() {
return "社内ライブラリ StringUtil.isEmpty() を "
+ "標準ライブラリ Strings.isBlank() に置換します。";
}
@Override
public JavaIsoVisitor<ExecutionContext> getVisitor() {
return new JavaIsoVisitor<>() {
@Override
public J.MethodInvocation visitMethodInvocation(
J.MethodInvocation method, ExecutionContext ctx) {
J.MethodInvocation m = super.visitMethodInvocation(method, ctx);
// StringUtil.isEmpty(x) → Strings.isBlank(x) に変換
if (m.getSimpleName().equals("isEmpty")
&& m.getSelect() != null
&& m.getSelect().toString().equals("StringUtil")) {
// インポートの更新
maybeAddImport("com.example.common.Strings");
maybeRemoveImport("com.example.legacy.StringUtil");
// メソッド呼び出しの書き換え
return m.withName(m.getName().withSimpleName("isBlank"))
.withSelect(
new J.Identifier(
m.getSelect().getId(),
m.getSelect().getPrefix(),
m.getSelect().getMarkers(),
java.util.Collections.emptyList(),
"Strings",
null, null
)
);
}
return m;
}
};
}
}
注意点:
OpenRewriteのカスタムレシピはJavaで書く必要があります。レシピ自体のテストには
org.openrewrite:rewrite-testモジュールが提供するRecipeSpecを使い、変換前後のコードをアサーションで検証してください。宣言的YAML定義で既存レシピを組み合わせる方が手軽なので、まずはYAMLベースの合成レシピから始めることをお勧めします。
# rewrite.yml - 宣言的レシピの合成例
type: specs.openrewrite.org/v1beta/recipe
name: com.example.MigrateToNewApi
displayName: Migrate to New API
description: 社内APIのv1→v2マイグレーション
recipeList:
- org.openrewrite.java.ChangeMethodName:
methodPattern: "com.example.legacy.StringUtil isEmpty(..)"
newMethodName: isBlank
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: com.example.legacy.StringUtil
newFullyQualifiedTypeName: com.example.common.Strings
JavaScript/TypeScriptプロジェクトをast-grepで高速変換する
JavaScript/TypeScriptの世界では、Facebookが開発したjscodeshiftが長年の標準でしたが、2025年以降はast-grepが急速に普及しています。Rust製で高速に動作し、tree-sitterベースで30以上の言語に対応する点が魅力です。
ast-grepの基本操作
ast-grepはコマンドラインから直接パターンマッチと置換を実行できます。
# インストール
npm install -g @ast-grep/cli
# または
cargo install ast-grep
# 基本的な検索(console.log の呼び出しを検索)
ast-grep --pattern 'console.log($$$ARGS)' --lang typescript src/
# 検索+置換(console.log → logger.info に一括変換)
ast-grep --pattern 'console.log($$$ARGS)' \
--rewrite 'logger.info($$$ARGS)' \
--lang typescript src/
$$$ARGS は可変長メタ変数で、任意の数の引数にマッチします。ast-grepのパターンは実際のコードとほぼ同じ見た目で書けるため、正規表現より直感的です。
YAMLルールによる宣言的codemod
複雑な変換はYAMLルールファイルで定義します。以下は、非推奨のReact componentDidMount をuseEffectフックに変換するガイドを検出するルールです。
# rules/react-lifecycle-to-hooks.yml
id: deprecated-componentDidMount
language: tsx
rule:
kind: method_definition
has:
kind: property_identifier
regex: "componentDidMount"
inside:
kind: class_body
inside:
kind: class_declaration
fix: |
// TODO: このcomponentDidMountをuseEffectに移行してください
// useEffect(() => { /* 元のcomponentDidMount処理 */ }, []);
$$$MATCH
message: "componentDidMount は非推奨です。useEffect への移行を検討してください。"
severity: warning
# ルールの実行(検出のみ)
ast-grep scan --rule rules/react-lifecycle-to-hooks.yml src/
# ルールの実行(自動修正付き)
ast-grep scan --rule rules/react-lifecycle-to-hooks.yml --update-all src/
jscodeshift との使い分け
ast-grepとjscodeshiftはどちらもAST操作ツールですが、得意な領域が異なります。
| 比較項目 | ast-grep | jscodeshift |
|---|---|---|
| 実装言語 | Rust | JavaScript |
| 処理速度 | 高速(大規模向き) | 中程度 |
| パターン定義 | 宣言的YAML / CLI | JavaScript API |
| 対応言語 | 30+言語 | JavaScript/TypeScript |
| 学習コスト | 低(パターン=コード) | 高(AST APIの理解必要) |
| 複雑な変換 | 制限あり(JavaScript拡張で対応) | 自由度が高い |
| コミュニティ | 急成長中 | 成熟(Facebook由来) |
使い分けの指針:
- ast-grep: 単純なパターン置換、複数言語への横断適用、高速なlint的利用
- jscodeshift: 複雑なロジックを含む変換(条件分岐、複数ノードの組み合わせ)
jscodeshift の後継として、Codemod社が**jssg(JavaScript ast-grep)**を2025年10月に発表しました(Codemod Blog)。jssgはjscodeshift互換のAPIをast-grepエンジン上で動作させるもので、既存のjscodeshiftのcodemodeを高速化できます。ただし2026年3月時点ではまだ初期リリース段階のため、本番導入には十分な検証が必要です。
jscodeshift による複雑な変換の例
ast-grepでは難しい、複数ノードを組み合わせた変換をjscodeshiftで実装してみましょう。Feature Toggleの除去を例にします。
// remove-feature-toggle.ts - jscodeshift transform
import type { API, FileInfo } from "jscodeshift";
const TOGGLE_NAME = "feature-new-product-list";
export default function transform(file: FileInfo, api: API) {
const j = api.jscodeshift;
const root = j(file.source);
// featureToggle('feature-new-product-list') ? A : B → A に変換
root
.find(j.ConditionalExpression, {
test: {
type: "CallExpression",
callee: { name: "featureToggle" },
arguments: [{ value: TOGGLE_NAME }],
},
})
.forEach((path) => {
// 条件式を consequent(true側)の値で置き換え
j(path).replaceWith(path.node.consequent);
});
// 使われなくなったインポートの除去
root
.find(j.ImportDeclaration, {
source: { value: "./featureToggle" },
})
.forEach((path) => {
// featureToggle が他で使われていなければ削除
const usages = root.find(j.Identifier, { name: "featureToggle" });
if (usages.length === 0) {
j(path).remove();
}
});
return root.toSource({ quote: "single" });
}
# jscodeshift の実行
npx jscodeshift --transform remove-feature-toggle.ts \
--extensions ts,tsx \
--parser tsx \
src/
ハマりポイント: jscodeshift は --parser tsx を指定しないとJSXを含むファイルでパースエラーが発生します。TypeScript + JSX のプロジェクトでは --extensions ts,tsx --parser tsx を必ず指定してください。
PythonプロジェクトをLibCST・Ropeで改善する
Python向けの自動リファクタリングにはLibCST(Instagram開発)とRopeの2つが主要な選択肢です。LibCSTはcodemod(大規模一括変換)に、RopeはIDE統合のインタラクティブなリファクタリングに向いています。
LibCSTでcodemodを書く
LibCSTはCST(Concrete Syntax Tree)を使い、コメントや空白を保持したまま変換を行います。以下は、非推奨の typing.Optional[X] を X | None(Python 3.10+のUnion型記法)に変換するcodemodです。
# optional_to_union.py - LibCST codemod
import libcst as cst
from libcst import matchers as m
class OptionalToUnionTransformer(cst.CSTTransformer):
"""typing.Optional[X] → X | None に変換する。
Python 3.10+ の Union 型記法を使用します。
"""
def leave_Subscript(
self, original_node: cst.Subscript, updated_node: cst.Subscript
) -> cst.BaseExpression:
# Optional[X] にマッチ
if not m.matches(
updated_node,
m.Subscript(
value=m.OneOf(
m.Name("Optional"),
m.Attribute(
value=m.Name("typing"),
attr=m.Name("Optional"),
),
),
),
):
return updated_node
# Optional[X] → X | None に変換
if len(updated_node.slice) == 1:
inner_type = updated_node.slice[0].slice
if isinstance(inner_type, cst.Index):
inner_type = inner_type.value
return cst.BinaryOperation(
left=inner_type,
operator=cst.BitOr(
whitespace_before=cst.SimpleWhitespace(" "),
whitespace_after=cst.SimpleWhitespace(" "),
),
right=cst.Name("None"),
)
return updated_node
def transform_file(source_code: str) -> str:
"""ファイル単位で変換を実行する。"""
tree = cst.parse_module(source_code)
transformer = OptionalToUnionTransformer()
modified_tree = tree.visit(transformer)
return modified_tree.code
if __name__ == "__main__":
import sys
from pathlib import Path
for filepath in sys.argv[1:]:
path = Path(filepath)
original = path.read_text()
modified = transform_file(original)
if original != modified:
path.write_text(modified)
print(f"Modified: {filepath}")
else:
print(f"No changes: {filepath}")
# 単体実行
python optional_to_union.py src/models/*.py
# LibCST の組み込みコマンドで一括実行
python -m libcst.tool codemod optional_to_union.OptionalToUnionTransformer src/
変換前後の例:
# Before
from typing import Optional
def get_user(user_id: int) -> Optional[User]:
...
def process(data: Optional[list[str]] = None) -> Optional[dict]:
...
# After
def get_user(user_id: int) -> User | None:
...
def process(data: list[str] | None = None) -> dict | None:
...
Ropeによるセマンティックリファクタリング
Ropeは単なる構文変換ではなく、変数のスコープ解析や参照追跡を行うセマンティックリファクタリングが特徴です。VS Code(python-rope拡張)やVim(ropevim)から利用できますが、スクリプトとしても使えます。
# rope_rename.py - Ropeで一括リネーム
from rope.base.project import Project
from rope.refactor.rename import Rename
# プロジェクトを開く
project = Project(".")
try:
# 対象ファイルのリソースを取得
resource = project.get_resource("src/services/user_service.py")
# ファイル内容を読み込み、リネーム対象のオフセットを特定
source = resource.read()
# "get_user_data" メソッドの出現位置(オフセット)
offset = source.index("get_user_data")
# リネームリファクタリングを実行
renamer = Rename(project, resource, offset)
# 変更セットを取得(プレビュー)
changes = renamer.get_changes("fetch_user_profile")
# 影響範囲の確認
print(f"影響ファイル数: {len(changes.get_changed_resources())}")
for res in changes.get_changed_resources():
print(f" - {res.path}")
# 変更を適用
project.do(changes)
print("リネーム完了")
finally:
project.close()
LibCST vs Ropeの使い分け:
| 用途 | LibCST | Rope |
|---|---|---|
| 大規模一括変換(codemod) | 得意 | 不向き |
| 変数・関数のリネーム | 構文のみ | スコープ考慮 |
| IDE統合 | なし | VS Code, Vim |
| Python型情報の活用 | なし | 部分的に対応 |
| カスタム変換の作成 | Visitor パターン | Refactoring API |
制約: Ropeは動的な属性アクセス(
getattr、メタクラスなど)を完全には追跡できません。Pythonの動的機能を多用するコードベースでは、リネーム後にテストで確認することが必須です。
codemod合成パターンでテスト駆動リファクタリングを実践する
Martin Fowlerのサイトで解説されているcodemod合成パターン(Refactoring with Codemods to Automate API Changes)は、大規模な変換を小さなステップに分解する手法です。MLパイプラインでのデータ変換を小さなステップに分割するのと同じ発想です。
合成パターンの基本構造
1つの大きな変換を、独立してテスト可能な小さな変換に分割します。
テスト駆動でcodemodを開発する
codemod自体をテスト駆動で開発することで、意図しない変換を防ぎます。以下はPytestを使った例です。
# test_optional_to_union.py
import pytest
from optional_to_union import transform_file
class TestOptionalToUnion:
"""Optional[X] → X | None 変換のテスト。"""
def test_simple_optional(self):
"""基本的な Optional[str] を変換する。"""
source = "def f(x: Optional[str]) -> Optional[int]: ..."
expected = "def f(x: str | None) -> int | None: ..."
assert transform_file(source) == expected
def test_nested_optional(self):
"""Optional[list[str]] のようなネストした型を変換する。"""
source = "def f(x: Optional[list[str]]) -> None: ..."
expected = "def f(x: list[str] | None) -> None: ..."
assert transform_file(source) == expected
def test_typing_prefix(self):
"""typing.Optional 形式も変換する。"""
source = "def f(x: typing.Optional[str]) -> None: ..."
expected = "def f(x: str | None) -> None: ..."
assert transform_file(source) == expected
def test_no_change_for_union(self):
"""既に X | None 形式のものは変更しない。"""
source = "def f(x: str | None) -> None: ..."
assert transform_file(source) == source
def test_no_change_for_non_optional(self):
"""Optional 以外の Subscript(List[str] 等)は変更しない。"""
source = "def f(x: List[str]) -> None: ..."
assert transform_file(source) == source
def test_string_literal_untouched(self):
"""文字列中の Optional は変換しない。"""
source = 'x = "Optional[str] is deprecated"'
assert transform_file(source) == source
# テストの実行
uv run pytest test_optional_to_union.py -v
よくある間違い: codemodのテストで「変換されるべきケース」だけを書き、「変換されないべきケース」(ネガティブテスト)を省略するのは危険です。Martin Fowlerの記事でも、ネガティブテストを先に書くことが推奨されています。意図しない変換は、静かにバグを埋め込むため発見が遅れがちです。
段階的な適用戦略
大規模コードベースへの適用は、一気にではなく段階的に行います。
# Step 1: ドライラン(変更プレビュー)
ast-grep --pattern 'console.log($$$ARGS)' \
--rewrite 'logger.info($$$ARGS)' \
--lang typescript src/ \
--debug-query # マッチ結果のみ表示
# Step 2: 影響範囲の限定(特定ディレクトリのみ)
ast-grep --pattern 'console.log($$$ARGS)' \
--rewrite 'logger.info($$$ARGS)' \
--lang typescript src/services/
# Step 3: テスト実行で回帰確認
npm test
# Step 4: 残りに拡大
ast-grep --pattern 'console.log($$$ARGS)' \
--rewrite 'logger.info($$$ARGS)' \
--lang typescript src/
# Step 5: 全テスト実行
npm test
AI支援ツールとASTベースツールを組み合わせる
2026年現在、GitHub CopilotやAmazon Q DeveloperといったAI支援リファクタリングが普及しています。GitHub社の2025年開発者調査によると、プロの開発者の78%がAIコーディングアシスタントを日常的に使用しています(Java Code Geeks)。
ASTベースのツールとAI支援ツールは競合するものではなく、補完関係にあります。
役割分担の指針
| リファクタリングの種類 | ASTベースツール | AI支援ツール |
|---|---|---|
| APIの一括マイグレーション | 得意(決定論的) | 不向き(一貫性が保証できない) |
| パターンの検出と警告 | 得意 | 得意 |
| 意味を理解した変数名改善 | 不向き | 得意 |
| 複雑なロジックの整理 | 不向き | 得意(提案ベース) |
| フレームワーク移行 | 得意(レシピあれば) | 部分的に対応 |
| テストコードの生成 | 不向き | 得意 |
トレードオフ: ASTベースのツールは決定論的(同じ入力に同じ出力)であり、数千ファイルに対して一貫した変換を保証できます。一方、AI支援ツールは確率的であり、ファイルごとに異なる結果になる可能性があります。大規模な一括変換にはASTベースツール、個別ファイルの知的なリファクタリングにはAI支援ツールという使い分けが実践的です。
組み合わせワークフロー
以下は、レガシーコードの改善で両方のツールを組み合わせるワークフローです。
# 実践例: レガシーなconsole.logの段階的移行
# Phase 1: ast-grep で機械的に変換可能なパターンを一括置換
ast-grep --pattern 'console.log($$$ARGS)' \
--rewrite 'logger.info($$$ARGS)' \
--lang typescript src/
# Phase 2: AI に残りの複雑なケースを相談
# (条件分岐内のログレベル判定、エラーハンドリング内のログなど)
# → AI支援ツールで個別ファイルごとに適切なログレベルを提案
注意: AI支援ツールの出力は必ず人間がレビューしてください。特にセキュリティに関わるコード(認証、暗号化、入力バリデーション)のリファクタリングでは、AIの提案をそのまま採用するのは危険です。ASTベースのツールは入力と出力が明確に定義されるため、この点では安全性が高いといえます。
よくある問題と解決方法
| 問題 | 原因 | 解決方法 |
|---|---|---|
| OpenRewrite実行後にコンパイルエラー | レシピが型情報を完全に解決できないケース |
mvn rewrite:dryRun で事前確認。手動修正が必要な箇所をTODOコメントで出力する |
| ast-grepのパターンがマッチしない | コードのフォーマットの違い(改行・空白) |
$$$ メタ変数で柔軟にマッチ。--debug-query で解析結果を確認 |
| LibCSTの変換でコメントが消える | CST操作でコメントノードを意図せず除去 |
leave_* メソッドで original_node のコメントを保持する実装を追加 |
| jscodeshift実行時にパースエラー | JSXを含むファイルで --parser tsx 未指定 |
--parser tsx --extensions ts,tsx を必ず指定 |
| Ropeのリネームで一部参照が残る | 動的属性アクセス(getattr等)の追跡不可 | 変換後に全テスト実行 + grep で残存参照を確認 |
| codemod適用後にテストが壊れる | 変換がテストコード自体も変更した | テストディレクトリを除外して先に適用し、テストは後から手動更新 |
まとめと次のステップ
まとめ:
- ASTベースの自動リファクタリングは、構文的に定義可能な変換を大規模に一括適用する手法です。正規表現では不可能な安全な変換が可能になります
- 言語ごとにツールが充実しています:Java→OpenRewrite、JS/TS→ast-grep(+jscodeshift)、Python→LibCST(+Rope)、PHP→Rector
- codemod合成パターン(小さなステップに分解)とテスト駆動でcodemodを開発することで、意図しない変換を防げます
- AI支援ツールとは補完関係にあり、「一括変換はAST、知的リファクタリングはAI」の使い分けが効果的です
- ただし、ビジネスロジックの理解を伴うリファクタリング(責務分離、アーキテクチャ変更)はこれらのツールの対象外です
次にやるべきこと:
- 自分のプロジェクトの言語スタックに合ったツールを選び、公式チュートリアルを試す
- 小さなcodemod(非推奨APIの置換など)をテスト駆動で書いてみる
- CI/CDパイプラインにcodemodのドライランを組み込み、技術的負債の蓄積を継続的に検出する
関連記事: アーキテクチャレベルのレガシーコード改善パターン(Strangler Fig、Branch by Abstraction等)については、別記事「Strangler Fig・Sprout/Wrapで進めるレガシーコード改善の実践ガイド」で詳しく解説しています。また、技術的負債の定量化と返済ロードマップについては「SonarQube+CodeSceneで技術的負債を定量化し返済ロードマップを設計する実践ガイド」も参考にしてください。
参考
- OpenRewrite 公式ドキュメント
- ast-grep 公式サイト
- Facebook jscodeshift - GitHub
- Instagram LibCST - GitHub
- python-rope/rope - GitHub
- Rector 公式サイト
- Martin Fowler - Refactoring with Codemods to Automate API Changes
- cronn - Using OpenRewrite for large-scale refactoring
- Codemod Blog - Announcing JavaScript ast-grep (jssg)
- Hypermod - Comparing ast-grep and jscodeshift
- Java Code Geeks - AI-Assisted Coding in 2026
注意: この記事はAI(Claude Code)により自動生成されました。内容の正確性については複数の情報源で検証していますが、実際の利用時は公式ドキュメントもご確認ください。