SonarQubeのプラグイン作成(その1)

# 目的

SonarQubeのプラグインを作成してみます。

参考: [sonar-custom-plugin-example](https://github.com/SonarSource/sonar-custom-plugin-example)

# 必要なもの

括弧内は試したバージョンになります。なお、JDKやMavenの準備については説明しません。

* JDK (1.8)
* Maven (3.6.1)
* SonarQube (7.9.1) この手順ではdockerで環境構築しているが環境は問わない

# 準備: SonarQube 環境構築

## SonarQubeサーバ起動

動作確認に使用するSonarQubeはdockerで構築します。

まずは docker-compose.yml を任意のディレクトリに作成します。これは以下を参考に作りました。
https://github.com/SonarSource/docker-sonarqube/blob/master/recipes/docker-compose-postgres-example.yml

上記例のdocker-compose.ymlとは異なりトップのvolumesを削除して、ボリュームの記述を相対パスに変えてます。これはカレントディレクトリに永続化したディレクトリを作成したいからです。

```docker-compose.yml
version: "2"

services:
  sonarqube:
    image: sonarqube
    ports:
      - "9000:9000"
    networks:
      - sonarnet
    environment:
      - sonar.jdbc.url=jdbc:postgresql://db:5432/sonar
    volumes:
      - ./sonarqube_conf:/opt/sonarqube/conf
      - ./sonarqube_data:/opt/sonarqube/data
      - ./sonarqube_extensions:/opt/sonarqube/extensions

  db:
    image: postgres
    networks:
      - sonarnet
    environment:
      - POSTGRES_USER=sonar
      - POSTGRES_PASSWORD=sonar
    volumes:
      - ./postgresql:/var/lib/postgresql
      # This needs explicit mapping due to https://github.com/docker-library/postgres/blob/4e48e3228a30763913ece952c611e5e9b95c8759/Dockerfile.template#L52
      - ./postgresql_data:/var/lib/postgresql/data

networks:
  sonarnet:
    driver: bridge
```

docker-compose.yml を作成したディレクトリで以下を実行します。

```console
docker-compose up -d
```

これでしばらくすると、 http://localhost:9000/ でSonarQubeにアクセスできます。
うまくいかない場合は、以下でログを参照しエラーがないかを確認してください。

```console
docker-compose logs
```

正常に起動すると以下の画面が開きます。画面右上の「Log in」から、ユーザ名admin,パスワードadminでログインしてください。

<img width="1258" alt="スクリーンショット 2019-10-21 15.23.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/919a33df-1b4a-74e4-12a0-f2cf20f89b95.png">

ログイン後の画面

<img width="1265" alt="スクリーンショット 2019-10-21 15.32.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/6507d52e-be73-73b5-d77b-416e5214d269.png">

任意のプラグインをダウンロードしておきます。以降、javaプログラムの解析を例に説明するので、
SonarJavaの「Install」をクリックします。

<img width="1269" alt="スクリーンショット 2019-10-21 15.51.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/f2d6861b-df4f-0cb9-e046-d23955c95324.png">

<img width="1262" alt="スクリーンショット 2019-10-21 16.04.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/e3b99e97-1ce0-61e3-241b-dda2923bceb3.png">

完了したら「Restart Server」をクリックします。

<img width="1267" alt="スクリーンショット 2019-10-21 16.38.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/27fb2357-3931-ad38-6bab-a480e7e5fd1e.png">


なお、この時点でカレントディレクトリの`sonarqube_extensions/downloads/` にプラグインファイルがダウンロードされます。

```console
ls sonarqube_extensions/downloads/
sonar-java-plugin-5.14.0.18788.jar
```

再起動後は、`sonarqube_extensions/plugins/` にこのファイルは移動しています。

```console
ls sonarqube_extensions/plugins/
sonar-java-plugin-5.14.0.18788.jar
```

以降はこの `sonarqube_extensions/plugins` ディレクトリにプラグインのjarファイルが残っている限りは個別のインストールは不要になります。他にプラグインが必要であれば任意に追加してください。解析対象の言語以外では `sonar-scm-git-plugin` はあった方が良いでしょう。

## SonarScanner 準備

サーバの準備ができたら解析対象の準備になります。
まずは以下から自分の環境にあったSonarScannerをダウンロードします。

https://docs.sonarqube.org/latest/analysis/scan/sonarscanner/

ここでは、"Mac OS X 64" をクリックしてダウンロードされる `sonar-scanner-cli-4.2.0.1873-macosx.zip` を使って説明します。

ダウンロードした sonar-scanner のzipファイルを任意の場所に展開し、binディレクトリをPATHに追加します。
ここでは、docker-compose.ymlを置いたカレントディレクトリに展開したとして説明します。

```
unzip ~/Download/sonar-scanner-cli-4.2.0.1873-macosx.zip

PATH=$PWD/sonar-scanner-4.2.0.1873-macosx/bin:$PATH
```

解析対象のプログラムを適当に準備します。
sample ディレクトリを作成し、以下のファイルを作ってください。

```sample/Foo.java
public class Foo {
  String hello = "Hello, world!\n";

  public static void main(String[] args) {
    Integer a = null;
    if (a != null) {
      System.out.println("Hello, world!\n" + a);
    }
  }
}
```

```sample/sonar-project.properties
sonar.projectKey=helloworld
sonar.projectName=Hello
sonar.projectVersion=1.0

sonar.sources=.
sonar.java.binaries=.
sonar.sourceEncoding=UTF-8
sonar.java.source=1.6
```

解析対象のソースとsonar-project.propertiesが準備できたら sample ディレクトリで sonnar-scanner を実行します。(sonar-scannerはサーバと通信するので、SonarQubeが起動している事は確認しておいてください)

```console
cd sample
sonnar-scanner
```

sonar-scanner がうまく行けば最後に以下の出力になります。

```console

INFO: ------------------------------------------------------------------------
INFO: EXECUTION SUCCESS
INFO: ------------------------------------------------------------------------
INFO: Total time: 6.865s
INFO: Final Memory: 6M/24M
INFO: ------------------------------------------------------------------------
```

sonar-scannerが完了した後でも、サーバではバックグラウンドタスクが実行中の場合があります。
これが完了するのを待つ必要があります。(上記サンプル程度ならすぐに完了します。)

<img width="1269" alt="スクリーンショット 2019-10-21 16.56.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/712df824-d654-4c28-e60d-5a0a0208b905.png">

以下の通り、解析が完了しました。

<img width="1274" alt="スクリーンショット 2019-10-21 17.00.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/3a41977e-a7f2-2c7b-ee2f-6ffe8a0a81a5.png">

<img width="1260" alt="スクリーンショット 2019-10-21 17.01.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/6aa27d9a-f226-b3c6-911c-a2673f7f0d70.png">

<img width="1274" alt="スクリーンショット 2019-10-21 17.02.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/cfcbd6b3-e1c9-8716-891d-4c86cab629b4.png">

<img width="1274" alt="スクリーンショット 2019-10-21 17.02.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/eea21663-b832-7577-91a0-9d4ff76cb408.png">

# プラグイン作成

最終的に作成するプラグインはあらかじめ用意した正誤表をもとに誤表記をチェックするものにします。

例えば、以下のような正誤表にしたがって左の語句が出てきたらエラーにするというものです。
このようなチェックで問題となるのは誤判定です。誤判定をいつまでも報告し続けてもゴミになり、そのゴミのせいで本来検出したい誤記が目立たなくなります。そのゴミを SonarQubeの機能により訂正できるなら便利なのではないかと考えたのがこのプラグイン作成の動機になってます。

誤 | 正
---|----
タイポ | タイプ
昨日 | 機能

ただし、この記事の進め方としては、プラグインの作成の仕方を説明するために順番に機能を追加する形で説明をしていきます。

まず、最初のステップとして Java ソースにチェックルールを追加するプラグインを作成します。
また作成するルールの第一段階目では(参考にした`sonar-custom-plugin-example`に倣って)先頭の1行目が必ずissueになる限定的な機能になっています。

さて、プラグインを作成するには以下を決めておく必要があります。

項目      |値
---------|-----
pluginKey| errata
artifactId | sonar-errata-plugin
packaging | sonar-plugin (必ずこれを指定します)
バージョン  | 0.0.1
パッケージ,groupId  | org.jca02266.sonarplugins.errata
pluginClass | org.jca02266.sonarplugins.errata.ErrataPlugin
name | Errata Plugin for SonarQube 7.9.x LTS
description | Check eratta for some text files
sonar.apiVersion | 7.1
jdk.min.version | 1.8

pluginKeyさえ決まれば、(パッケージ, name, descriptionは適当にしてますが)自ずと決まります。なおerrataとは正誤表のことです。

[Create a Maven Project](https://docs.sonarqube.org/latest/extend/developing-plugin/)のpom.xmlの例では artifactId を `sonar-{key}-plugin` という名前にすることを推奨されているのでそれに従っています。

packaging は通常、`jar`などを指定する箇所ですが、ここは `sonar-plugin` 固定です。

## 最初のプラグイン

まずは、Maven Project作成にあたって、以下のpom.xmlを準備します。
前項で決めた内容にしたがって決まった通りに記述します。

```sonar-errata-plugin/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.jca02266.sonarplugins.errata</groupId>
  <artifactId>sonar-errata-plugin</artifactId>
  <packaging>sonar-plugin</packaging>
  <version>0.0.1</version>

  <name>Errata Plugin for SonarQube 7.9.x LTS</name>
  <description>Check eratta for some text files</description>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <sonar.apiVersion>7.1</sonar.apiVersion>
    <jdk.min.version>1.8</jdk.min.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.sonarsource.sonarqube</groupId>
      <artifactId>sonar-plugin-api</artifactId>
      <version>${sonar.apiVersion}</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.sonarsource.sonar-packaging-maven-plugin</groupId>
        <artifactId>sonar-packaging-maven-plugin</artifactId>
        <version>1.18.0.372</version>
        <extensions>true</extensions>
        <configuration>
          <pluginKey>errata</pluginKey>
          <pluginClass>org.jca02266.sonarplugins.errata.ErrataPlugin</pluginClass>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.5.1</version>
        <configuration>
          <source>${jdk.min.version}</source>
          <target>${jdk.min.version}</target>
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>
```

### プラグインの入り口

pom.xmlで、pluginClass に指定したクラスがプラグインの入り口になります。
以下のファイルを準備します。

```src/main/java/org/jca02266/sonarplugins/errata/ErrataPlugin.java
package org.jca02266.sonarplugins.errata;

import org.jca02266.sonarplugins.errata.rules.CreateIssuesOnTextFilesSensor;
import org.jca02266.sonarplugins.errata.rules.TextRulesDefinition;
import org.sonar.api.Plugin;

/**
 * This class is the entry point for all extensions. It is referenced in pom.xml.
 */
public class ErrataPlugin implements Plugin {

  @Override
  public void define(Context context) {
    context.addExtensions(TextRulesDefinition.class, CreateIssuesOnTextFilesSensor.class);
  }
}
```

作成するクラスは、Plugin インタフェースの実装になります。唯一必要なのは、defineメソッドで、
`context.addExtensions()`でプラグインの機能を追加します。

`context.addExtension()`(sなしの単数形）も用意されており、以下のように必要な数だけ呼び出す形で利用します。どちらを使っても同じです。

```
    context.addExtension(TextRulesDefinition.class);     
    context.addExtension(CreateIssuesOnTextFilesSensor.class);
```

大事なことは用意した機能はこの `addExtension` または `addExtensions` で機能を追加しなければならない(そうでなければSonarQubeに認識されない)ということです.

### リポジトリとルール

TextRulesDefinition.class というのは以下になります。
ここでは、ルールの集まりを表す「リポジトリ」を作成し、そこに一つ一つのルールを追加します。
(この例では追加するルールは一つだけです)

```src/main/java/org/jca02266/sonarplugins/errata/rules/TextRulesDefinition.java
package org.jca02266.sonarplugins.errata.rules;

import org.sonar.api.rule.RuleKey;
import org.sonar.api.rule.RuleStatus;
import org.sonar.api.rule.Severity;
import org.sonar.api.server.rule.RulesDefinition;

public class TextRulesDefinition implements RulesDefinition {

  public static final String REPOSITORY = "text-errata";
  public static final String LANGUAGE = "java";
  public static final RuleKey RULE = RuleKey.of(REPOSITORY, "errata");

  @Override
  public void define(Context context) {
    NewRepository repository = context.createRepository(REPOSITORY, LANGUAGE).setName("Check for text files");

    NewRule rule = repository.createRule(RULE.rule())
      .setName("Errata")
      .setHtmlDescription("Check errata for all text files")
      .setTags("errata")
      .setStatus(RuleStatus.READY)
      .setSeverity(Severity.MINOR);

    rule.setDebtRemediationFunction(rule.debtRemediationFunctions().linearWithOffset("0h", "1min"));

    repository.done();
  }
}
```

大事なのは

```java
context.createRepository(REPOSITORY, LANGUAGE)
```

でリポジトリを作り、

```java
public static final RuleKey RULE = RuleKey.of(REPOSITORY, "errata");
```

でリポジトリにルールを追加している部分になります。あとはそれぞれの属性値になります。

後の手順で確認しますが、このプラグインを組み込むことで以下のようにリポジトリとルールが追加されていることが確認できます。

<img width="1269" alt="スクリーンショット 2019-10-21 18.26.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/d84d5476-04e4-5d07-58ee-4f6b5f4945f6.png">

<img width="1278" alt="スクリーンショット 2019-10-21 18.42.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/bfa209a2-05bf-ad9c-e655-cbbdd7b77495.png">

### センサー

CreateIssuesOnTextFilesSensor.class のソースは以下になります。

```src/main/java/org/jca02266/sonarplugins/errata/rules/CreateIssuesOnTextFilesSensor.java
package org.jca02266.sonarplugins.errata.rules;

import org.sonar.api.batch.fs.FileSystem;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.sensor.Sensor;
import org.sonar.api.batch.sensor.SensorContext;
import org.sonar.api.batch.sensor.SensorDescriptor;
import org.sonar.api.batch.sensor.issue.NewIssue;
import org.sonar.api.batch.sensor.issue.NewIssueLocation;

public class CreateIssuesOnTextFilesSensor implements Sensor {

  private static final double ARBITRARY_GAP = 1.0;

  @Override
  public void describe(SensorDescriptor descriptor) {
    descriptor.name("Check errata for text files");

    descriptor.onlyOnLanguage("java");
    descriptor.createIssuesForRuleRepositories(TextRulesDefinition.REPOSITORY);
  }

  @Override
  public void execute(SensorContext context) {
    FileSystem fs = context.fileSystem();
    Iterable<InputFile> textFiles = fs.inputFiles(fs.predicates().all());
    for (InputFile textFile : textFiles) {
      NewIssue newIssue = context.newIssue()
        .forRule(TextRulesDefinition.RULE)
        .gap(ARBITRARY_GAP);

      NewIssueLocation primaryLocation = newIssue.newLocation()
        .on(textFile)
        .at(textFile.selectLine(1))
        // .at(textFile.newRange(textFile.newPointer(1, 10), textFile.newPointer(2,5)))
        .message("Fix typo");
      newIssue.at(primaryLocation);

      newIssue.save();
    }
  }
}
```

上記のように、インタフェース Sensor を実装したクラスを用意することで、SonarScannarの実行に応じてルールの適用を試みます。

具体的には、execute()メソッドにあるように以下のステップで検出した問題を登録します。

* newIssue ルール(ここではTextRulesDefinition.RULE)を生成する
* そのルールに適合するソースを検出したらNewIssueLocationを生成する
* newIssue.at()でその検出箇所を登録する
* newIssue.save()で問題を登録する

`newIssue.at()` に対して、`textFile.selectLine(1)`を渡していますが、これは対象ファイルの1行目を表しています。
コメントにしている`textFile.newRange(textFile.newPointer(1, 10), textFile.newPointer(2,5))`であれば、1行目の10文字目から2行目の5文字目までの意味になります。問題を検出する処理を作った場合に、これらのメソッドで検出した場所を示します。

メソッド describe() では、以下の処理を行なっています。

```
    descriptor.onlyOnLanguage("java");
    descriptor.createIssuesForRuleRepositories(TextRulesDefinition.REPOSITORY);
```

これは以下の意味になっています。

* 対象ファイルが java 言語の場合にのみセンサーを実行する
* 指定したリポジトリ(TextRulesDefinition.REPOSITORY)のルールが一つもactiveでなければセンサーを実行しない

## コンパイルとプラグインの登録

作成したプラグインは以下のコマンドでコンパイルします。

```
$ mvn clean package
```

これにより、targetディレクトリに`sonar-errata-plugin-0.0.1.jar`が生成されます。
このファイルを最初の方で説明したplugin格納ディレクトリにコピーします。

```console
$ ls target/sonar-errata-plugin-0.0.1.jar
target/sonar-errata-plugin-0.0.1.jar
$ cp target/sonar-errata-plugin-0.0.1.jar ../sonarqube_extensions/plugins/ 
```

そして、SonarQubeを再起動します。Web画面からAdministration→System→Restart Serverでも良いのですが、dockerの再起動でも大丈夫です。

```
$ cd ..
$ docker-compose restart
Restarting sonarqube_sonarqube_1 ... done
Restarting sonarqube_db_1        ... done
```

## ルールを有効にする

追加したルールを有効にするにはいくつかステップが必要になります。

まず、Quality Profiles を選びます。そうすると、(現在 sonar-java-plugin のプラグインを登録しているので、) Java 向けに Sonar way プロファイルが表示されています。
この行の歯車マークをクリックすると Compare, Copy, Extend のプルダウンが表示されます。

※ ここで、Compare しか表示されない場合は admin でログインできていません。サーバの再起動によりセッションの状態がおかしくなっている可能性があるので、ログインしている場合でもLogout→再ログインを試してみてください。

<img width="1276" alt="スクリーンショット 2019-10-22 9.01.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/5826fbc9-8e40-b49f-24bc-ff5bcdc2fa26.png">

Copyを選びます。(Extendでも良いです。Extendの場合は元のProfileを継承したProfileを作成します)

<img width="492" alt="スクリーンショット 2019-10-22 9.07.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/a2740844-cb4e-c8d0-9ef9-5ced06bca0a5.png">

コピーした Sonar way2プロファイルを選択した画面になります。

<img width="1273" alt="スクリーンショット 2019-10-22 9.08.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/deef52c2-0c32-b571-ab74-2c3b49f9702d.png">

プロファイルの一覧に戻って作成したプロファイルをデフォルトにします。

<img width="1272" alt="スクリーンショット 2019-10-22 9.12.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/1a2bb179-ca26-ce3a-3e09-ca41fe99c6b8.png">

これにより、今後のスキャンではこのプロファイルが利用されるようになります。

次に、ルールを選びます。リポジトリを選ぶとルールErrataが一覧に表示されるのでそれを選択します。

<img width="1276" alt="スクリーンショット 2019-10-22 9.17.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/8d739cd2-986e-3648-da14-9ed0bb395985.png">

Quality Profilesの欄に「Activate」というボタンが表示されるので、Activateします。
最初のステップでSonar wayをコピーした編集可能なプロファイルを作成しないと、このActivateボタンは表示されないので注意してください。

<img width="1275" alt="スクリーンショット 2019-10-22 9.19.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/3fc8f89d-cead-5cfb-5a79-ac26368b301e.png">

<img width="453" alt="スクリーンショット 2019-10-22 9.19.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/9e0110da-8809-78a7-4861-e87e1dee1ccb.png">

以上の手順で、プラグインで追加したルールをプロファイルに登録することができました。
改めてソースをスキャンすることで、ルールが有効になっていることを確認しましょう。

```console
cd sample
sonar-scanner
...
INFO: Sensor Check errata for text files [errata]
INFO: Sensor Check errata for text files [errata] (done) | time=11ms
...
INFO: ------------------------------------------------------------------------
INFO: Total time: 5.677s
INFO: Final Memory: 6M/27M
INFO: ------------------------------------------------------------------------
```

scannerの出力をよく見るとプラグインで追加したセンサーが動作しているログが表示されます。

プロジェクト Helloを見るとちゃんと(ニセの)バグが検出されています。
また、sonar-project.propertiesというソースとは異なるファイルも検出対象になってしまってます。
対象ファイルの制御についてはこの後おいおいやっていくことにします。

<img width="1277" alt="スクリーンショット 2019-10-22 9.28.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/4b0349ab-6c14-5af0-c94f-370f678a6c30.png">

続く... 
→[SonarQubeのプラグイン作成(その2)](/jca02266/items/f26f65721a5e5c0ff0be)