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 RuleSetProvider
interfaces.
So it is possible to define rules/rule sets and enhance detekt with your own flavor.
You need a resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider
file which
has as content the fully qualified name of your RuleSetProvider
e.g. io.gitlab.arturbosch.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.
Own rules have to extend the abstract Rule class and override the visitXXX()
-functions from the AST.
A RuleSetProvider
must be implemented, which declares a RuleSet
in the instance()
-function.
To leverage the configuration mechanism of detekt you must pass the Config object from your rule set provider to your rule.
An Issue
property defines what ID, severity and message should be printed on the console or on any other output format.
Example of a custom rule:
class TooManyFunctions(config: Config) : Rule(config) {
override val issue = Issue(javaClass.simpleName,
Severity.CodeSmell,
"This rule reports a file with an excessive function count.",
Debt.TWENTY_MINS)
private val threshold = 10
private var amount: Int = 0
override fun visitKtFile(file: KtFile) {
super.visitKtFile(file)
if (amount > threshold) {
report(CodeSmell(issue, 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++
}
}
Example of a much preciser rule in terms of more specific CodeSmell constructor and Rule attributes:
class TooManyFunctions2(config: Config) : Rule(config) {
override val issue = Issue(
javaClass.simpleName,
Severity.CodeSmell,
"This rule reports a file with an excessive function count.",
Debt.TWENTY_MINS
)
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(ThresholdedCodeSmell(issue,
entity = Entity.from(file),
metric = Metric(type = "SIZE", value = amount, threshold = threshold),
message = "The file ${file.name} has $amount function declarations. " +
"Threshold is specified with $threshold.",
references = emptyList())
)
}
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.
Please note that this will only take effect, if the Config
object is passed on by the RuleSetProvider
to the rule itself.
MyRuleSet:
TooManyFunctions2:
active: true
threshold: 5
OtherRule:
active: false
By specifying the rule set and rule ids, detekt will use the sub configuration of TooManyFunctions2
:
val threshold = valueOrDefault("threshold", THRESHOLD)
As of version 1.2.0 detekt now verifies if all configured properties actually exist in a configuration created by --generate-config
.
This means that by default detekt does not know about your new properties.
Therefore we need to mention them in the configuration under config>excludes
.
config:
validation: true
# 1. exclude rule set 'sample' and all its nested members
# 2. exclude every property in every rule under the rule set 'sample'
excludes: "sample.*,sample>.*>.*"
Testing your rules
To test your rules, add the dependency on detekt-test
to your project: testCompile "io.gitlab.arturbosch.detekt:detekt-test:$version"
.
The easiest way to detect issues with your newly created rule is to use the lint
extension function:
Rule.lint(StringContent/Path/KtFile): List<Finding>
If you need to reuse the Kotlin file for performance reasons within similar test cases, please use one of these functions:
compileContentForTest(content: String): KtFile
compileForTest(path: Path): KtFile
Custom Processors
Custom processors can be used for example to implement additional project metrics.
When for whatever reason you want to count all loop statements inside your code base, 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/io.gitlab.arturbosch.detekt.api.FileProcessListener
file
with the full qualify name of our processor as the content: io.gitlab.arturbosch.detekt.sample.extensions.processors.NumberOfLoopsProcessor
.
To test the code we use the detekt-test
module and write a JUnit 5 testcase.
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.
All they need are an implementation of the render()
-function which takes an object with all findings and returns a string to be printed out.
abstract fun render(detektion: Detektion): String?
Let detekt know about your extensions
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.
Integrate your extension with the detekt CLI
Mention your jar
with the --plugins
flag when calling the cli fatjar:
detekt --input ... --plugins /path/to/my/jar
Integrate your extension with the Detekt Gradle Plugin
For example detekt
itself provides a wrapper over ktlint as a
custom formatting
rule set.
To enable it, we add the published dependency to detekt
via the detektPlugins
configuration:
Gradle (Kotlin/Groovy DSL)
dependencies {
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.1")
}
Pitfalls
- All rules are disabled by default and have to be explicitly enabled in the
detekt
yaml configuration file. - If you do not pass the
Config
object from theRuleSetProvider
to the rule, the rule is active, but you will not be able to use any configuration options or disable the rule via config file. - If your extension is part of your project and you integrate it like
detektPlugins project(":my-rules")
make sure that this subproject is build beforegradle detekt
is run. In thekotlin-dsl
you could add something liketasks.withType<Detekt> { dependsOn(":my-rules:assemble") }
to explicitly rundetekt
only after your extension sub project 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 created a pure kotlin module which has no Android dependencies.
apply plugin: "kotlin"
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 --stop
to stop gradle daemons and run the task again.