Rule Configuration using annotations (#3637)

* Create initial idea for @Configuration annotation
* Use config parameter constants again
* Generate documentation from @Configuration
* Introduce config delegate and extract default value from it
* Exclude rules configured with annotation from checks
* Remove support for factory methods in RuleSetProviderCollector
* Add support for lists that are empty by default
* Update documentation for rule contribution
* Restrict config delegate to supported types
* Update .github/CONTRIBUTING.md

Co-authored-by: Brais Gabín <braisgabin@gmail.com>
Co-authored-by: Markus Schwarz <post@markus-schwarz.net>
Co-authored-by: Chao Zhang <zhangchao6865@gmail.com>
This commit is contained in:
marschwar
2021-04-09 01:34:22 +02:00
committed by GitHub
parent 7ae2a96d81
commit cbee5e7e2e
13 changed files with 838 additions and 255 deletions

View File

@@ -1,17 +1,17 @@
package io.gitlab.arturbosch.detekt.rules.complexity
import io.github.detekt.metrics.CyclomaticComplexity
import io.github.detekt.metrics.CyclomaticComplexity.Companion.DEFAULT_NESTING_FUNCTIONS
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Debt
import io.gitlab.arturbosch.detekt.api.Entity
import io.gitlab.arturbosch.detekt.api.Issue
import io.gitlab.arturbosch.detekt.api.Metric
import io.gitlab.arturbosch.detekt.api.Rule
import io.gitlab.arturbosch.detekt.api.Severity
import io.gitlab.arturbosch.detekt.api.ThresholdRule
import io.gitlab.arturbosch.detekt.api.ThresholdedCodeSmell
import io.gitlab.arturbosch.detekt.api.internal.ActiveByDefault
import io.gitlab.arturbosch.detekt.api.internal.valueOrDefaultCommaSeparated
import io.gitlab.arturbosch.detekt.api.internal.Configuration
import io.gitlab.arturbosch.detekt.api.internal.config
import org.jetbrains.kotlin.psi.KtBlockExpression
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtNamedFunction
@@ -36,21 +36,24 @@ import org.jetbrains.kotlin.psi.KtWhenExpression
* - __Exceptions__ - `catch`, `use`
* - __Scope Functions__ - `let`, `run`, `with`, `apply`, and `also` ->
* [Reference](https://kotlinlang.org/docs/reference/scope-functions.html)
*
* @configuration threshold - McCabe's Cyclomatic Complexity (MCC) number for a method (default: `15`)
* @configuration ignoreSingleWhenExpression - Ignores a complex method if it only contains a single when expression.
* (default: `false`)
* @configuration ignoreSimpleWhenEntries - Whether to ignore simple (braceless) when entries. (default: `false`)
* @configuration ignoreNestingFunctions - Whether to ignore functions which are often used instead of an `if` or
* `for` statement (default: `false`)
* @configuration nestingFunctions - Comma separated list of function names which add complexity
* (default: `[run, let, apply, with, also, use, forEach, isNotNull, ifNull]`)
*/
@ActiveByDefault(since = "1.0.0")
class ComplexMethod(
config: Config = Config.empty,
threshold: Int = DEFAULT_THRESHOLD_METHOD_COMPLEXITY
) : ThresholdRule(config, threshold) {
class ComplexMethod(config: Config = Config.empty) : Rule(config) {
@Configuration("McCabe's Cyclomatic Complexity (MCC) number for a method.")
private val threshold: Int by config(DEFAULT_THRESHOLD_METHOD_COMPLEXITY)
@Configuration("Ignores a complex method if it only contains a single when expression.")
private val ignoreSingleWhenExpression: Boolean by config(false)
@Configuration("Whether to ignore simple (braceless) when entries.")
private val ignoreSimpleWhenEntries: Boolean by config(false)
@Configuration("Whether to ignore functions which are often used instead of an `if` or `for` statement.")
private val ignoreNestingFunctions: Boolean by config(false)
@Configuration("Comma separated list of function names which add complexity.")
private val nestingFunctions: List<String> by config(DEFAULT_NESTING_FUNCTIONS)
override val issue = Issue(
"ComplexMethod",
@@ -59,11 +62,7 @@ class ComplexMethod(
Debt.TWENTY_MINS
)
private val ignoreSingleWhenExpression = valueOrDefault(IGNORE_SINGLE_WHEN_EXPRESSION, false)
private val ignoreSimpleWhenEntries = valueOrDefault(IGNORE_SIMPLE_WHEN_ENTRIES, false)
private val ignoreNestingFunctions = valueOrDefault(IGNORE_NESTING_FUNCTIONS, false)
private val nestingFunctions = valueOrDefaultCommaSeparated(NESTING_FUNCTIONS, DEFAULT_NESTING_FUNCTIONS.toList())
.toSet()
private val nestingFunctionsAsSet: Set<String> = nestingFunctions.toSet()
override fun visitNamedFunction(function: KtNamedFunction) {
if (ignoreSingleWhenExpression && hasSingleWhenExpression(function.bodyExpression)) {
@@ -73,7 +72,7 @@ class ComplexMethod(
val complexity = CyclomaticComplexity.calculate(function) {
this.ignoreSimpleWhenEntries = this@ComplexMethod.ignoreSimpleWhenEntries
this.ignoreNestingFunctions = this@ComplexMethod.ignoreNestingFunctions
this.nestingFunctions = this@ComplexMethod.nestingFunctions
this.nestingFunctions = this@ComplexMethod.nestingFunctionsAsSet
}
if (complexity >= threshold) {
@@ -104,9 +103,16 @@ class ComplexMethod(
companion object {
const val DEFAULT_THRESHOLD_METHOD_COMPLEXITY = 15
const val IGNORE_SINGLE_WHEN_EXPRESSION = "ignoreSingleWhenExpression"
const val IGNORE_SIMPLE_WHEN_ENTRIES = "ignoreSimpleWhenEntries"
const val IGNORE_NESTING_FUNCTIONS = "ignoreNestingFunctions"
const val NESTING_FUNCTIONS = "nestingFunctions"
val DEFAULT_NESTING_FUNCTIONS = listOf(
"run",
"let",
"apply",
"with",
"also",
"use",
"forEach",
"isNotNull",
"ifNull"
)
}
}

View File

@@ -10,6 +10,8 @@ import io.gitlab.arturbosch.detekt.test.lint
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
private val defaultConfigMap: Map<String, Any> = mapOf("threshold" to "1")
class ComplexMethodSpec : Spek({
val defaultComplexity = 1
@@ -19,7 +21,7 @@ class ComplexMethodSpec : Spek({
context("different complex constructs") {
it("counts different loops") {
val findings = ComplexMethod(threshold = 1).compileAndLint(
val findings = ComplexMethod(TestConfig(defaultConfigMap)).compileAndLint(
"""
fun test() {
for (i in 1..10) {}
@@ -34,7 +36,7 @@ class ComplexMethodSpec : Spek({
}
it("counts catch blocks") {
val findings = ComplexMethod(threshold = 1).compileAndLint(
val findings = ComplexMethod(TestConfig(defaultConfigMap)).compileAndLint(
"""
fun test() {
try {} catch(e: IllegalArgumentException) {} catch(e: Exception) {} finally {}
@@ -46,7 +48,7 @@ class ComplexMethodSpec : Spek({
}
it("counts nested conditional statements") {
val findings = ComplexMethod(threshold = 1).compileAndLint(
val findings = ComplexMethod(TestConfig(defaultConfigMap)).compileAndLint(
"""
fun test() {
try {
@@ -79,27 +81,27 @@ class ComplexMethodSpec : Spek({
"""
it("counts three with nesting function 'forEach'") {
val config = TestConfig(mapOf(ComplexMethod.IGNORE_NESTING_FUNCTIONS to "false"))
val config = TestConfig(defaultConfigMap.plus("ignoreNestingFunctions" to "false"))
assertExpectedComplexityValue(code, config, expectedValue = 3)
}
it("can ignore nesting functions like 'forEach'") {
val config = TestConfig(mapOf(ComplexMethod.IGNORE_NESTING_FUNCTIONS to "true"))
val config = TestConfig(defaultConfigMap.plus("ignoreNestingFunctions" to "true"))
assertExpectedComplexityValue(code, config, expectedValue = 2)
}
it("skips all if if the nested functions is empty") {
val config = TestConfig(mapOf(ComplexMethod.NESTING_FUNCTIONS to ""))
val config = TestConfig(defaultConfigMap.plus("nestingFunctions" to ""))
assertExpectedComplexityValue(code, config, expectedValue = 2)
}
it("skips 'forEach' as it is not specified") {
val config = TestConfig(mapOf(ComplexMethod.NESTING_FUNCTIONS to "let,apply,also"))
val config = TestConfig(defaultConfigMap.plus("nestingFunctions" to "let,apply,also"))
assertExpectedComplexityValue(code, config, expectedValue = 2)
}
it("skips 'forEach' as it is not specified list") {
val config = TestConfig(mapOf(ComplexMethod.NESTING_FUNCTIONS to listOf("let", "apply", "also")))
val config = TestConfig(defaultConfigMap.plus("nestingFunctions" to listOf("let", "apply", "also")))
assertExpectedComplexityValue(code, config, expectedValue = 2)
}
}
@@ -111,16 +113,18 @@ class ComplexMethodSpec : Spek({
it("does not report complex methods with a single when expression") {
val config = TestConfig(
mapOf(
ComplexMethod.IGNORE_SINGLE_WHEN_EXPRESSION to "true"
"threshold" to "4",
"ignoreSingleWhenExpression" to "true"
)
)
val subject = ComplexMethod(config, threshold = 4)
val subject = ComplexMethod(config)
assertThat(subject.lint(path)).hasSourceLocations(SourceLocation(43, 5))
}
it("reports all complex methods") {
val subject = ComplexMethod(threshold = 4)
val config = TestConfig(mapOf("threshold" to "4"))
val subject = ComplexMethod(config)
assertThat(subject.lint(path)).hasSourceLocations(
SourceLocation(6, 5),
@@ -132,7 +136,7 @@ class ComplexMethodSpec : Spek({
}
it("does not trip for a reasonable amount of simple when entries when ignoreSimpleWhenEntries is true") {
val config = TestConfig(mapOf(ComplexMethod.IGNORE_SIMPLE_WHEN_ENTRIES to "true"))
val config = TestConfig(mapOf("ignoreSimpleWhenEntries" to "true"))
val subject = ComplexMethod(config)
val code = """
fun f() {
@@ -216,7 +220,7 @@ class ComplexMethodSpec : Spek({
})
private fun assertExpectedComplexityValue(code: String, config: TestConfig, expectedValue: Int) {
val findings = ComplexMethod(config, threshold = 1).lint(code)
val findings = ComplexMethod(config).lint(code)
assertThat(findings).hasSourceLocations(SourceLocation(1, 5))