Extending detekt
The following page describes how to extend detekt and how to customize it to your domain-specific needs. The associated code samples to this guide can be found in the package detekt/detekt-sample-extensions.
Custom RuleSets
detekt uses the ServiceLoader pattern to collect all instances of the RuleSetProvider interface, making it possible to define rules/rule sets and enhance detekt with your own flavor.
You need a resources/META-INF/services/dev.detekt.api.RuleSetProvider file containing the fully qualified name of
your RuleSetProvider. For example:
dev.detekt.sample.extensions.SampleProvider
You can use our GitHub template to have a basic scaffolding to develop your own custom rules. Another option is to clone the provided detekt/detekt-sample-extensions project.
It's important that the dependency of dev.detekt:detekt-api is configured as compileOnly (as in the examples).
You can read more information about this here.
Custom rules must extend the Rule class and override the visitXXX() functions from the AST.
A RuleSetProvider must also be implemented, declaring a RuleSet in the instance() function.
Example of a custom rule:
class TooManyFunctions(config: Config) : Rule(
config,
"This rule reports a file with an excessive function count.",
) {
private val threshold: Int by config(defaultValue = 10)
private var amount: Int = 0
override fun visitKtFile(file: KtFile) {
super.visitKtFile(file)
if (amount > threshold) {
report(Finding(Entity.from(file),
"Too many functions can make the maintainability of a file costlier"))
}
amount = 0
}
override fun visitNamedFunction(function: KtNamedFunction) {
super.visitNamedFunction(function)
amount++
}
}
If you want your rule to be configurable, write down your properties inside the detekt.yml file.
MyRuleSet:
TooManyFunctions:
active: true
threshold: 5
OtherRule:
active: false
By specifying the rule set and rule IDs, detekt will use the sub-configuration of TooManyFunctions.
Testing custom rules
To test your rules, add the detekt-test dependency to your project:
// Required
testImplementation("dev.detekt:detekt-test:2.0.0-alpha.2")
// Optional - makes use of the "assertThat" test structure
testImplementation("dev.detekt:detekt-test-assertj:2.0.0-alpha.2")
// Optional - handy to test rules that use type resolution
testImplementation("dev.detekt:detekt-test-junit:2.0.0-alpha.2")
Basic tests
The simplest way to test a rule is with the lint extension function, which runs your rule against inline Kotlin code:
class TooManyFunctionsSpec {
val subject = TooManyFunctions(Config.empty)
@Test
fun `reports files with too many functions`() {
val code = """
class MyClass {
fun a() = Unit
fun b() = Unit
// ...
}
""".trimIndent()
assertThat(subject.lint(code)).hasSize(1)
}
@Test
fun `does not report files within threshold`() {
val code = """
class MyClass {
fun a() = Unit
}
""".trimIndent()
assertThat(subject.lint(code)).isEmpty()
}
}
With custom configs
To validate configurable rules, use TestConfig instead of Config.empty:
val subject = TooManyFunctions(
TestConfig(
"threshold" to 5,
"someBooleanKey" to false,
"someStringKey" to "abc",
)
)
With type resolution
If your rule requires type resolution (i.e. it implements RequiresAnalysisApi):
- annotate the test class with
@KotlinCoreEnvironmentTest, - put an instance of
KotlinEnvironmentContainerin the test class constructor, - use the
lintWithContextextension function to generate findings using full analysis:
@KotlinCoreEnvironmentTest
class MyTypeAwareRuleSpec(val env: KotlinEnvironmentContainer) {
private val subject = MyTypeAwareRule(Config.empty)
@Test
fun `detects issue with type info`() {
val code = """...""".trimIndent()
val findings = subject.lintWithContext(env, code)
assertThat(findings).hasSize(1)
}
}
By default, code snippets passed into lintWithContext are compiled against the full test classpath (kotlin-stdlib, any testImplementation dependencies, etc.). If your rule targets a specific third-party library, just add it as a testRuntimeOnly dependency in your build file and any classes in that library will be available for import/analysis in test snippets automatically.
You can also make Java source files available to your test snippets by placing them under test/resources and referencing them via the @KotlinCoreEnvironmentTest annotation, but remember that this is Java only - not Kotlin files.
@KotlinCoreEnvironmentTest(additionalJavaSourcePaths = ["myJavaSources"])
class MyRuleSpec(val env: KotlinEnvironmentContainer) {
// Java classes under test/resources/myJavaSources/ are now importable in test snippets
}
Custom assertions
The custom assertThat from detekt-test-assertj supports more idiomatic assertions on findings:
assertThat(findings)
.singleElement()
.hasMessage("Expected message")
.hasStartSourceLocation(3, 5)
Custom Processors
Custom processors can be used, for example, to implement additional project metrics.
For instance, if you want to count all loop statements in your codebase, you could write something like:
class NumberOfLoopsProcessor : FileProcessListener {
override fun onProcess(file: KtFile) {
val visitor = LoopVisitor()
file.accept(visitor)
file.putUserData(numberOfLoopsKey, visitor.numberOfLoops)
}
companion object {
val numberOfLoopsKey = Key<Int>("number of loops")
}
class LoopVisitor : DetektVisitor() {
internal var numberOfLoops = 0
override fun visitLoopExpression(loopExpression: KtLoopExpression) {
super.visitLoopExpression(loopExpression)
numberOfLoops++
}
}
}
To let detekt know about the new processor, we specify a resources/META-INF/services/dev.detekt.api.FileProcessListener file
with the fully qualified name of the processor as its content, e.g. dev.detekt.sample.extensions.processors.NumberOfLoopsProcessor.
To test the code, use the detekt-test module and write a JUnit 5 test case.
class NumberOfLoopsProcessorTest {
@Test
fun `should expect two loops`() {
val code = """
fun main() {
for (i in 0..10) {
while (i < 5) {
println(i)
}
}
}
"""
val ktFile = compileContentForTest(code)
NumberOfLoopsProcessor().onProcess(ktFile)
assertThat(ktFile.getUserData(NumberOfLoopsProcessor.numberOfLoopsKey)).isEqualTo(2)
}
}
Custom Reports
detekt allows you to extend the console output and to create custom output formats.
If you want to customize the output, take a look at the ConsoleReport and OutputReport classes.
Each requires an implementation of the render() function, which takes an object with all findings and returns a string to be printed.
abstract fun render(detektion: Detektion): String?
Integrating extensions with detekt
So you have implemented your own rules or other extensions and want to integrate them
into your detekt run? Great, make sure to have a jar with all your needed dependencies
minus the ones detekt brings itself.
Take a look at our sample project on how to achieve this with gradle.
Via the Detekt CLI
Pass your jar with the --plugins flag when calling the CLI fatjar:
detekt --input ... --plugins /path/to/my/jar
Via the Detekt Gradle Plugin
For example detekt itself provides a wrapper over ktlint as a
custom rule set. To enable it, we add the published dependency to detekt via the detektPlugins configuration:
dependencies {
detektPlugins("dev.detekt:detekt-rules-ktlint-wrapper:2.0.0-alpha.2")
}
You can use the same method to apply any other custom rulesets! See the Detekt 3rd-party Marketplace for more.
Pitfalls
- All rules are disabled by default and have to be explicitly enabled in the
detektyaml configuration file. - If your extension is part of your project and you integrate it like
detektPlugins(project(":my-rules"))make sure that this subproject is built beforegradle detektis run. In thekotlin-dslyou could add something liketasks.withType<Detekt> { dependsOn(":my-rules:assemble") }to explicitly rundetektonly after your extension subproject is built. - If you use detekt for your Android project and if you want to integrate all your custom rules in a new module, please make sure that
you put them in a pure kotlin module with no Android dependencies.
kotlin("jvm")is enough to make it work. - Sometimes when you run detekt task, you may not see the violations detected by your custom rules. In this case open a terminal and run
./gradlew --stopto stop gradle daemons and run the task again. - If you are configuring a custom detekt task at the root project level, you will need to apply the detektPlugins at the root project as well (not subprojects). See this issue for more.