You are using SonarQube and its OpenAPI Analyzer to analyze your projects, but there aren't rules that allow you to target some of your company's specific needs? Then your logical choice may be to implement your own set of custom OpenAPI rules.
This document is an introduction to custom rule writing for the SonarQube OpenAPI Analyzer. It will cover all the main concepts of static analysis required to understand and develop effective rules, relying on the API provided by the SonarQube OpenAPI Plugin.
The rules you are going to develop will be delivered using a dedicated, custom plugin, relying on the SonarQube OpenAPI Plugin API. In order to start working efficiently, we provide a empty template maven project, that you will fill in while following this tutorial.
Grab the template project from there and import it to your IDE.
This project already contains one custom rule. Our goal will be to add an extra rule!
A custom plugin is a Maven project, and before diving into code, it is important to notice a few relevant lines related to the configuration of your soon-to-be-released custom plugin.
In the code snippet below, note the plugin API version (<sonar.version>
) provided through the properties. It relates
to the minimum version of SonarQube your plugin will support, and is generally aligned to your company's SonarQube
instance. In this template, we rely on the version 6.7.4.
You'll notice there's a separate property (<sonarQubeMinVersion>
) defined for the version of
your company instance's SonarQube version. Here we'll use 6.7. Notice how the two versions
are aligned.
The property <sonaropenapi.version>
is the minimum version of the OpenAPI Analyzer that will
be required to run your custom plugin in your SonarQube instance. Consequently, as we will rely
on version 1.0-SNAPHSOT of the OpenAPI plugin, the SonarQube instance which will use the custom
plugin will need version 1.0-SNAPSHOT of the Java Plugin as well.
For the moment, don't touch these two properties.
Other properties such as <groupId>
, <artifactId>
, <version>
, <name>
and <description>
can be freely modified.
<groupId>org.sonarsource.samples</groupId>
<artifactId>openapi-custom-rules</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>sonar-plugin</packaging>
<name>SonarQube OpenAPI Custom Rules Example</name>
<description>OpenAPI Custom Rules Example for SonarQube</description>
<inceptionYear>2018</inceptionYear>
<properties>
<sonar.version>6.7.4</sonar.version>
<sonarQubeMinVersion>6.7</sonarQubeMinVersion>
<sonaropenapi.version>1.0-SNAPSHOT</sonaropenapi.version>
<sonaranalyzer.version>1.6.0.219</sonaranalyzer.version>
</properties>
In the code snippet below, it is important to note that the entry point of the plugin is
provided as the <pluginClass>
in the configuration of the sonar-packaging-maven plugin, using
the fully qualified name of the java class MyOpenAPIRulesPlugin
. If you refactor your code,
rename, or move the class extending org.sonar.api.SonarPlugin
, you will have to change this
configuration.
It is very important to set the basePlugin
property to openapi
. This will allow your extension
plugin to correctly share the same classpath as the OpenAPI Analyzer plugin, which is necessary
to correctly reference the classes from the common Sonar SSLR API.
<plugin>
<groupId>org.sonarsource.sonar-packaging-maven-plugin</groupId>
<artifactId>sonar-packaging-maven-plugin</artifactId>
<version>1.17</version>
<extensions>true</extensions>
<configuration>
<pluginKey>openapi-custom</pluginKey>
<pluginName>OpenAPI Custom Rules</pluginName>
<pluginClass>org.sonar.samples.openapi.MyOpenAPIRulesPlugin</pluginClass>
<skipDependenciesPackaging>true</skipDependenciesPackaging>
<sonarLintSupported>true</sonarLintSupported>
<sonarQubeMinVersion>${sonarQubeMinVersion}</sonarQubeMinVersion>
<basePlugin>openapi</basePlugin>
</configuration>
</plugin>
In this section, we'll write a custom rule from scratch. To do so, we will use a Test Driven Developement (TDD) approach, relying on writing some test cases first, followed by the implementation a solution. The rule we will develop will check that no path and no parameter contains the word "foo".
When implementing a rule, there is always a minimum of 3 distinct files to create:
- A test file, which contains OpenAPI code used as input data for testing the rule
- A test class, which contains the rule's unit test
- A rule class, which contains the implementation of the rule.
To create our first custom rule (usually called a "check"), let's start by creating these 3 files in the template project, as described below:
-
In folder
/src/test/resources/checks/v3
, create a new empty file namedMyFirstCustomCheck.yaml
, and copy-paste the content of the following code snippet.openapi: "3.0.1" info: version: 1.0.0 title: Swagger Petstore paths: /pets: {}
-
In package
org.sonar.samples.openapi.checks
of/src/test/java
, create a new test class calledMyFirstCustomCheckTest
and copy-paste the content of the following code snippet.package org.sonar.samples.openapi.checks; import org.junit.Test; public class MyFirstCustomCheckTest { @Test public void test() { } }
-
In package
org.sonar.samples.openapi.checks
of/src/main/java
, create a new class calledMyFirstCustomCheck
extending classorg.sonar.plugins.openapi.api.OpenApiCheck
provided by the OpenPI Plugin API. Then, replace the content of thesubscribedKinds()
method with the content from the following code snippet (you may have to importcom.google.common.collect.ImmutableSet
). This file will be described when dealing with implementation of the rule!package org.sonar.samples.openapi.checks; import com.google.common.collect.ImmutableSet; import com.sonar.sslr.api.AstNodeType; import org.sonar.plugins.openapi.api.OpenApiCheck; import java.util.Set; public class MyFirstCustomCheck extends OpenApiCheck { @Override public Set<AstNodeType> subscribedKinds() { return ImmutableSet.of(); } }
More files ?
If the 3 files described above are always the base of rule writing, there are situations where extra files may be needed. For instance, when a rule uses parameters, multiple test files could be required. It is also possible to use external files to describe rule metadata, such as a description in html format. Such situations will be described in other topics of this documentation.
Because we chose a TDD approach, the first thing to do is to write examples of the code our rule will target. In this file, we consider numerous cases that our rule may encounter during an analysis, and flag the lines which will require our implementation to raise issues.
Covering all the possible cases is not necessarily required, the goal of this file is to cover all the situations which may be encountered during an analysis, but also to abstract irrelevant details. For instance, in the context of our first rule, the content of schemas, the url of the servers make no difference. Note that this sample file should be structurally correct.
In the test file MyFirstCustomCheck.yaml
created earlier, copy-paste the following code:
openapi: "3.0.1"
info:
version: 1.0.0
title: Swagger Petstore
paths:
/pets/foo/{id}:
get:
responses:
'200':
description: some operation
parameters:
- name: foo-parameter
in: query
- name: good
in: query
/animals: {}
The test file now contains the following test cases:
- line 6: a path that contains the word
foo
- line 12: an operation parameter that contains the word
foo
- line 14: a parameter that does not contain the forbidden word
- line 15: a path that does not contain the forbidden word
Once the test file is updated, let's update our test class to use it, and link the test to our (not yet implemented) rule.
To do so, get back to our test class MyFirstCustomCheckTest
, and update the test()
method as shown in the following
code snippet (you may need to import some classes):
package org.sonar.samples.openapi.checks;
import org.junit.Test;
import org.sonar.openapi.TestOpenApiVisitorRunner;
import org.sonar.plugins.openapi.api.OpenApiVisitorContext;
import org.sonar.plugins.openapi.api.PreciseIssue;
import java.io.File;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class MyFirstCustomCheckTest {
@Test
public void test() {
OpenApiVisitorContext context = TestOpenApiVisitorRunner.createContext(new File("src/test/resources/checks/v3/MyFirstCustomCheck.yaml"));
List<PreciseIssue> issues = new MyFirstCustomCheck().scanFileForIssues(context);
assertThat(issues)
.extracting(i -> i.primaryLocation().startLine())
.containsExactly(6, 12);
}
}
For the sake of this test, we are just checking that the check collects the issues at the right line. We can later add other verifications, such as the message raised by the issue.
Now, let's proceed to the next step of TDD: make the test fail!
To do so, simply execute the test from the test file using JUnit. The test should fail as shown below:
java.lang.AssertionError:
Actual and expected should have same size but actual size was:
<0>
while expected size was:
<2>
Actual was:
<[]>
Expected was:
<[6, 12]>
Before we start with the implementation of the rule itself, you need a little background.
Prior to running any rule, the SonarQube OpenAPI Analyzer parses a given OpenAPI contract file and produces an equivalent
data structure: the Syntax Tree. Each construction of the OpenAPI specification can be represented with a specific kind
of Syntax Tree, detailing each of its particularities. Each of these constructions is associated with a specific AstNodeType
.
For instance, the type associated to the declaration of an operation in an OpenAPI v3 document will be
org.sonar.plugins.openapi.api.v3.OpenApi3Grammar.OPERATION
.
The plugin provides types for both versions of the API, in the org.sonar.plugins.openapi.api.v2.OpenApi2Grammar
and org.sonar.plugins.openapi.api.v3.OpenApi3Grammar
enums. For our example rule, we will focus on just OpenAPI v3,
but most of your rules will need also to be compatible with OpenAPI v2.
Our rule class derives from the OpenApiCheck
class provided by the Sonar OpenAPI plugin's API. This class, on top of
providing a bunch of useful methods to raise issues, also defines the strategy which will be used when analyzing a file.
It is based on a subscription mechanism, allowing to specify on what kind of tree the rule should react. The list of node
types to cover is specified through the subscribedKinds()
method. In the previous steps, we modified the implementation of the method to return an empty list, therefore not subscribing to any node of the syntax tree.
Now its finally time to jump in to the implementation of our first rule! Go back to the MyFirstCustomCheck
class, and
modify the list of AstNodeType
returned by the subscribedKinds()
method. Since our rule targets path keys and
parameters declarations, we only need to visit these nodes. To do so, simply add OpenApi3Grammar.PATH
and
OpenApi3Grammar.PARAMETER
as a parameter of the returned immutable set, as shown in the following code snippet.
@Override
public Set<AstNodeType> subscribedKinds() {
return ImmutableSet.of(OpenApi3Grammar.PATHS, OpenApi3Grammar.PARAMETER);
}
Once the nodes to visit are specified, we have to implement how the rule will react when encountering declarations.
To do so, override method visitNode(AstNode node)
, inherited from OpenApiCheck
.
@Override
public void visitNode(AstNode node) {
}
Now, let's add the checks for the path keys. We will use tools provided in org.sonar.sslr.yaml.grammar.Utils
by the
Sonar OpenAPI plugin API, that help us navigate in the AST node tree. We will navigate through all the properties
of the API's paths
section. As defined per the specification, the key of each property defines a * path* on which
some operations are defined.
The OpenApiCheck
class provides utility methods to declare new issues. By providing a specific AstNode
, you can
mark the exact location of the issue.
static final String MESSAGE = "You shall not use the 'foo' word!";
@Override
public void visitNode(AstNode node) {
if (node.getType() == OpenApi3Grammar.PATHS) {
for (AstNode property : Utils.properties(node)) {
checkPathKeys(property);
}
}
}
private void checkPathKeys(AstNode node) {
AstNode keyNode = key(node);
String path = keyNode.getTokenValue();
if (path.contains("foo")) {
addIssue(MESSAGE, keyNode);
}
}
Let's re-run our test!
java.lang.AssertionError:
Actual and expected should have same size but actual size was:
<1>
while expected size was:
<2>
Actual was:
<[6]>
Expected was:
<[6, 12]>
That's better! The rule is now capturing the violation on line 6, caused by the incorrect path. We'll now add the same check for the parameter names.
@Override
protected void visitNode(AstNode node) {
if (node.getType() == OpenApi3Grammar.PATHS) {
} else {
checkParameterDefinition(node);
}
}
private void checkParameterDefinition(AstNode node) {
AstNode valueNode = value(at(node, "/name"));
String name = valueNode.getTokenValue();
if (name.contains("foo")) {
addIssue(MESSAGE, valueNode);
}
}
Now, ** execute the test** class again.
Test passed? If not, then check if you somehow missed a step.
If it passed...
You implemented your first custom rule for the SonarQube OpenAPI Analyzer!
OK, you are probably quite happy at this point, as our first rule is running as expected... However, we are not really done yet. Before playing our rule against any real projects, we have to finalize its creation within the custom plugin, by registering it.
The first thing to do is to provide to our rule all the metadata which will allow us to register it properly in the
SonarQube platform. To do so, add the org.sonar.check.Rule
annotation to MyFirstCustomCheck
class rule, and provide
a key.
@Rule(key = "MyFirstCustomRule")
public class MyFirstCustomCheck extends OpenApiCheck {
The rest of the metadata is provided by a JSON file and an HTML file, that gives a name, description and optional tags:
-
In folder
/src/main/resources/org/sonar/l10n/openapi/rules/openapi
, create a new empty file namedMyFirstCustomRule.html
, and copy-paste the content of the following code snippet:<p>This rule detects paths and parameters that contain <tt>foo</tt>.</p> <h2>Noncompliant Code Example</h2> <pre> TO DO </pre> <h2>Compliant Solution</h2> <pre> TO DO </pre>
-
In folder
/src/main/resources/org/sonar/l10n/openapi/rules/openapi
, create a new empty file namedMyFirstCustomRule.json
, and copy-paste the content of the following code snippet:{ "title": "Title of MyFirstCustomCheck", "type": "CODE_SMELL", "status": "ready", "remediation": { "func": "Constant\/Issue", "constantCost": "5min" }, "tags": [ "pitfall" ], "defaultSeverity": "Minor" }
The second things to to is to activate the rule within the plugin. To do so, open class RulesList
(org.sonar.samples.openapi.checks.RulesList
). In this class, you will notice the method getChecks()
.
This method is used to register our rules with alongside the rule of the OpenAPI plugin. To register the rule, simply
add the rule class to the list, as in the following code snippet:
public static List<Class> getChecks() {
return Arrays.asList(
// other rules...
MyFirstCustomCheck.class
);
}
Prerequisite
For this chapter, you will need a local instance of SonarQube. If you don't have a SonarQube platform installed on your machine, now is time to download its latest version from HERE!
At this point, we've completed the implementation of a first custom rule and registered it into the custom plugin. The last remaining step is to test it directly with the SonarQube platform and try to analyse a project!
Start by building the project using maven:
$ pwd
/home/gandalf/workspace/openapi-custom-rules
$ mvn clean install
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building OpenAPI Custom Rules - Template 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 4.102 s
[INFO] Finished at: 2016-05-23T16:21:55+02:00
[INFO] Final Memory: 25M/436M
[INFO] ------------------------------------------------------------------------
Then, grab the jar file openapi-custom-rules-1.0-SNAPSHOT.jar
from the target folder of the project, and move it
to the extensions folder of your SonarQube instance, which will be located at $SONAR_HOME/extensions/plugins
.
SonarQube Java Plugin compatible version
Before going further, be sure to have the adequate version of the SonarQube OpenAPI Plugin with your SonarQube instance. The dependency over the OpenAPI Plugin of our custom plugin is defined in its
pom
, as seen in the first chapter of this tutorial.If you have a fresh install or do not possess the same version, install the adequate version of the OpenAPI Plugin.
Now, (re-)start your SonarQube instance, log as admin
and navigate to the Rules tab.
From there, under the language section, select "OpenAPI", and then "MyCompany Custom Repository" under the repository section. Your rule should now be visible (with all the other sample rules).
Select the rule and activate it in the default quality profile.
TODO - insert picture here
Once activated, the only step remaining is to analyse one of your project!
When encountering a path containing foo
, the issue will now raise issue.
TODO - insert picture here
- Try to add similar checks for OpenApi v2 grammar
- Add verifications in the test that the correct message is raised in the error
- Add integration tests (TODO - document it a bit)