Building and Deploying Custom Qodana Rules: From Inspection to CI
Learn how to author IntelliJ‑style inspections as Qodana plugins, test them locally, and drop them into your CI pipeline with minimal effort. A hands‑on example shows a rule that flags any method named "foo".
05 Feb 2026, 16:00 UTC

Why You Need a Custom Rule Engine
Qodana ships with a comprehensive set of inspections that catch common bugs, style violations, and security issues. Yet every project has its own coding conventions, domain‑specific patterns, and legacy quirks that the default rule set can’t cover. Manually adding comments or using @SuppressWarnings is error‑prone and clutters the codebase. Qodana’s Custom Rule Engine lets you author IntelliJ‑style inspections in Kotlin or Java, package them as a plugin, and run them automatically in the same scan that produces the built‑in findings.
Step 1: Scaffold a Small Kotlin Project
Start with a minimal Kotlin application to keep the example focused. Create a src/main/kotlin/App.kt file that contains a method named foo—our target for the custom inspection.
package com.example
fun main() {
foo()
}
fun foo() {
println("This should be flagged by the custom rule")
}
Step 2: Author the Inspection
In Qodana, a custom inspection extends com.intellij.codeInspection.LocalInspectionTool. The following Kotlin class traverses the PSI (Program Structure Interface) tree, finds KtNamedFunction nodes, and reports any function whose name is "foo".
import com.intellij.codeInspection.*
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.psi.KtNamedFunction
class FlagFooInspection : LocalInspectionTool() {
override fun getShortName() = "FlagFoo"
override fun getDisplayName() = "Method named 'foo'"
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): KtVisitor {
return object : KtVisitor() {
override fun visitNamedFunction(function: KtNamedFunction) {
super.visitNamedFunction(function)
if (function.name == "foo") {
holder.registerProblem(
function.nameIdentifier!!,
"Method name 'foo' is prohibited",
ProblemHighlightType.ERROR
)
}
}
}
}
}
Key points:
- Use
ProblemsHolderto emit problems with severity and a message. - Access the PSI tree via
KtNamedFunctionfor Kotlin; for Java you would usePsiMethod. - Keep the visitor lightweight; only inspect the nodes you care about.
Step 3: Package the Inspection as a Qodana Plugin
Qodana plugins are plain JARs that expose an implementation of com.jetbrains.qodana.api.QodanaPlugin. Create a build.gradle.kts that compiles the inspection and bundles it.
plugins {
kotlin("jvm") version "2.0.0"
}
repositories {
mavenCentral()
maven("https://plugins.gradle.org/m2/")
maven("https://repo.maven.apache.org/maven2/")
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib:2.0.0")
implementation("com.jetbrains.qodana:api:1.0.0")
}
tasks.register<Jar>("qodanaPlugin") {
archiveClassifier.set("qodana")
from(sourceSets.main.get().output)
manifest {
attributes("Plugin-Class" to "com.example.FlagFooInspection")
}
}
Run ./gradlew qodanaPlugin to produce build/libs/yourproject-qodana.jar. This JAR contains the inspection and a manifest entry that Qodana uses to locate the plugin class.
Step 4: Test the Plugin Locally
Place the JAR in a directory and reference it in qodana.yaml:
version: 1
plugins:
- path: ./build/libs/yourproject-qodana.jar
Run Qodana from the project root:
qodana run --config qodana.yaml
Check the generated HTML report (default: qodana-report/index.html). You should see an error under “Custom Inspections” highlighting the foo() call. Verify that the severity matches the ProblemHighlightType.ERROR you set.
Step 5: Integrate into CI
In a typical GitHub Actions workflow, add the JAR to the repository or publish it to a private Maven repository. Example snippet for .github/workflows/qodana.yml:
name: Qodana Scan
on: [push, pull_request]
jobs:
qodana:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Cache Qodana
uses: actions/cache@v4
with:
path: ~/.cache/qodana
key: ${{ runner.os }}-qodana-${{ hashFiles('**/build.gradle.kts') }}
- name: Run Qodana
uses: JetBrains/qodana-action@v2023.1
with:
config: qodana.yaml
token: ${{ secrets.QODANA_TOKEN }}
Key notes:
- Ensure the CI runner can resolve the JAR path. If you host it in a Maven repo, update
plugins:to usemavencoordinates. - The
qodana-actionautomatically downloads the plugin JAR before scanning. - Because Qodana caches results, re‑running the scan with unchanged code and the same plugin version will skip the custom inspection, keeping the pipeline fast.
Trade‑offs and Practical Limits
- Performance: Simple string checks add negligible overhead. Complex semantic analyses (e.g., cross‑module dependency checks) can increase scan time by 5‑10% per module. Profile with
--verboseand consider incremental execution if the scan becomes a bottleneck. - False Positives: A bug in the inspection logic can flood the report. Unit‑test the visitor against a small set of PSI files before merging into CI.
- Version Compatibility: The Qodana API evolves. Pin the
com.jetbrains.qodana:apidependency to a specific version and test against the Qodana CLI you plan to use. - Distribution: Public plugins can be published to
https://plugins.jetbrains.com, private plugins to a Maven repo. Keep the JAR in version control or a secure artifact store to avoid breaking CI builds.
Actionable Checklist
- Write the inspection in Kotlin/Java.
- Package it as a Qodana plugin JAR.
- Add the JAR path to
qodana.yamland run a local scan. - Validate the custom issue appears in the report.
- Commit the plugin JAR (or publish it) and update CI to reference it.
- Monitor scan times; if performance degrades, simplify the visitor or enable caching.
- Maintain a changelog for the plugin and pin the API dependency.
By following these steps you can enforce project‑specific quality rules without littering code with @SuppressWarnings or relying on external linters. Custom Qodana inspections integrate seamlessly into your existing CI workflow, delivering consistent, actionable feedback to developers.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.