website/docs/introduction/extensions.md
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.
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.
:::caution Attention
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.
:::note
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.
To test your rules, add the detekt-test dependency to your project:
// Required
testImplementation("dev.detekt:detekt-test:[detekt_version]")
// Optional - makes use of the "assertThat" test structure
testImplementation("dev.detekt:detekt-test-assertj:[detekt_version]")
// Optional - handy to test rules that use type resolution
testImplementation("dev.detekt:detekt-test-junit:[detekt_version]")
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()
}
}
To validate configurable rules, use TestConfig instead of Config.empty:
val subject = TooManyFunctions(
TestConfig(
"threshold" to 5,
"someBooleanKey" to false,
"someStringKey" to "abc",
)
)
If your rule requires type resolution (i.e. it implements RequiresAnalysisApi):
@KotlinCoreEnvironmentTest,KotlinEnvironmentContainer in the test class constructor,lintWithContext extension 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
}
The custom assertThat from detekt-test-assertj supports more idiomatic assertions on findings:
assertThat(findings)
.singleElement()
.hasMessage("Expected message")
.hasStartSourceLocation(3, 5)
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)
}
}
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?
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.
Pass your jar with the --plugins flag when calling the CLI fatjar:
detekt --input ... --plugins /path/to/my/jar
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:[detekt_version]")
}
You can use the same method to apply any other custom rulesets! See the Detekt 3rd-party Marketplace for more.
detekt yaml configuration file.detektPlugins(project(":my-rules")) make sure that this
subproject is built before gradle detekt is run.
In the kotlin-dsl you could add something like tasks.withType<Detekt> { dependsOn(":my-rules:assemble") } to explicitly run detekt only
after your extension subproject is built.kotlin("jvm") is enough to make it work../gradlew --stop to stop gradle daemons and run the task again.