This is the 3rd post in a series 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.
Failed to generate image: PlantUML preprocessing failed: [From <input> (line 7) ] @startuml interface Plugin { + {abstract} define(context: Plugin.Context) } class Plugin.Context { + addExtensions(first: Object, second: Object, others: Object...) } ^^^^^ Bad hierarchy for class Plugin.Context interface Plugin { + {abstract} define(context: Plugin.Context) } class Plugin.Context { + addExtensions(first: Object, second: Object, others: Object...) } class AbstractLanguage { - key: String - name: String + AbstractLanguage(key: String, name: String) } interface Sensor { + {abstract} describe(descriptor: SensorDescriptor) + {abstract} execute(context: SensorContext) } interface RulesDefinition { + {abstract} define(context: Context) } class RulesDefinition.Context { + createRepository(key: String, language: String): NewRepository } abstract class ProfileDefinition { + {abstract} createProfile(validation: ValidationMessages): RulesProfile } Plugin +-- Plugin.Context RulesDefinition +-- RulesDefinition.Context Sensor -down[hidden]- RulesDefinition AbstractLanguage -down[hidden]- Plugin hide empty members hide empty attributes
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:
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:
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!