LoginSignup
5
1

More than 1 year has passed since last update.

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

Posted at

はじめに

みなさんDart書いてますか。

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

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

ASTとcodemodとは

AST

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

DartコードをASTモデルに変換すると、Dartコードが細かいノードに分けられて木構造のような形に構造化されます。1つ1つのノードはAstNodeのサブクラスになっています。

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

codemod

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

実践

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

dependencies:
  codemod: 1.0.2

公式のExample

公式の紹介している一番簡単そうな例を見ていってcodemodを使ってどのように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コードを修正する動作をするコードがここです。

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で指定できます。

void main(List<String> args) async {
  exitCode = await runInteractiveCodemod(
    filePathsFromGlob(Glob('license_header_fixtures/**.dart')),
    licenseHeaderInserter,
    args: args,
  );
}

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

AST Visitorを使って楽に修正

先程の例でFileContextsourceTextからソースファイル内の全コードを探索すれば修正していけそうな気配がします。
しかし、該当の自分が修正したい箇所を頑張って正規表現を使って見つけるのは大変です。
そこで、AST Visitorを使えば楽にソースコードを探索できます。

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

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

まとめ

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

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

Links

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1