Making Custom Kotlin Code Inspection Plugin

Elye
Elye
Sep 5, 2018 · 8 min read

Ever wish to have your write custom code inspection? And have the warning shown even before you compile? Check out the below GIF, where I made a non-CamelCase name on the fly.

Code Inspection vs Linting

Lint — a static code analysis perform during COMPILATION

Code Inspection — a static code analysis while CODING

So when I say Code Inspection, I literally mean something that check your code while you CODE! So much better than Lint. (though lint does have some advantage, e.g. provide you ability to have a report and also used in Continuous Integration)

If you are looking to write custom lint for Kotlin code, checkout the below

To understand further about Code Inspection you could refers to

Let’s start the work

Unfortunately it only show how to print a system out log for the plugin, and it is missing what is needed for Kotlin.

Nonetheless a great reference where I’ll follow the initial step (with some modification) for our Kotlin Code Inspection plugin.

Create the below folders

Create the build.gradle

buildscript {
ext.kotlin_version = '1.2.50'
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
maven {
url 'http://dl.bintray.com/jetbrains/intellij-plugin-service'
}
}
dependencies {
classpath
"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath
"gradle.plugin.org.jetbrains:gradle-intellij-plugin:0.1.10"
}
}

Then follow by the needed plugin and dependencies

apply plugin: 'kotlin'
apply plugin: 'org.jetbrains.intellij'

sourceCompatibility = 1.8

repositories {
mavenCentral()
}

intellij {
version '2018.1.4'
pluginName 'Custom Plugin'
plugins 'kotlin'
updateSinceUntilBuild false
//alternativeIdePath "/Applications/Android App Path/"
}

dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
implementation
"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

}

group 'com.personal'
version '1.0-SNAPSHOT'

Note: we have alterantiveIdePath there commented. With it commented, when you run, it will open up the IntelliJ Sandbox to test your plugin. If you uncomment it out, and put your Android App’s path there, it will open up Android Studio instead as your Sandbox to test your plugin.

Create plugin.xml

<idea-plugin>
<id>com.custom.plugin</id>
<name>Custom Plugin</name>
<vendor email="test@example.com"
url="http://www.example.com">Example</vendor>

<description><![CDATA[
Demo plugin written in Kotlin for Kotlin syntax check to
ensure camelcase naming and also a notification plugin
]]></description>

<change-notes><![CDATA[
Release notes : Camelcase naming check and Notification plugin
]]>
</change-notes>

<depends>org.jetbrains.kotlin</depends>
<!-- Code Inspection Component -->
<extensions defaultExtensionNs="com.intellij">
<inspectionToolProvider implementation=
"com.plugin.inspection.CustomInspectionProvider"/>
</extensions>

<depends>com.intellij.modules.lang</depends>
<!-- Notification Component -->
<project-components>
<component>
<implementation-class>
com.plugin.inspection.CustomNotificationComponent
</implementation-class>
</component>
</project-components>

</idea-plugin>

From here, you could see there’s two components I add to my plugin

  1. Notification Component.
  2. Code Inspection Component.

I will describe the two components below

Notification Component

I extracted this code sample from the below.

Note: Don’t follow the entire step in the blog above, as it start off downloading the entire IntelliJ source and build from there.

I change it to Kotlin version of it though

class CustomNotificationComponent : ProjectComponent {

override fun projectOpened() {}

override fun projectClosed() {}

override fun initComponent() {
ApplicationManager.getApplication()
.invokeLater({
Notifications.Bus.notify(NOTIFICATION_GROUP.value
.createNotification(
"Testing Personal Plugin",
"Check for Kotlin non CamelCase usage",
NotificationType.INFORMATION,
null))
}, ModalityState.NON_MODAL)
}

override fun disposeComponent() {}

override fun getComponentName(): String {
return CUSTOM_NOTIFICATION_COMPONENT
}

companion object {
private const val CUSTOM_NOTIFICATION_COMPONENT =
"CustomNotificationComponent"
private val NOTIFICATION_GROUP = object :
NotNullLazyValue<NotificationGroup>() {
override fun compute(): NotificationGroup {
return NotificationGroup(
"Motivational message",
NotificationDisplayType.STICKY_BALLOON,
true)
}
}
}
}

Code Inspection Component

The Provider

class CustomInspectionProvider : InspectionToolProvider {
override fun getInspectionClasses(): Array<Class<*>> {
return arrayOf(CamelcaseInspection::class.java)
}
}

The Inspection (in Java)

To see how it is implemented, there’s quite a bit of example on the Android code found in the below link

Unfortunately they are all in Java, and doesn’t show how to get the needed PSI (Program Structure Interface) for Kotlin.

The Inspection (in Kotlin)

I search the entire Kotlin Library, but can’t find it easily. Later found that we could get that included by adding plugins ‘kotlin’ in the intellij section of build.gradle.

Using that AbstractKotlinInspection class, we could easily get access to various method that extract your Kotlin code for analyzing it.

In my case, I’m accessing all the name extracted, and check if it is CamelCase to decide if we could register it as a problem.

class CamelcaseInspection : AbstractKotlinInspection() {

override fun getDisplayName(): String {
return "Use CamelCase naming"
}

override fun getGroupDisplayName(): String {
return GroupNames.STYLE_GROUP_NAME
}

override fun getShortName(): String {
return "Camelcase"
}

override fun buildVisitor(holder: ProblemsHolder,
isOnTheFly: Boolean): KtVisitorVoid {
return namedDeclarationVisitor { declaredName ->
if (declaredName.name?.isDefinedCamelCase() == false) {
System.out.println(
"Non CamelCase Name Detected for
${declaredName.name}")
holder.registerProblem(
declaredName.nameIdentifier as PsiElement,
"Please use CamelCase for #ref #loc")
}
}
}

private fun String.isDefinedCamelCase(): Boolean {
val toCharArray = toCharArray()
return toCharArray
.mapIndexed { index, current ->
current to toCharArray.getOrNull(index + 1) }
.none { it.first.isUpperCase() &&
it.second?.isUpperCase() ?: false }
}

override fun isEnabledByDefault(): Boolean {
return true
}
}

Refer to the above code, would give you some idea of how to write an Inspection Code in Kotlin.

For more code reference, you could check the below link (actual Kotlin code inspection codes)

In it also, other than just detecting the issue, you could also code to fix the issue. Check the code out!

The inspection description

Under the resources folder, create inspectionDescriptions folder.

Then create a html file with the name that is name after the Custom Inspection you made, which is in our case CamelCase.html

In it, just write the description of the Inspection message as below.

<html>
<body>
The name detected is not in CamelCase format. <br>
Consider changing it to CamelCase format.
</body>
</html>

The description of official Kotlin code inspection could be found in

Deploying the Plugin

Sandbox testing

./gradlew runIdea

It will launch the sandBox IntelliJ that you could check out if your plugin works.

If you like to launch other IDE (e.g. Android Studio), just use the alternativeIdePath as mentioned above.

Applying on actual Android Studio.

  1. run ./gradlew buildPlugin
  2. Go to Android Studio->Preferences->Plugin->Install plugin from disk
  3. Go to your plugin project’s build->distributions
  4. You’ll find a zip file there, and you could install it.

Publishing to IntelliJ Plugin Repository

./gradlew publishPlugin

You’ll need to have your account ready though, and follow the tutorial below

Some hiccups along the way

1. bash: ./gradlew: Permission denied

chmod 777 ./gradlew

2. Need to have one Java Class

* What went wrong:
Execution failed for task ‘:classes’.
> destination directory “/demo_kotlin_inspection_plugin/build/classes/java/main” does not exist or is not a directory

Just create a dummy Java class, or make one class as Java class instead.

3. Some odd issue

The issue I got is, my inspectionDescriptions is not showing, due to initial error. After correcting it, it is still not okay until I clear my build and reset everything.


You could get my working code here

Hopes this provides you some good pointer on writing your own custom Inspection rules for Kotlin. Let me know if you publish anything up on IntelliJ Plugin Repository one day. Would love to know this blog helps people contribute better to everyone.

I hope this post is helpful to you. You could check out my other interesting topics here.

Follow me on medium, Twitter or Facebook for little tips and learning on Android, Kotlin etc related topics. ~Elye~

Elye

Written by

Elye

Learning and Sharing Android and iOS Development