Help us understand the problem. What is going on with this article?

DartのASTを触ってみる

More than 5 years have passed since last update.

頓挫したくないでござる。

3行でおk

  • dartの依存解決はimportpart+part ofでできてる
  • part "path/to/something.dart"; めんどい
  • DartのAST系のツールのanalyzer使って自動生成できるようにした

Dart歴的なの

ここ2週間ほど、Dartをさわさわして楽しんでます。つまりDart歴2週間です。
JSが駆逐されてDartがユーザーのブラウザ上で動く世界が来るかもしれないので(5年後くらいにもしかしたら・・・)、今からさわさわしててもいいんじゃね?的なノリでさわり始めたのですが、普段PHPとかJSとかメインでかいてるゆるふわ系言語エンジニアとしては、中々新鮮で、楽しめてます。

Dartについて

Dartについての紹介的なのは、きっとDart Advent Calendar 2014に何かしら書かれてたり、書かれたりすると思うのでそちらをご覧ください。
2013年のアドベントカレンダーと比較しても、随分盛り上がってますね!Dart!2015年にはDartが流行るかもですよ!

partなにこれ面倒くさい

ここから若干前振りが長くなりますが、Dartの依存関係の解決方法について説明します。

Dartの依存関係解決

Dartは非常にモジュール性が高い言語ですが、大きく2つの依存関係の解決方法が用意されています。

  • import
  • part

簡単にいうと、importは外部のライブラリ(package)の読み込みに使い、partは自分自身(package)の内部のファイルの解決に使われます。

詳しくはこのへんとか見てもらえればいいんですが、簡単におさらいしておきます。

something_libで理解するimport/part

something_libのファイル構造
/packages (依存してる外部パッケージ群)
    /lib1 (外部パッケージその1: lib1)
        /src
            foo.dart
            bar.dart
        lib1.dart
    /lib2 (外部パッケージその2: lib2[以下省略])
/lib (something_lib
    /src
        something_foo.dart
        something_bar.dart
    something.dart

何がしかライブラリ「something_lib」を作る場合、上記のようなディレクトリ構成になります。
ディレクトリ講師ついてはlaco0416さんのこちらの記事が大変勉強になります。

上記のようなパッケージの場合、編集するのはsomething_foo.dart something_bar.dart something.dart の3つになります。
something.dart がメインとなるファイルで、something_foo.dartsomething_bar.dartは、その構成要素としてのファイル、パーツとしてのファイルというニュアンスになります。

import

外部のパッケージは全て/packages以下に格納されるので、lib1lib2といった外部ライブラリを利用したい場合は、something.dartからlib1lib2import してあげます。

something.dart
library something; //パッケージ名宣言
import "packages:lib1/lib1.dart"; //lib1をインポート
void main() { //メインの処理
    //importしたライブラリが使えるようになる
    Lib1 lib1 = new Lib1();
}

このように、packages:パッケージ名/公開ライブラリファイル の形式でimportすると、外部ライブラリを読み込む事ができます。

初めてこれを見たときに気になったのが 「lib1読み込むっていってんだから/lib1.dartまで書くのが面倒くさいなりぃ」 だったんですが、これは、ひとつのパッケージの中から、必要なライブラリだけを読み込む事ができるようになっていると考えると、利点になります。
ひとつのpackageの中に複数のlibraryがあるわけですね。

例えば、GithubクライアントのDartパッケージであるgithub.dartを見ると、以下の画像のように、ひとつのパッケージの中に、「browser.dart(ブラウザ用公開ライブラリ)」と「server.dart(サーバーサイド用公開ライブラリ)」があり、利用用途に合わせて必要なファイルだけをimportする事ができるようになってます。

github.dart_lib_at_master_·_DirectMyFile_github.dart-7.png

ブラウザ用・サーバーサイド用はちょっと極端だとしても、巨大なパッケージの一部のライブラリだけを使う、みたいな事が比較的簡単にできるようになっている感じです。たぶん。

part

外部の場合はimportでしたが、自分自身のパッケージ内のファイルの依存関係の解決は partpart of と呼ばれる2つのキーワードで行います。それぞれ日本語に直すと

  • part
    • 私のパーツはこの子です
  • part of
    • 私はこの親のパーツです

こんな感じの事を宣言するシンタックスになってます。
コードを見たほうが分かりやすいと思います。

something.dart(親)
library something;

part "src/something_foo.dart";

void main () {
    Foo foo = new Foo();
}

src/something_foo.dart(子)
part of something;

class Foo() {
    // something
}

このように、それぞれがsomethingライブラリに属しながらも、親の方が「こいつが俺の子供だぜ!」って言う感じです。逆にいうと、いくら子の方が「part of!! part of!!」と叫んでも、親のほうでpartしてないとガン無視です。可哀想な子。

また、子のほうが複数の親に属する事もできないので注意です。
[error] Only one part-of directive may be declared in a file といった感じで怒られます。

import/partまとめ

  • import は外部packageを読み込む
  • partpart of で内部の依存関係を表す

つまりpartは面倒臭い

importpartが理解できたところで、partの面倒臭さに気づいた人とはお友達になれそうです。
慣れると対して面倒臭くもなくなってきたんですが、最初はガッツリ面倒くさいオーラを出しながらタイピングしてました。
つまり、子ファイルが増えれば増えるほど、part祭りになってくるわけです。

実際のコードを見たほうが理解してもらえそうな気がするので、また先ほどと同じくgithub.dartさんに登場してもらいましょう。common.dartという子ファイルを読み込んでるDartファイルがあるのですが、これを見ると、いかに面倒くさいかが伝わる気がします。

github.dart_common.dart_at_master_·_DirectMyFile_github.dart.png

最終的に73行目までpart 'hoge/hoge.dart';が続いてます。

ライブラリの規模として適切なのか?といった、設計の適切さみたいな話はひとまず置いておくにして、クラス毎にファイルを切り出すと、こんな感じになってしまいます。

というわけで 「こんなにpart書くのは面倒くさいなりぃ」 となってしまうわけです。

※ちなみにDartのパッケージをいくつか見てると、一つのファイルに複数のクラス置くのは別にお作法違反じゃない雰囲気もあります。

ならば自動生成だ

前振りが大分長くなってしまいましたが、書くのが面倒くさいなら自動生成すればいいじゃない!ということで、パッケージを書きました。

part_generator
https://pub.dartlang.org/packages/part_generator

Githubはこちら

使い方はREADMEにも書いてますが、Dartのパッケージ依存管理ツールであるところの pub を使ってインストール&コマンド実行します。

  • install
    • pub global activate part_generator
  • run
    • pub global run part_generator -r ライブラリのディレクトリルート -l ライブラリ名

何をやってくれるツールなのか

とりあえず作った程度なので大した機能は無いですが、-r で指定したディレクトリ以下の *.dart ファイルを探し、それらが -l で指定したライブラリ名の part of を持ってたら、part "そのファイルのパス.dart"; をコンソールに出力するだけの簡単なお仕事です。

ためしに part_generator 自身に実行してみると、こんな感じ。

1._takyam_mba-2____IdeaProjects_part_generator__zsh_.png

本当にコンソールに出力するだけなんですね。ええ。

DartのAST(Abstract Syntax Tree:抽象構文木)

で!ようやくタイトルのASTが出てきました。
前振りが長くてすみませぬ。

*.dartファイルの一覧取得はDart開発チームご謹製の glob パッケージを利用してサクッと取得できるのですが、part of ほげほげなファイルを取得するにはどうしたらよいか、といったあたりの話でようやくASTが出てきます。

ASTはご存知のとおり、テキストファイルでいらしゃるところのソースコードを、何かいい感じのアレにしてくれた状態のやつです。詳しくないからggrks。

で、ASTをDartのコードからいい感じに扱えるようにする analyzer パッケージがこれまたDart開発チームご謹製でありますので、これを使ってglobで取得した*.dartファイルから、part of ほげほげなものをピックアップしたいと思います。

analyzerでdirectiveを取得する

実際のコードはGithubを参照してください
lib/src/part_of_file.dart

簡略化したものが以下になります。

part_of_file.dart
import "package:analyzer/analyzer.dart" as analyzer;
//注: dartFIle は取得した*.dartのFileSystemEntity

//まずanalyzerでDartファイルをパースしてASTに変換
analyzer.CompilationUnit parsed = analyzer.parseDartFile(dartFile.path);

// directive を取得
analyzer.NodeList<analyzer.Directive> directives = parsed.directives;

//ほげほげライブラリのpart of のdirectiveを含むかどうかを確認する
bool isPartOf = directives.any((analyzer.Directive directive) {
  //まずは part of かどうかを確認
  if (!(directive is analyzer.PartOfDirective)) {
    return false;
  }
  //part ofで指定されてるライブラリ名が指定のものかどうか
  if ((directive as analyzer.PartOfDirective).libraryName.name != "ほげほげ") {
    return false;
  }
  return true;
});

上から順に見ていきます。

parseDartFile()
analyzer.CompilationUnit parsed = analyzer.parseDartFile(dartFile.path);

まず、parseDartFile()メソッドに*.dartのファイルパスを渡すと、Dartファイルをパースして、全てオブジェクトにしてくれます。やったね!

このとき、CompilationUnitが返ってきます。直訳するとコンパイル単位
このCompilationUnitdirectivesdeclarationsの2つのNodeListを持っていて、それぞれ、

  • directives
    • analyzerDirectiveのインスタンスのリストになってる
    • 先頭に書く系のシンタックス library/import/part/part of 等が該当
  • declarations
    • analyzerCompilationUnitのインスタンスのリストになってる
    • directives以外のクラスの宣言等が該当

みたいな感じになってます。
また、PartDirective/PartOfDirective/LibraryDirective/ImportDirectiveなどそれぞれが別のクラスになっている感じです。

PartOfDirectiveを確認する

part ofが含まれるファイルかどうかを見たい場合は、parseDartFile()でパースしたCompilationUnitdirectivesPartOfDirectiveが含まれているかどうかを確認すれば良いわけです。

partOfが含まれてるか確認
//anyはListにFuture(クロージャ)の返り値がtrueのものが含まれていればtrueを返すメソッド
bool isPartOf = directives.any((analyzer.Directive directive) {
  //まずは part of かどうかを確認
  if (!(directive is analyzer.PartOfDirective)) {
    return false;
  }
  //part ofで指定されてるライブラリ名が指定のものかどうか
  if ((directive as analyzer.PartOfDirective).libraryName.name != "ほげほげ") {
    return false;
  }
  return true;
});

directive is analyzer.PartOfDirective が該当箇所になります。
(※isinstance of的なやつです)

で、PartOfDirectivelibraryNameを持ってますので、あとは名前を比較してあげればいいわけです。

(directive as analyzer.PartOfDirective).libraryName.name != "ほげほげ" が該当箇所になります。
ちなみにこの.libraryNameLibraryIdentifierというクラスのインスタンスです。

以上の事をするだけで、ASTを利用してpart ofが書かれたファイルを取得する事ができました。すばらしい。

あとは、これで対象のファイル群を適当なリストに突っ込んで、パスを出力してあげるだけのツールがpart_generatorになってます。

AST周りまとめ

  • analyzer パッケージを使うと簡単にASTを扱える
  • コンパイル単位から、directiveとdeclarationに別れる
  • ひとつひとつがクラスになっているので、OO的に扱える

おわり

  • 頓挫せずにすんでよかった
  • Dart Teamご謹製パッケージがたくさんあって助かる
  • pub publish 叩くだけで何も考えずに(Githubすら不要)パッケージ公開できたすごい。
  • Dart日本ユーザー会とか無いんですかね
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした