SonarQube Continuous Inspection logo

This is the 3rd post in a serie about creating a SonarQube plugin for the Kotlin language:

  • The first post was about creating the parsing code itself.
  • The 2nd post detailed how to use the parsing code to check for two rules.

In this final post, we will be creating the plugin proper using the code of the 2 previous posts.

The Sonar model

The Sonar model is based on the following abstractions:

Plugin
Entry-point for plugins to inject extensions into SonarQube

A plugin points to the other abstraction instances to make the SonarQube platform load them

AbstractLanguage

Pretty self-explanatory. Represents a language - Java, C#, Kotlin, etc.

ProfileDefinition
Define a profile which is automatically registered during sonar startup

A profile is a mutable set of fully-configured rules. While not strictly necessary, having a Sonar profile pre-registered allows users to analyze their code without further configuration. Every language plugin offered by Sonar has at least one profile attached.

RulesDefinition
Defines some coding rules of the same repository

Defines an immutable set of rule definitions into a repository. While a rule definition defines available parameters, default severity, etc. the rule (from the profile) defines the exact value for parameters, a specific severity, etc. In short, the rule implements the role definition.

Sensor
A sensor is invoked once for each module of a project, starting from leaf modules. The sensor can parse a flat file, connect to a web server…​ Sensors are used to add measure and issues at file level.

The sensor is the entry-point where the magic happens.

sonarqube api class diagram

Starting to code the plugin

Every abstraction above needs a concrete subclass. Note that the API classes themselves are all fairly decoupled. It’s the role of the Plugin child class to bind them together.

class KotlinPlugin : Plugin {

    override fun define(context: Context) {
        context.addExtensions(
                Kotlin::class.java,
                KotlinProfile::class.java,
                KotlinSensor::class.java,
                KotlinRulesDefinition::class.java)
    }
}

Most of the code is mainly boilerplate, but for ANTLR code.

Wiring the ANTLR parsing code

On one hand, the parsing code is based on generated listeners. On the other hand, the sensor is the entry-point to the SonarQube parsing. There’s a need for a bridge between the 2.

In the first article, we used an existing grammar for Kotlin to generate parsing code. SonarQube provides its own lexer/parser generating tool (SonarSource Language Recognizer). A sizeable part of the plugin API is based on it. Describing the grammar is no small feat for any real-life language, so I preferred to design my own adapter code instead.

AbstractKotlinParserListener

Subclass of the generated ANTLR KotlinParserBaseListener. It has an attribute to store violations, and a method to add such a violation.

Violation

The violation only contains the line number, as the rest of the required information will be stored into a KotlinCheck instance.

KotlinCheck

Abstract class that wraps an AbstractKotlinParserListener. Defines what constitutes a violation. It handles the ANTLR boilerplate code itself.

This can be represented as the following:

kotlin plugin class diagram

The sensor proper

The general pseudo-code should look something akin to:

FOR EACH source file
    FOR EACH rule
        Check for violation of the rule
        FOR EACH violation
            Call the SonarQube REST API to create a violation in the datastore

This translates as:

class KotlinSensor(private val fs: FileSystem) : Sensor {

    val sources: Iterable<InputFile>
        get() = fs.inputFiles(MAIN)

    override fun execute(context: SensorContext) {
        sources.forEach { inputFile: InputFile ->
            KotlinChecks.checks.forEach { check ->
                val violations = check.violations(inputFile.file())
                violations.forEach { (lineNumber) ->
                    with(context.newIssue().forRule(check.ruleKey())) {
                        val location = newLocation().apply {
                            on(inputFile)
                            message(check.message)
                            at(inputFile.selectLine(lineNumber))
                        }
                        at(location).save()
                    }
                }
            }
        }
    }
}

Finally, the run

Let’s create a dummy Maven project with 2 classes, Test1 and Test2 in one Test.kt file, with the same code as last week. Running mvn sonar:sonar yields the following output:

SonarQube screenshot

Et voilà, our first SonarQube plugin for Kotlin, checking for our custom-developed violations.

Of course, it has (a lot of) room for improvements:

  • Rules need to be activated through the GUI - I couldn’t find how to do it programmatically
  • Adding new rules needs updates to the plugin. Rules in 3rd-party plugins are not added automatically, as could be the case for standard SonarQube plugins.
  • So far, code located outside of classes seems not to be parsed.
  • The walk through the parse tree is executed for every check. An obvious performance gain would be to walk only once and do every check from there.
  • A lof of the above improvements could be achieved by replacing ANTLR’s grammar with Sonar’s internal SSLR
  • No tests…​

That still makes the project a nice starting point for a full-fledged Kotlin plugin. Pull requests are welcome!

The complete source code for this post can be found on Github in Maven format.