DartのASTとcodemodを使って機械的なリファクタリングを自動化

# はじめに
みなさんDart書いてますか。

「プロジェクト発足時にあったコーディングルールで開発してきたけど、最近そこからリファクタリングしたい箇所があってリファクタリングルールは決まっていて単純だけどファイルの量が多すぎて大変」みたいなことありませんか。

今回は大規模なFlutter規模で役立つリファクタリングのためのpackageやAPIの紹介です。

# ASTとcodemodとは
## AST
ASTとは、Abstract Syntax Treeの略で、Dartのコードを構造化してモデル化したときの名称です。

DartコードをASTモデルに変換すると、Dartコードが細かいノードに分けられて木構造のような形に構造化されます。1つ1つのノードは[AstNode](https://pub.dev/documentation/analyzer/latest/dart_ast_ast/AstNode-class.html)のサブクラスになっています。

さらに詳しい情報は公式ドキュメントに記載されているので読んでみてください。

https://pub.dev/documentation/analyzer/latest/dart_ast_ast/dart_ast_ast-library.html

## codemod
コードの修正やリファクタリングを自動化するコードを書けるようにするpackageです。このpackageではASTをコードのリファクタリング目的に応用しています。

https://pub.dev/packages/codemod

# 実践
pub.devのcodemodのページから `pubspec.yaml`に依存関係を追加すると使えます。

```yaml
dependencies:
  codemod: 1.0.2
```

## 公式のExample
公式の紹介している一番簡単そうな例を見ていってcodemodを使ってどのようにDartコードを自動的に修正するかみていきましょう。
こちらのコードを見ていきます。

https://github.com/Workiva/dart_codemod/blob/master/example/license_header_inserter.dart

この例ではソースファイルの先頭にライセンスヘッダーがなければ自動的に追加していくという例です。
ライセンスヘッダーはこんな感じのものです。

```dart
// Copyright 2019 Workiva Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
```

実際にDartコードを修正する動作をするコードがここです。

```dart
Stream<Patch> licenseHeaderInserter(FileContext context) async* {
  // Skip if license header already exists.
  if (context.sourceText.trimLeft().startsWith(licenseHeader)) return;

  yield Patch(
    // Text to insert.
    licenseHeader,
    // Start offset.
    // 0 means "insert at the beginning of the file."
    0,
    // End offset.
    // Using the same offset as the start offset here means that the patch
    // is being inserted at this point instead of replacing a span of text.
    0,
  );
}
```

`FileContext`を通して`sourceText`からソースファイル内のコードをStringで取得しています。
そこですでに挿入しようとしているライセンスヘッダーがなければ `yield Patch`を使って指定したoffsetにライセンスヘッダーを挿入しています。

実際に起動させるときは、こんな感じで修正するプロジェクト内のディレクトリの範囲を`Glob`で指定できます。
```dart
void main(List<String> args) async {
  exitCode = await runInteractiveCodemod(
    filePathsFromGlob(Glob('license_header_fixtures/**.dart')),
    licenseHeaderInserter,
    args: args,
  );
}
```

codemodではこのような感じで、ソースコード内を探索して手を加えたい場所を検知したらそこに `yield Patch`を使って実際に手を加えるということをやってDartコードを修正するコードを書いていきます。

## AST Visitorを使って楽に修正
先程の例で`FileContext`の`sourceText`からソースファイル内の全コードを探索すれば修正していけそうな気配がします。
しかし、該当の自分が修正したい箇所を頑張って正規表現を使って見つけるのは大変です。
そこで、[AST Visitor](https://pub.dev/documentation/analyzer/latest/dart_ast_ast/AstVisitor-class.html)を使えば楽にソースコードを探索できます。

公式にも例が載せられています。

```dart
import 'package:analyzer/analyzer.dart';
import 'package:codemod/codemod.dart';

class DeprecatedRemover extends GeneralizingAstVisitor<void>
    with AstVisitingSuggestor {
  static bool isDeprecated(AnnotatedNode node) =>
      node.metadata.any((m) => m.name.name.toLowerCase() == 'deprecated');

  @override
  void visitDeclaration(Declaration node) {
    if (isDeprecated(node)) {
      // Remove the node by replacing the span from its start offset to its end
      // offset with an empty string.
      yieldPatch('', node.offset, node.end);
    }
  }
}
```

例では、ソースコード内に`@Deprecated`などがつけられていてDeprecatedになった宣言を見つけて、その宣言を空文字で置き換えて削除するという修正を自動化しています。

具体的には、宣言している箇所をASTVisitorが用意している `visitDeclaration`をオーバーライドして検知してその宣言がDeprecatedだったらという感じで修正したい箇所を見つけ出しています。

ASTVisitorには他にも色々なメソッドが用意されていて、ソースコード内の色々な部分を下端に検知できて可能性は無限大です。
気になったらぜひ覗いてみて下さい。

https://pub.dev/documentation/analyzer/latest/dart_ast_ast/AstVisitor-class.html


# まとめ
個人の趣味で開発しているアプリや、発足してまだ間もなくコーディングルールなどががっちり決まっていない場合、登場する場面はないかもしれませんが、大規模なFlutterプロジェクトなどで昔の書き方からある一定のルールに基づいて全体的にリファクタリングしたいとき絶大な効果を発揮すると思います。

まずは公式の例にもあった、ライセンスヘッダーをつけるところからなど始めて見ると楽しさがわかるかもしれません。

# Links
- https://pub.dev/documentation/analyzer/latest/dart_ast_ast/dart_ast_ast-library.html
- https://pub.dev/packages/codemod
- https://pub.dev/documentation/analyzer/latest/dart_ast_ast/AstNode-class.html
- https://pub.dev/documentation/analyzer/latest/dart_ast_ast/AstVisitor-class.html