Android Lintによるコード解析ルールの策定(その1/2)

KotlinとJavaを一度に扱う場合

Balázs Ruda
Balázs Ruda

Follow

Sep 9, 2019 – 9 min read

TL;DR

この記事の最初の部分では、主に、コード規約を強制するためにカスタム コード解析ルールを書くことに時間を割く価値があることを示したいと思います。 次に、Checkstyle と ktlint に加えて、Android Lint も、その基本的な目的ではないにもかかわらず、フォーマット関連のコード解析ルールを作成する際に考慮すべきであることを示したいと思います。 他のツールに対する私のケースでの主な利点を説明し、この利点を提供する内部構造を説明します。

動機

自分のマージ リクエストにフォーマット関連のコメントがあったり、他の人のコメントにそのようなコメントを追加しなければならないときには、イライラします。 ときには、MR のたった 1 つのフォーマット エラーを修正するために、ブランチを変更しなければならないこともあり、かなりやる気を失います。

私たちは、グローバルな書式規則 (変数の順序、行の長さなど) を強制するために、ktlint や Checkstyle などのいくつかの lint ツールを使用していますが、これらにはデフォルトでは特定の全社的な規則が含まれていません。 そのような場合には、上記のツールはカスタムルールの追加もサポートしています。 私は、私たちが犯しがちなフォーマット上のミスのためのものを書くことにしました。

Missing empty line around “block statements” issue

私たちは常に、if、switch(Kotlinではwhen)、for、try、whileなどのブロックである「ブロックステートメント」の前後に二重の改行を必要としていますが、メソッドや他のブロックの最初か最後に正確に位置している場合は除きます。

この問題に最適なツールは何か

Kotlin用のktlintとJava用のCheckstyleが主要なフォーマットのコード解析ツールだと言えますが、今はまだ別のツールを選びます。 Android Lint(Androidプロジェクト用だけではありません)は、KotlinとJavaに同時に適用できるカスタムルールの記述もサポートしているからです。 Androidプロジェクトでは両方の言語を使用しており、同じルールを2回書いたり、同時にメンテナンスしたりする必要がないので、より論理的な選択だと思います。 言うまでもなく、ルールの統合も2回行う必要があります。

Why Java is still important at all

Android開発者として、私はJavaとKotlinの両方でコードを書かなければなりません。 しかし、Kotlinが登場してきて、コードベースでJavaの代わりにKotlinを使うことが多くなってきたので、もうJavaに注力する価値はないと言えます。 一方で、すでに本番稼動している大きなプロジェクトでは、移行はそれほど早く進んでいないと思います。

カスタム Lint ルールの書き方

カスタム コード解析ルールの書き方については、多くのチュートリアルや記事がありますので、この記事ではあまり詳しく説明しません。 しかし、もし興味があれば、異なるツールについてお勧めできるリンクをいくつか紹介します。 公式ドキュメントである Checkstyle に、ktlint と Android Lint に、Niklas Baudy 氏の素晴らしい中文記事です。

ASTの例(出典:Wikipedia about Abstract Syntax Tree)。

Android Lintと他の静的コード解析ツールの仕組み

当初、Android Lintは、主にAndroid特有の問題をJavaコードから発見するために作成されました。 Android Lintは、Javaコードを解析するために、ソースコードを木構造で表現したJava固有の抽象構文木(AST)を作成していました(詳しくはWikipediaをご覧ください)。 他の静的コード解析ツールでも、解析には言語固有のASTを使用します。 Java-, ktlint: Kotlin-, detekt: Kotlin-specific.

Android Lintが2つの言語を同時にチェックする方法

Android Lintと他のツールとの違いは、KotlinがAndroidのサポート言語になったことで、Android LintはJavaに対してすでに持っていたルールでKotlinもサポートするようになりました。 そのために、Universal Abstract Syntax Tree(JetBrains社開発)を導入し、KotlinとJavaの両方の言語に同時に適用できるコードのツリー表現を提供しており、言語固有のASTよりも高い抽象度を持っています。

UASTとASTは、どちらもソースコードに関する高レベルの詳細を提供します。 UAST と AST には、空白や中括弧に関する情報は含まれていませんが、Android 固有のルールを適用するには通常これで十分です。 以下にUASTの例を示します。

div

div

Android Lint APIで特定のUASTノードに対してasRecursiveLogString()メソッドを呼び出すことで、UASTを出力することができます。 UASTをプリントアウトすることができます。

UASTライブラリには、言語固有の式がすべて含まれていますが、それらの共通インターフェースも提供しています。 if式の例。

UIfExpressionの共通インターフェイスは、JavaUIfExpressionで実装されています。 JavaUTernaryExpression、およびKotlinUIfExpressionによって実装されています。

PSIツリー(Program Structure Interface Tree)

PSIツリーは、Android Lintの場合はUASTをベースに構築されており(他のツールの場合は言語固有のASTをベースに構築されています)、空白や中括弧などコードの構造に関するより詳細な要素を含むツリーです。

Android Lintでは、PSI ExpressionsはUAST Expressionsの実装に含まれており、UASTのノードとなっています。 例えば、org.jetbrains.kotlin.psi.KtIfExpressionはKotlinUIfExpressionからアクセスできます。

便利なintelliJプラグインもあります。 PSIツリーベースのコード解析ルールのデバッグを容易にするPsiViewerがあります。 同じコードスニペットを使った以下の例を見てください。2つの言語でツリー内の言語固有のトークンが異なることがわかります。

PSIツリーによるメソッドの表現

UASTとPSIツリーの両方を使用する

私のフォーマットルールを作成するには、UASTとPSIツリーの両方が必要です。 というのも、UAST は、フィルタリングして、両方の言語で興味のあるノードを一度に訪問するのに適していますが、前述したように、フォーマットに関する情報、たとえば、必要不可欠な空白の情報は提供されないからです。 しかし、前述したように、UASTはフォーマットに関する情報、例えば、私にとって不可欠なホワイトスペースの情報を提供しません。

Gradle プラグイン 3.4 および関連する Android Lint バージョン 26.4.0 から、Java だけでなく Kotlin でも、各ノードとその周辺の PSI ツリー表現を取得できるようになりました。

ルールの実装方法

まず、課題を作成する必要があり、スコープをJAVA_FILEに設定します。 (ここで、XMLやGradleファイルなど、他の種類のファイルも設定することができます。なぜなら、Android Lintはそれらもチェックすることができるからです。

カスタムIssueクラスの初期化

そして、検出器クラスでは。 Android LintがvisitForEachExpressionなどの関連するオーバーライドされたvisitメソッドを呼び出すべきことを知るために、getApplicableUastTypes関数内で興味のあるノードを列挙します。

Android Lintにチェックしたいノードを伝える方法

私のチェッカーメソッドでは、forwardパラメータで、「ブロック文」の前後の改行をチェックしたいかどうかを記述します。 メソッド本体では、訪問先のブロック文の周りにある空白の改行数を調べます。 そのために、主に3つのステップを行います。

  • まず、firstBlockLevelNodeメソッドで、空白をチェックしたい最初のブロックレベルノードを確認します。
値の代入における「ブロック文」

Lintはifキーワードの直前のホワイトスペースを調査しますが、私はvalキーワードの前のホワイトスペースに関心があります。

  • 2 番目に、firstRelevantWhiteSpaceNode メソッドで、最初の関連するホワイトスペース ノードが何かをチェックしますが、ここで改行をカウントする必要があります。 ときには、チェックすべき関連するホワイトスペースがない場合もあります。なぜなら、私のブロックがメソッドや他のブロックの最初または最後にある場合、それは問題ないので、それ以上の調査を見合わせることができます。
Single block statement in a method

この時点では、すでにPSIノードを使用しています。

“ブロックステートメント “の前にコメントがある場合

エッジケースとして、ブロックステートメントのすぐ上にコメントがある場合があります。

  • 最後に、関連する空白の中の改行の数を数え、それが 1 より大きくなければ問題を報告します。 また、lint gradle taskを実行すると、Lintレポートにこれらの警告が表示されます。 さらに、マージ要求ジョブをブロックしたい場合は、問題の深刻度をエラーに上げることもできます。

    div

    IDE warning before and after block statement

    Conclusion

    カスタムルールを統合した後は、このような問題は発生しなくなりました。 もうこのイライラする問題に集中する必要はありません。 代わりに、お互いのコードをレビューする際に、より複雑な問題を見つけることに脳力を使うことができます。

    Android Lint は主にフォーマット ルールのためのものではありませんが、私の場合は、Java と Kotlin の両方に使用できるので、非常に実用的でした。

    一方で、このルールは非常に単純なもので、PSI レベルで空白と中括弧をチェックするだけで、これらは 2 つの言語で同じであることに気づかなければなりません。 そのため、言語固有のコードを書く必要はありません。 しかし、もし言語固有のコードを書くことになった場合 (例えば、Kotlin の Elvis 演算子や Java の Ternary 演算子の処理など)、私は一対一の ktlint や Checkstyle ルールを書くよりも、Android Lint の方が良いと考えています。

    次のパート…

    記事の前編を気に入っていただけたなら、後編もチェックしてください。ここでは、Android (および非 Android) プロジェクトでの私のルールの統合、コードベースにすでに存在する問題の修正方法、およびルールのユニット テストとデバッグの方法について詳しく説明します。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です