【Golang】goa(ver 1.x)frameworkのlinter(goavl)を試作【go/astを利用】

前書き:DSLに半日悩み、カッとなって作った

goaは、DSLで記述されたデザインをもとに、Web APIホスティングに必要なベース処理(ルーティング、コントローラ、Swaggerなど)を生成するFrameworkです。goaを採用している会社の例は、DMM。goaを使うとコード記述量が減り、APIドキュメントが自動生成される利点があります。

私は、2022年1月からgoaを開発で使用するようになりました。goaは、DSLを覚えるコストが小さくはありません。DSLを書き間違えると、当然goa-designから各種ファイルの生成処理でエラーとなります。

ここでのエラー文章は簡潔(例:「パニックが起きた」)であり、プログラマに優しくありません。例えば、近年のコンパイラはユーザフレンドリーで、実装ミスをしている箇所を表示し、間違いの原因(推測)も出力します。goaはそのような情報を一切出力しません。この仕様は、goa初心者には辛いものです。ドキュメントやgoa-designコードを読み漁る時間が増えます。

 

この課題を解決するため、goa version1(フォーク版)のlinterとして、goavlの開発を始めました。開発目的は、goaを用いたWeb APIデザイン設計速度を早め、チーム内でのgoa-designの差異を極力小さくする事です。

goavlは、goa version 1 のみをサポートし、現行のversion 3 はサポートしない予定です。その理由は、フォークしたgoa version 1 を私が利用しているからです。何故、フォーク版を使用しているかの背景は、他のサイトで説明しています(記事の作者とgoavl開発者は別人のため注意)

 

本記事では、「goavlをどのように機能を用いて実装したかの説明(ザックリ説明)」と「goavlの基本機能」について説明します。

              

GolangはASTを作る標準パッケージが存在

linter(静的解析ツール)を作る場合は、ソースコードの中身を解析する必要があります。

コンパイラを自作した経験がある方は、「字句解析器(lexer、tokenizer)や構文解析器(parser)を作るの大変だな」と考えるかもしれません(私は新卒で入社した会社で、構文解析器を上手く作れなかった苦い経験があります。雑な設計では動きません)。

Golangでは、標準パッケージgo/astがAST(構文解析木)を作成してくれます。そのため、lexerやparserを自作せずにすみます。さらに、ASTを可視化する関数ast.Print() が用意されており、ASTを目視確認しながら実装を進められます。これが本当に便利。

どれぐらい便利かというと、目的の機能をlinterに導入するのに要した時間が10〜12時間ぐらいでした。go/astやast.Print() がなければ、10倍以上の時間が必要だったと思います。

          

ASTの出力例

以下のコードで、ASTを出力できます。

 

上記ファイル自体をast.Print()した結果は、以下の通りです(約250行)。

0行目に書かれているast.Fileノードは、goファイル(1個)のASTを表す構造体です。ast.File構造体には、パッケージ名やインポートPATH、変数や関数の宣言/実装が全て代入されています。ここからお目当ての変数や関数を探し出す作業は、以下のast.Print()結果を見ながら行います。

ast.Print()結果には、Package、Name、Specsなどの変数名が書かれており、変数の内容も合わせて出力されています。この情報を見ながら、ノードを辿る作業を行います。

       

ast.Fileノードを愚直に辿るとどうなるか

ast.Print()の結果を見ながら、愚直にast.File構造体を辿っていくと、あなたのエンジニア人生で見た事がない深さのネストコードが拝めます。そして、この実装方法は間違えています。

 

愚直にast.File構造体を辿っても、パースしたファイルによってast.Fileの内容は変わります。そのため、愚直に書いたコードは汎用性がありません。

通常は、ast.Inspect()を使用してast.Fileの内容を探索します。ast.Inspect()は、探しているノード位置(型)をswitch-caseに記載しておくと、その位置で探索が止まります(case文に入ります)。ここでの型はプリミティブ型ではなく、go/ast内で定義されている型(ASTを表現するための型)です。

以下、「愚直にコードを書いた場合」と「ast.Inspect()を使った場合」の例です。後者はもっとネストを浅く書けますが、この時の実力では以下が限界でした。

       

より詳細なAST情報が欲しい方への参考文献(日本語)

           

goavlの設計

goavlは限定的な用途のツールと言っても差し支えないので、簡単な作りにしました。より正確に言うと、既存のLinterのプラグインとして開発してもマージされない可能性が高かったので、自分で管理できる範囲のLinterとして設計しました。

goavlは、以下の2つを機能として持ちます。

  • Task(チェック項目)のランナー
  • ASTを用いたTask(チェック項目)

 

Taskのランナーは、非常にシンプルな作りです。まず、Task構造体の中に関数ポインタ(Check)を持ち、このポインタに文法チェック関数を登録します。ランナーは関数ポインタを順番にコールするだけです。優先度も何もありません。

ASTを用いたTaskは、例えば「命名規則が正しいか」といった観点(1個単位)で作ります。多くのLinterがこの方針を採用していると思われます。今回実装したTaskは、goa-designに関する知識がある方でないと理解できないので、説明を省略します。

         

goavlの使い方

インストール方法は、README(日本語)を参照してください。goavlは、引数指定が無い場合はカレントディレクトリ以下のgoa-designファイルをチェックし、–fileオプションをつけた場合は任意のgoa-designファイルをチェックします。

以下、実行例です。

現状は、「命名規則チェック」や「関数の使用条件チェック」、「APIや属性の説明が書かれているかのチェック」を行います。指摘箇所(ファイル、行数)と直し方を併記しているため、goavlを使えばgoa-designファイルの修正が容易にできる筈です。

          

最後に:goavlの由来

初期名称は”goalinter-v1″でした。

末尾の”v1″がlinterのVersion 1に見えるため、”goav1linter”に改名。その後、”1″と”l”が似ていたため、それらを統合する事を思いつきました。

その結果、 “goavl”が誕生しました。goavlは「ゴアブル」と呼んでいます。

     

 おまけ:2022年に作成したGolang製コマンド一覧  

【Golang】2022年に開発した自作CLIコマンド/ライブラリに対する所感と宣伝【OSS】

おすすめ

1件の返信

  1. 2022年2月11日

    […] 【Golang】goa(ver 1.x)frameworkのlinter(goavl)を試作【go/astを利用】 […]