More than 1 year has passed since last update.

この記事は、Android Advent Calendar 2015 22日目の記事です。
2日も過ぎてしまい大変申し訳ございません…:confounded::confounded::confounded:

前日は、@shirajiさんのAnnotation Processing(apt)のまとめ+AndroidAnnotations(AA)とAutoValueのサンプルコード書いてみた。でした。


かなりマイナーネタですが、lintを拡張してオリジナルのルールを作成する方法に関してご紹介します。

普段アプリ開発をしていると必要になる事はほぼ無いと思いますが、ライブラリを提供している場合、少しトリッキーなインタフェースだったりすると標準のlintで警告が出てしまったりするのでそうした問題の解消に役立ちます。
また、チーム開発をしていて静的検査の項目を追加したい場合にも便利だと思います。

How to develop

lintのカスタムルールを作成する為には、大きく以下の3ステップを踏む必要があります。

  • Detectorを作成
  • Issueを作成
  • Registryを作成

まずは、プロジェクトを作成しましょう。

lintはPC上で動作させる為、Androidプロジェクトではなく純粋なJavaプロジェクトとして作成します。
ビルドシステムにGradleを採用している場合、以下の記述をbuild.gradleに追加します。

build.gradle
apply plugin: 'java'

repositories {
    jcenter()
}

dependencies {
    compile 'com.android.tools.lint:lint-api:24.3.1'
    compile 'com.android.tools.lint:lint-checks:24.3.1'
}

jar {
    manifest {
        // define your registry
        attributes("Lint-Registry": "com.example.google.lint.MyIssueRegistry")
    }
}

defaultTasks 'assemble'

lint-apiとlint-checksという2種類のjarをdependenciesに追加しています。
jarタスクの内部でMyIssueRegistryというクラスを定義していますが、これは後程説明します。

Detector

まず、detectorというクラスを作成します。このクラスは、実際にissueをレポートする為のロジックを定義しする為のクラスです。

例として、特定のCustomViewを利用する際にはあるattributeの利用を必須としたいケースを考えます。

package googleio.demo;

import java.util.Collection;
import java.util.Collections;

import org.w3c.dom.Element;

import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.ResourceXmlDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.XmlContext;

public class MyDetector extends ResourceXmlDetector {
    public static final Issue ISSUE = Issue.create(
            "MyId",
            "My brief summary of the issue",
            "My longer explanation of the issue",
            Category.CORRECTNESS, 6, Severity.WARNING,
            new Implementation(MyDetector.class, Scope.RESOURCE_FILE_SCOPE));

    @Override
    public Collection<String> getApplicableElements() {
        return Collections.singletonList(
                "com.google.io.demo.MyCustomView");
    }

    @Override
    public void visitElement(XmlContext context, Element element) {
        if (!element.hasAttributeNS(
                "http://schemas.android.com/apk/res/com.google.io.demo",
                "exampleString")) {
            context.report(ISSUE, element, context.getLocation(element),
                    "Missing required attribute 'exampleString'");
        }
    }
}

まず、このクラスはResourceXmlDetectorというクラスを拡張しています。このクラスはレイアウトやStringリソース等のXMLファイルを検査する際に用いられるdetectorです。
detectorは他にもJavaのソースコードやバイトコードを検査する為の物が用意されており、こちらから確認する事ができます。

getApplicableElementsメソッドは、このdetectorが検査の対象とすべきXMLタグを返却します。
ここでは、MyCustomViewのFQDNを指定しています。そうすると、lintはMyCustomViewタグの検出後、visitElementメソッドを呼びます。

visitElementメソッド内では、第二引数のelementに属性が含まれているかを検査し、無い場合はreportで警告を発生させます。第一引数のcontextには様々な情報が含まれており、プロジェクトの情報(minSdkVersionやtargetSdkVersion)やXMLのpathを取得する事も可能です。

Issue

context.reportメソッドの第一引数は、Issueでした。Issueとはdetectorによって検出される警告を表現するクラスです。

public static final Issue ISSUE = Issue.create(
    "MyId",
    "My brief summary of the issue",
    "My longer explanation of the issue",
    Category.CORRECTNESS, 6, Severity.WARNING,
    new Implementation(MyDetector.class, Scope.RESOURCE_FILE_SCOPE));

引数が大量にあり紛らわしいですが、以下の様になっています。

  • ID...このissueを識別する為に利用されます。短く叙述的である必要があります。
  • Summary...一行に収まる文字数で、問題の概要を記載します。IDEのlint UIでは原則のこのsummaryが利用されます。
  • Explanation...Summaryよりも長い文字数で、問題が何なのかを詳細に説明します。この説明はlintのHTMLレポートにて利用されます。
  • Category…Categoryは、ネストさせる事ができ(ex) Usability > Icons)、問題のフィルタリングやソートに役立ちます。また、コマンドラインから特定のtypeのlintのみを走らせる事もできます。"correctness"や"performance"など、既にAPIに定義されている値を利用します。
  • Priority…1から10までの整数を定義する事ができ、10が最も深刻なレベルを表します。issueをソートする際に利用されます。
  • Severity...fatal, error, warning, ignoreという4つのデフォルト値を定義できます。この値は、ユーザーがlint.xmlによって上書きする事ができます。
  • Detector class...先程実装したDetectorクラスを指定します。Detectorはlintのrun毎に生成されるので、状態のcleanup等について気を配る必要はありません。
  • Scope...このissueがどのスコープに適用されるかを指定します。こちらに一覧があるので適当なものを選択してください。

Registry

既存のIssueはBuiltinIssueRegistryというクラスに集約されていますが、カスタムルールを定義した場合は、独自にRegistryクラスを定義してやる必要があります。

package googleio.demo;

import java.util.Arrays;
import java.util.List;

import com.android.tools.lint.client.api.IssueRegistry;
import com.android.tools.lint.detector.api.Issue;

public class MyIssueRegistry extends IssueRegistry {

    public MyIssueRegistry() {
    }

    @Override
    public List<Issue> getIssues() {
        return Arrays.asList(
            MyDetector.ISSUE
        );
    }
}

getIssuesメソッドで、作成したIssueを返却するようにします。Registryはlintによってインスタンス化される際に引数なしコンストラクタを利用する為、引数なしコンストラクタ定義は必須です。

Run lint

以上で、カスタムルールの作成は終了になりますが、実際に実行するにはもう一手間必要になります。
上記のプロジェクトから生成したjarを.android/lint/に配備する必要があります。

ちなみに、いちいちcpするのは手間なので、build.gradleに以下の様なタスクを定義しておくといいかもしれません。

task install(type: Copy, dependsOn: build) {
    from configurations.lintChecks
    into System.getProperty('user.home') + '/.android/lint/'
}

lintは、以下の順番で.androidフォルダを探索していきます。

  • ANDROID_SDK_HOME (system prop or environment variable)
  • user.home (system prop)
  • HOME (environment variable)

その為、ANDROID_SDK_HOME下に.androidが存在している場合は、user.homeは探索されないので注意してください。

配備が完了すると、lintコマンドで実行できるようになります。

$ lint --show MyId
MyId
----
Summary: My summary of the issue

Priority: 6 / 10
Severity: Warning
Category: Correctness

My longer explanation of the issue
$ lint --check MyId /demo/workspace/Demo

Scanning Demo: ...............
res/layout/sample_my_custom_view.xml:20: Warning: Missing required attribute 'exampleString' [MyId]
    <com.google.io.demo.MyCustomView
    ^
0 errors, 1 warnings

Jenkinsを使っている場合…

Android Lint Pluginを利用している場合は$ANDROID_LINT_JARSという環境変数でjarを指定してあげれば良いそうです。

もっと複雑なRuleを作りたい…

既存のソースを読んで勉強しましょう。

Link