目的
SonarQubeのプラグインを作成してみます。
参考: 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を削除して、ボリュームの記述を相対パスに変えてます。これはカレントディレクトリに永続化したディレクトリを作成したいからです。
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 を作成したディレクトリで以下を実行します。
docker-compose up -d
これでしばらくすると、 http://localhost:9000/ でSonarQubeにアクセスできます。
うまくいかない場合は、以下でログを参照しエラーがないかを確認してください。
docker-compose logs
正常に起動すると以下の画面が開きます。画面右上の「Log in」から、ユーザ名admin,パスワードadminでログインしてください。

ログイン後の画面

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


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

なお、この時点でカレントディレクトリのsonarqube_extensions/downloads/
にプラグインファイルがダウンロードされます。
ls sonarqube_extensions/downloads/
sonar-java-plugin-5.14.0.18788.jar
再起動後は、sonarqube_extensions/plugins/
にこのファイルは移動しています。
ls sonarqube_extensions/plugins/
sonar-java-plugin-5.14.0.18788.jar
以降はこの sonarqube_extensions/plugins
ディレクトリにプラグインのjarファイルが残っている限りは個別のインストールは不要になります。他にプラグインが必要であれば任意に追加してください。解析対象の言語以外では sonar-scm-git-plugin
はあった方が良いでしょう。
SonarScanner 準備
サーバの準備ができたら解析対象の準備になります。
まずは以下から自分の環境にあった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 ディレクトリを作成し、以下のファイルを作ってください。
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);
}
}
}
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が起動している事は確認しておいてください)
cd sample
sonnar-scanner
sonar-scanner がうまく行けば最後に以下の出力になります。
INFO: ------------------------------------------------------------------------
INFO: EXECUTION SUCCESS
INFO: ------------------------------------------------------------------------
INFO: Total time: 6.865s
INFO: Final Memory: 6M/24M
INFO: ------------------------------------------------------------------------
sonar-scannerが完了した後でも、サーバではバックグラウンドタスクが実行中の場合があります。
これが完了するのを待つ必要があります。(上記サンプル程度ならすぐに完了します。)

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




プラグイン作成
最終的に作成するプラグインはあらかじめ用意した正誤表をもとに誤表記をチェックするものにします。
例えば、以下のような正誤表にしたがって左の語句が出てきたらエラーにするというものです。
このようなチェックで問題となるのは誤判定です。誤判定をいつまでも報告し続けてもゴミになり、そのゴミのせいで本来検出したい誤記が目立たなくなります。そのゴミを 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のpom.xmlの例では artifactId を sonar-{key}-plugin
という名前にすることを推奨されているのでそれに従っています。
packaging は通常、jar
などを指定する箇所ですが、ここは sonar-plugin
固定です。
最初のプラグイン
まずは、Maven Project作成にあたって、以下の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 に指定したクラスがプラグインの入り口になります。
以下のファイルを準備します。
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 というのは以下になります。
ここでは、ルールの集まりを表す「リポジトリ」を作成し、そこに一つ一つのルールを追加します。
(この例では追加するルールは一つだけです)
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();
}
}
大事なのは
context.createRepository(REPOSITORY, LANGUAGE)
でリポジトリを作り、
public static final RuleKey RULE = RuleKey.of(REPOSITORY, "errata");
でリポジトリにルールを追加している部分になります。あとはそれぞれの属性値になります。
後の手順で確認しますが、このプラグインを組み込むことで以下のようにリポジトリとルールが追加されていることが確認できます。


センサー
CreateIssuesOnTextFilesSensor.class のソースは以下になります。
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格納ディレクトリにコピーします。
$ 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→再ログインを試してみてください。

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

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

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

これにより、今後のスキャンではこのプロファイルが利用されるようになります。
次に、ルールを選びます。リポジトリを選ぶとルールErrataが一覧に表示されるのでそれを選択します。

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


以上の手順で、プラグインで追加したルールをプロファイルに登録することができました。
改めてソースをスキャンすることで、ルールが有効になっていることを確認しましょう。
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というソースとは異なるファイルも検出対象になってしまってます。
対象ファイルの制御についてはこの後おいおいやっていくことにします。

続く...
→SonarQubeのプラグイン作成(その2)