diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 54d3362b1..e5160c10a 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -14,48 +14,12 @@
- ... do not forget to add the new rule to a `RuleSetProvider` (e.g. StyleGuideProvider)
- ... do not forget to write a description for the issue of the new rule.
-- Add the correct KDoc to the Rule class. This KDoc is used to generate wiki pages and the `default-detekt-config.yml`
-automatically. The format of the KDoc should be as follows:
-
- ```kotlin
- /**
- * This is a nice description for the rule, explaining what it checks, why it exists and how violations can be
- * solved.
- *
- *
- * // add the non-compliant code example here
- *
- *
- *
- * // add the compliant code example here
- *
- *
- * @configuration name - Description for the configuration option (default: `whatever should be the default`)
- */
- class SomeRule : Rule {
-
- }
- ```
-
- The description should be as detailed as possible as it will act as the documentation of the rule. Add links to
- references that explain the rationale for the rule if possible.
- The `` and `` code examples should be added right after the description of the rule.
- The `@configuration` tag should follow the correct pattern. The name of the configuration option *has* to match the
- actual name used in the code, otherwise an invalid `default-detekt-config.yml` will be generated and the rule won't
- function correctly by default.
- The default value will be taken as is for the configuration option and pasted into the `default-detekt-config.yml`.
-
- A `@configuration` tag as described above will translate to a rule entry in the `default-detekt-config.yml`:
- ```yml
- SomeRule:
- active: false
- name: whatever should be the default
- ```
+- ... add the [correct KDoc](#contents-and-structure-of-a-rules-kdoc) and [annotations](#rule-annotations) to your `Rule` class. This is used to generate documentation pages and the `default-detekt-config.yml` automatically.
+- ... do not forget to test the new rule and/or add tests for any changes made to a rule.
+Run detekt on itself and other kotlin projects with the `--run-rule RuleSet:RuleId` option to test your rule in isolation.
+Make use of the `scripts/get_analysis_projects.groovy` script to automatically establish a set of analysis projects.
- ... run `./gradlew generateDocumentation` to add your rule and its config options to the `default-detekt-config.yml`.
- ... do not forget to run `./gradlew build`. This will execute tests locally.
-- ... do not forget to test the new rule and/or add tests for any changes made to a rule. Run detekt on itself and other
- kotlin projects with the `--run-rule RuleSet:RuleId` option to test your rule in isolation. Make use of
- the `scripts/get_analysis_projects.groovy` script to automatically establish a set of analysis projects.
- To print the AST of sources you can pass the `--print-ast` flag to the CLI which will print each
Kotlin files AST. This can be helpful when implementing and debugging rules.
- To view the AST (PSI) of your source code you can use the [PSI Viewer plugin](https://plugins.jetbrains.com/plugin/227-psiviewer) for IntelliJ.
@@ -63,7 +27,58 @@ Kotlin files AST. This can be helpful when implementing and debugging rules.
After some time and testing there is a chance this rule will become active on default.
-Rules annotated with `@ActiveByDefault` will be marked as active in the`default-detekt-config.yml`.
+#### Contents and structure of a rule's KDoc
+
+```kotlin
+/**
+ * This is a nice description for the rule explaining what it checks, why it
+ * exists and how violations can be solved.
+ *
+ *
+ * // add the non-compliant code example here
+ *
+ *
+ *
+ * // add the compliant code example here
+ *
+ */
+class SomeRule(config: Config = Config.empty) : Rule(config) {
+
+}
+```
+
+The description should be as detailed as possible as it will act as the documentation of the rule. Add links to
+references that explain the rationale for the rule if possible.
+
+The `` and `` code examples should be added right after the description of the rule.
+
+#### Rule annotations
+
+```kotlin
+@ActiveByDefault(since = "1.0.0")
+@RequiresTypeResolution
+class SomeRule(config: Config = Config.empty) : Rule(config) {
+
+ @Configuration("This is the description for the configuration parameter below.")
+ private val name: String by config(default = "whatever should be the default")
+
+}
+```
+
+Use the `@Configuration` annotation in combination with the `config` delegate to create a configurable property for your rule. The name of the property will become the key and the provided default will be the value in the `default-detekt-config.yml`. All information are also used to generate the rule documentation in the wiki.
+Note that a property that is marked with `@Configuration` must use the config delegate (and vice versa).
+
+Rules annotated with `@ActiveByDefault` will be marked as active in the `default-detekt-config.yml`. Generally this will not be the case for new rules.
+
+A rule that requires type resolution must be marked with `@RequiresTypeResolution`. See [the type resolution wiki page](../docs/pages/gettingstarted/type-resolution.md) for more detail on this topic.
+
+The rule defined above will translate to a rule entry in the `default-detekt-config.yml`:
+```yml
+SomeRule:
+ active: true
+ name: 'whatever should be the default'
+```
+
### When updating the website ...
diff --git a/detekt-api/src/main/kotlin/io/gitlab/arturbosch/detekt/api/internal/ConfigProperty.kt b/detekt-api/src/main/kotlin/io/gitlab/arturbosch/detekt/api/internal/ConfigProperty.kt
new file mode 100644
index 000000000..47e79893a
--- /dev/null
+++ b/detekt-api/src/main/kotlin/io/gitlab/arturbosch/detekt/api/internal/ConfigProperty.kt
@@ -0,0 +1,26 @@
+package io.gitlab.arturbosch.detekt.api.internal
+
+import io.gitlab.arturbosch.detekt.api.ConfigAware
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+
+fun config(defaultValue: String): ReadOnlyProperty = simpleConfig(defaultValue)
+fun config(defaultValue: Int): ReadOnlyProperty = simpleConfig(defaultValue)
+fun config(defaultValue: Long): ReadOnlyProperty = simpleConfig(defaultValue)
+fun config(defaultValue: Boolean): ReadOnlyProperty = simpleConfig(defaultValue)
+fun config(defaultValue: List): ReadOnlyProperty> = ListConfigProperty(defaultValue)
+
+private fun simpleConfig(defaultValue: T): ReadOnlyProperty =
+ SimpleConfigProperty(defaultValue)
+
+private class SimpleConfigProperty(private val defaultValue: T) : ReadOnlyProperty {
+ override fun getValue(thisRef: ConfigAware, property: KProperty<*>): T {
+ return thisRef.valueOrDefault(property.name, defaultValue)
+ }
+}
+
+private class ListConfigProperty(private val defaultValue: List) : ReadOnlyProperty> {
+ override fun getValue(thisRef: ConfigAware, property: KProperty<*>): List {
+ return thisRef.valueOrDefaultCommaSeparated(property.name, defaultValue)
+ }
+}
diff --git a/detekt-api/src/main/kotlin/io/gitlab/arturbosch/detekt/api/internal/Configuration.kt b/detekt-api/src/main/kotlin/io/gitlab/arturbosch/detekt/api/internal/Configuration.kt
new file mode 100644
index 000000000..d7dead232
--- /dev/null
+++ b/detekt-api/src/main/kotlin/io/gitlab/arturbosch/detekt/api/internal/Configuration.kt
@@ -0,0 +1,7 @@
+package io.gitlab.arturbosch.detekt.api.internal
+
+@Target(AnnotationTarget.PROPERTY)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class Configuration(
+ val description: String,
+)
diff --git a/detekt-core/src/main/resources/default-detekt-config.yml b/detekt-core/src/main/resources/default-detekt-config.yml
index 96f4ac11b..7426a34ff 100644
--- a/detekt-core/src/main/resources/default-detekt-config.yml
+++ b/detekt-core/src/main/resources/default-detekt-config.yml
@@ -87,7 +87,7 @@ complexity:
ignoreSingleWhenExpression: false
ignoreSimpleWhenEntries: false
ignoreNestingFunctions: false
- nestingFunctions: [run, let, apply, with, also, use, forEach, isNotNull, ifNull]
+ nestingFunctions: ['run', 'let', 'apply', 'with', 'also', 'use', 'forEach', 'isNotNull', 'ifNull']
LabeledExpression:
active: false
ignoredLabels: []
diff --git a/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/ConfigAssert.kt b/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/ConfigAssert.kt
index 1dde48cac..d6985867d 100644
--- a/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/ConfigAssert.kt
+++ b/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/ConfigAssert.kt
@@ -2,6 +2,7 @@ package io.gitlab.arturbosch.detekt.core
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Rule
+import io.gitlab.arturbosch.detekt.api.internal.Configuration
import io.gitlab.arturbosch.detekt.core.config.DefaultConfig
import io.gitlab.arturbosch.detekt.core.config.YamlConfig
import org.assertj.core.api.Assertions.assertThat
@@ -45,6 +46,8 @@ class ConfigAssert(
}
private fun checkOptions(ymlOptions: HashMap, ruleClass: Class) {
+ if (ruleClass.isConfiguredWithAnnotations()) return
+
val configFields = ruleClass.declaredFields.filter { isPublicStaticFinal(it) && it.name != "Companion" }
var filter = ymlOptions.filterKeys { it !in allowedOptions }
if (filter.containsKey(THRESHOLD)) {
@@ -59,6 +62,9 @@ class ConfigAssert(
}
}
+ private fun Class.isConfiguredWithAnnotations(): Boolean =
+ declaredMethods.any { it.isAnnotationPresent(Configuration::class.java) }
+
private fun getYmlRuleConfig() = config.subConfig(name) as? YamlConfig
?: error("yaml config expected but got ${config.javaClass}")
diff --git a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/Annotations.kt b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/Annotations.kt
index 199f76765..fdd6a0add 100644
--- a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/Annotations.kt
+++ b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/Annotations.kt
@@ -1,16 +1,16 @@
package io.gitlab.arturbosch.detekt.generator.collection
+import org.jetbrains.kotlin.psi.KtAnnotated
import org.jetbrains.kotlin.psi.KtAnnotationEntry
-import org.jetbrains.kotlin.psi.KtClassOrObject
import kotlin.reflect.KClass
-fun KtClassOrObject.isAnnotatedWith(annotation: KClass): Boolean =
+fun KtAnnotated.isAnnotatedWith(annotation: KClass): Boolean =
annotationEntries.any { it.isOfType(annotation) }
-fun KtClassOrObject.firstAnnotationParameter(annotation: KClass): String =
+fun KtAnnotated.firstAnnotationParameter(annotation: KClass): String =
checkNotNull(firstAnnotationParameterOrNull(annotation))
-fun KtClassOrObject.firstAnnotationParameterOrNull(annotation: KClass): String? =
+fun KtAnnotated.firstAnnotationParameterOrNull(annotation: KClass): String? =
annotationEntries
.firstOrNull { it.isOfType(annotation) }
?.firstParameterOrNull()
@@ -25,6 +25,9 @@ private fun KtAnnotationEntry.firstParameterOrNull() =
?.text
?.withoutQuotes()
-private fun String.withoutQuotes() = removePrefix(QUOTES).removeSuffix(QUOTES)
+internal fun String.withoutQuotes() = removePrefix(QUOTES)
+ .removeSuffix(QUOTES)
+ .replace(STRING_CONCAT_REGEX, "")
private const val QUOTES = "\""
+private val STRING_CONCAT_REGEX = """["]\s*\+[\n\s]*["]""".toRegex()
diff --git a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/ConfigurationCollector.kt b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/ConfigurationCollector.kt
new file mode 100644
index 000000000..a9f2f8df7
--- /dev/null
+++ b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/ConfigurationCollector.kt
@@ -0,0 +1,166 @@
+package io.gitlab.arturbosch.detekt.generator.collection
+
+import io.gitlab.arturbosch.detekt.generator.collection.exception.InvalidDocumentationException
+import org.jetbrains.kotlin.psi.KtCallExpression
+import org.jetbrains.kotlin.psi.KtConstantExpression
+import org.jetbrains.kotlin.psi.KtElement
+import org.jetbrains.kotlin.psi.KtObjectDeclaration
+import org.jetbrains.kotlin.psi.KtProperty
+import org.jetbrains.kotlin.psi.KtPropertyDelegate
+import org.jetbrains.kotlin.psi.KtStringTemplateExpression
+import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType
+import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType
+import org.jetbrains.kotlin.psi.psiUtil.findDescendantOfType
+import org.jetbrains.kotlin.psi.psiUtil.referenceExpression
+import io.gitlab.arturbosch.detekt.api.internal.Configuration as ConfigAnnotation
+
+class ConfigurationCollector {
+
+ private val constantsByName = mutableMapOf()
+ private val properties = mutableListOf()
+
+ fun getConfiguration(): List {
+ return properties.mapNotNull { it.parseConfigurationAnnotation() }
+ }
+
+ fun addProperty(prop: KtProperty) {
+ properties.add(prop)
+ }
+
+ fun addCompanion(aRuleCompanion: KtObjectDeclaration) {
+ constantsByName.putAll(
+ aRuleCompanion
+ .collectDescendantsOfType()
+ .mapNotNull(::resolveConstantOrNull)
+ )
+ }
+
+ private fun resolveConstantOrNull(prop: KtProperty): Pair? {
+ if (prop.isVar) return null
+
+ val propertyName = checkNotNull(prop.name)
+ val constantOrNull = prop.getConstantValueAsStringOrNull()
+
+ return constantOrNull?.let { propertyName to it }
+ }
+
+ private fun KtProperty.getConstantValueAsStringOrNull(): String? {
+ if (hasListDeclaration()) {
+ return getListDeclarationAsConfigString()
+ }
+
+ return findDescendantOfType()?.text
+ ?: findDescendantOfType()?.text?.withoutQuotes()
+ }
+
+ private fun KtProperty.getListDeclarationAsConfigString(): String {
+ return getListDeclaration()
+ .valueArguments
+ .map { "'${it.text.withoutQuotes()}'" }.toString()
+ }
+
+ private fun KtProperty.parseConfigurationAnnotation(): Configuration? {
+ if (isAnnotatedWith(ConfigAnnotation::class)) return toConfiguration()
+ if (isInitializedWithConfigDelegate()) {
+ invalidDocumentation {
+ "'$name' is using the config delegate but is not annotated with @Configuration"
+ }
+ }
+ return null
+ }
+
+ private fun KtProperty.toConfiguration(): Configuration {
+ if (!hasSupportedType()) {
+ invalidDocumentation {
+ "Type of '$name' is not supported. " +
+ "For properties annotated with @Configuration use one of$SUPPORTED_TYPES."
+ }
+ }
+ if (!isInitializedWithConfigDelegate()) {
+ invalidDocumentation { "'$name' is not using the '$DELEGATE_NAME' delegate" }
+ }
+
+ val propertyName: String = checkNotNull(name)
+ val deprecationMessage = firstAnnotationParameterOrNull(Deprecated::class)
+ val description: String = firstAnnotationParameter(ConfigAnnotation::class)
+ val defaultValueAsString = delegate?.getDefaultValueAsString()
+ ?: invalidDocumentation { "'$propertyName' is not a delegated property" }
+
+ return Configuration(
+ name = propertyName,
+ description = description,
+ defaultValue = defaultValueAsString,
+ deprecated = deprecationMessage
+ )
+ }
+
+ private fun KtPropertyDelegate.getDefaultValueAsString(): String {
+ val delegateArgument = checkNotNull(
+ (expression as KtCallExpression).valueArguments[0].getArgumentExpression()
+ )
+ val listDeclarationForDefault = delegateArgument.getListDeclarationOrNull()
+ if (listDeclarationForDefault != null) {
+ return listDeclarationForDefault.valueArguments.map {
+ val value = constantsByName[it.text] ?: it.text
+ "'${value.withoutQuotes()}'"
+ }.toString()
+ }
+
+ val defaultValueOrConstantName = checkNotNull(
+ delegateArgument.text?.withoutQuotes()
+ )
+ val defaultValue = constantsByName[defaultValueOrConstantName] ?: defaultValueOrConstantName
+ return property.formatDefaultValueAccordingToType(defaultValue)
+ }
+
+ companion object {
+ private const val DELEGATE_NAME = "config"
+ private const val LIST_OF = "listOf"
+ private const val EMPTY_LIST = "emptyList"
+ private val LIST_CREATORS = setOf(LIST_OF, EMPTY_LIST)
+
+ private const val TYPE_STRING = "String"
+ private const val TYPE_BOOLEAN = "Boolean"
+ private const val TYPE_INT = "Int"
+ private const val TYPE_LONG = "Long"
+ private const val TYPE_STRING_LIST = "List"
+ private val SUPPORTED_TYPES = listOf(TYPE_STRING, TYPE_BOOLEAN, TYPE_INT, TYPE_LONG, TYPE_STRING_LIST)
+
+ private val KtPropertyDelegate.property: KtProperty
+ get() = parent as KtProperty
+
+ private val KtProperty.declaredTypeOrNull: String?
+ get() = typeReference?.text
+
+ private fun KtElement.getListDeclaration(): KtCallExpression =
+ checkNotNull(getListDeclarationOrNull())
+
+ private fun KtElement.getListDeclarationOrNull(): KtCallExpression? =
+ findDescendantOfType { it.isListDeclaration() }
+
+ private fun KtProperty.isInitializedWithConfigDelegate(): Boolean =
+ delegate?.expression?.referenceExpression()?.text == DELEGATE_NAME
+
+ private fun KtProperty.hasSupportedType(): Boolean =
+ declaredTypeOrNull in SUPPORTED_TYPES
+
+ private fun KtProperty.formatDefaultValueAccordingToType(value: String): String {
+ val defaultValue = value.withoutQuotes()
+ return when (declaredTypeOrNull) {
+ TYPE_STRING -> "'$defaultValue'"
+ TYPE_BOOLEAN, TYPE_INT, TYPE_LONG, TYPE_STRING_LIST -> defaultValue
+ else -> error("Unable to format unexpected type '$declaredTypeOrNull'")
+ }
+ }
+
+ private fun KtProperty.hasListDeclaration(): Boolean =
+ anyDescendantOfType { it.isListDeclaration() }
+
+ private fun KtCallExpression.isListDeclaration() =
+ referenceExpression()?.text in LIST_CREATORS
+
+ private fun KtElement.invalidDocumentation(message: () -> String): Nothing {
+ throw InvalidDocumentationException("[${containingFile.name}] ${message.invoke()}")
+ }
+ }
+}
diff --git a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/DocumentationCollector.kt b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/DocumentationCollector.kt
new file mode 100644
index 000000000..5bd655695
--- /dev/null
+++ b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/DocumentationCollector.kt
@@ -0,0 +1,81 @@
+package io.gitlab.arturbosch.detekt.generator.collection
+
+import io.gitlab.arturbosch.detekt.generator.collection.exception.InvalidCodeExampleDocumentationException
+import org.jetbrains.kotlin.psi.KtClassOrObject
+
+class DocumentationCollector {
+
+ private var name: String = ""
+ var description: String = ""
+ private set
+ var compliant: String = ""
+ private set
+ var nonCompliant: String = ""
+ private set
+
+ fun setClass(classOrObject: KtClassOrObject) {
+ name = classOrObject.name?.trim() ?: ""
+ classOrObject.kDocSection()
+ ?.getContent()
+ ?.trim()
+ ?.replace("@@", "@")
+ ?.let(::extractRuleDocumentation)
+ }
+
+ private fun extractRuleDocumentation(comment: String) {
+ val nonCompliantIndex = comment.indexOf(TAG_NONCOMPLIANT)
+ val compliantIndex = comment.indexOf(TAG_COMPLIANT)
+ when {
+ nonCompliantIndex != -1 -> {
+ extractNonCompliantDocumentation(comment, nonCompliantIndex)
+ extractCompliantDocumentation(comment, compliantIndex)
+ }
+ compliantIndex != -1 -> throw InvalidCodeExampleDocumentationException(
+ "Rule $name contains a compliant without a noncompliant code definition"
+ )
+ else -> description = comment
+ }
+ }
+
+ private fun extractNonCompliantDocumentation(comment: String, nonCompliantIndex: Int) {
+ val nonCompliantEndIndex = comment.indexOf(ENDTAG_NONCOMPLIANT)
+ if (nonCompliantEndIndex == -1) {
+ throw InvalidCodeExampleDocumentationException(
+ "Rule $name contains an incorrect noncompliant code definition"
+ )
+ }
+ description = comment.substring(0, nonCompliantIndex).trim()
+ nonCompliant = comment.substring(nonCompliantIndex + TAG_NONCOMPLIANT.length, nonCompliantEndIndex)
+ .trimStartingLineBreaks()
+ .trimEnd()
+ }
+
+ private fun extractCompliantDocumentation(comment: String, compliantIndex: Int) {
+ val compliantEndIndex = comment.indexOf(ENDTAG_COMPLIANT)
+ if (compliantIndex != -1) {
+ if (compliantEndIndex == -1) {
+ throw InvalidCodeExampleDocumentationException(
+ "Rule $name contains an incorrect compliant code definition"
+ )
+ }
+ compliant = comment.substring(compliantIndex + TAG_COMPLIANT.length, compliantEndIndex)
+ .trimStartingLineBreaks()
+ .trimEnd()
+ }
+ }
+
+ private fun String.trimStartingLineBreaks(): String {
+ var i = 0
+ while (i < this.length && (this[i] == '\n' || this[i] == '\r')) {
+ i++
+ }
+ return this.substring(i)
+ }
+
+ companion object {
+ private const val TAG_NONCOMPLIANT = ""
+ private const val ENDTAG_NONCOMPLIANT = ""
+ private const val TAG_COMPLIANT = ""
+ private const val ENDTAG_COMPLIANT = ""
+ }
+}
diff --git a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/RuleSetProviderCollector.kt b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/RuleSetProviderCollector.kt
index c561059fa..a75e43123 100644
--- a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/RuleSetProviderCollector.kt
+++ b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/RuleSetProviderCollector.kt
@@ -100,8 +100,8 @@ class RuleSetProviderVisitor : DetektVisitor() {
val ruleArgumentNames = (ruleListExpression as? KtCallExpression)
?.valueArguments
- ?.map { it.getArgumentExpression() }
- ?.mapNotNull { it?.referenceExpression()?.text }
+ ?.mapNotNull { it.getArgumentExpression() }
+ ?.mapNotNull { it.referenceExpression()?.text }
?: emptyList()
ruleNames.addAll(ruleArgumentNames)
diff --git a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/RuleVisitor.kt b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/RuleVisitor.kt
index bc00bd06e..a6f0eeb13 100644
--- a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/RuleVisitor.kt
+++ b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/collection/RuleVisitor.kt
@@ -7,13 +7,13 @@ import io.gitlab.arturbosch.detekt.api.internal.ActiveByDefault
import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
import io.gitlab.arturbosch.detekt.formatting.FormattingRule
import io.gitlab.arturbosch.detekt.generator.collection.exception.InvalidAliasesDeclaration
-import io.gitlab.arturbosch.detekt.generator.collection.exception.InvalidCodeExampleDocumentationException
import io.gitlab.arturbosch.detekt.generator.collection.exception.InvalidDocumentationException
import io.gitlab.arturbosch.detekt.generator.collection.exception.InvalidIssueDeclaration
import io.gitlab.arturbosch.detekt.rules.empty.EmptyRule
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtClassOrObject
+import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.KtSuperTypeList
import org.jetbrains.kotlin.psi.KtValueArgument
import org.jetbrains.kotlin.psi.psiUtil.containingClass
@@ -24,10 +24,8 @@ internal class RuleVisitor : DetektVisitor() {
val containsRule
get() = classesMap.any { it.value }
- private var description = ""
- private var nonCompliant = ""
- private var compliant = ""
private var name = ""
+ private val documentationCollector = DocumentationCollector()
private var defaultActivationStatus: DefaultActivationStatus = Inactive
private var autoCorrect = false
private var requiresTypeResolution = false
@@ -35,25 +33,33 @@ internal class RuleVisitor : DetektVisitor() {
private var debt = ""
private var aliases: String? = null
private var parent = ""
- private val configuration = mutableListOf()
+ private var configurationByKdoc = emptyList()
+ private val configurationCollector = ConfigurationCollector()
private val classesMap = mutableMapOf()
fun getRule(): Rule {
- if (description.isEmpty()) {
+ if (documentationCollector.description.isEmpty()) {
throw InvalidDocumentationException("Rule $name is missing a description in its KDoc.")
}
+ val configurationByAnnotation = configurationCollector.getConfiguration()
+ if (configurationByAnnotation.isNotEmpty() && configurationByKdoc.isNotEmpty()) {
+ throw InvalidDocumentationException(
+ "Rule $name is using both annotations and kdoc to define configuration parameter."
+ )
+ }
+
return Rule(
name = name,
- description = description,
- nonCompliantCodeExample = nonCompliant,
- compliantCodeExample = compliant,
+ description = documentationCollector.description,
+ nonCompliantCodeExample = documentationCollector.nonCompliant,
+ compliantCodeExample = documentationCollector.compliant,
defaultActivationStatus = defaultActivationStatus,
severity = severity,
debt = debt,
aliases = aliases,
parent = parent,
- configuration = configuration,
+ configuration = configurationByAnnotation + configurationByKdoc,
autoCorrect = autoCorrect,
requiresTypeResolution = requiresTypeResolution
)
@@ -97,52 +103,19 @@ internal class RuleVisitor : DetektVisitor() {
autoCorrect = classOrObject.hasKDocTag(TAG_AUTO_CORRECT)
requiresTypeResolution = classOrObject.isAnnotatedWith(RequiresTypeResolution::class)
+ configurationByKdoc = classOrObject.parseConfigurationTags()
- val comment = classOrObject.kDocSection()?.getContent()?.trim()?.replace("@@", "@") ?: return
- extractRuleDocumentation(comment)
- configuration.addAll(classOrObject.parseConfigurationTags())
+ documentationCollector.setClass(classOrObject)
}
- private fun extractRuleDocumentation(comment: String) {
- val nonCompliantIndex = comment.indexOf(TAG_NONCOMPLIANT)
- val compliantIndex = comment.indexOf(TAG_COMPLIANT)
- when {
- nonCompliantIndex != -1 -> {
- extractNonCompliantDocumentation(comment, nonCompliantIndex)
- extractCompliantDocumentation(comment, compliantIndex)
- }
- compliantIndex != -1 -> throw InvalidCodeExampleDocumentationException(
- "Rule $name contains a compliant without a noncompliant code definition"
- )
- else -> description = comment
- }
+ override fun visitProperty(property: KtProperty) {
+ super.visitProperty(property)
+ configurationCollector.addProperty(property)
}
- private fun extractNonCompliantDocumentation(comment: String, nonCompliantIndex: Int) {
- val nonCompliantEndIndex = comment.indexOf(ENDTAG_NONCOMPLIANT)
- if (nonCompliantEndIndex == -1) {
- throw InvalidCodeExampleDocumentationException(
- "Rule $name contains an incorrect noncompliant code definition"
- )
- }
- description = comment.substring(0, nonCompliantIndex).trim()
- nonCompliant = comment.substring(nonCompliantIndex + TAG_NONCOMPLIANT.length, nonCompliantEndIndex)
- .trimStartingLineBreaks()
- .trimEnd()
- }
-
- private fun extractCompliantDocumentation(comment: String, compliantIndex: Int) {
- val compliantEndIndex = comment.indexOf(ENDTAG_COMPLIANT)
- if (compliantIndex != -1) {
- if (compliantEndIndex == -1) {
- throw InvalidCodeExampleDocumentationException(
- "Rule $name contains an incorrect compliant code definition"
- )
- }
- compliant = comment.substring(compliantIndex + TAG_COMPLIANT.length, compliantEndIndex)
- .trimStartingLineBreaks()
- .trimEnd()
- }
+ override fun visitClass(klass: KtClass) {
+ super.visitClass(klass)
+ klass.companionObjects.forEach(configurationCollector::addCompanion)
}
private fun extractAliases(klass: KtClass) {
@@ -197,20 +170,8 @@ internal class RuleVisitor : DetektVisitor() {
)
private const val TAG_AUTO_CORRECT = "autoCorrect"
- private const val TAG_NONCOMPLIANT = ""
- private const val ENDTAG_NONCOMPLIANT = ""
- private const val TAG_COMPLIANT = ""
- private const val ENDTAG_COMPLIANT = ""
private const val ISSUE_ARGUMENT_SIZE = 4
private const val DEBT_ARGUMENT_INDEX = 3
}
}
-
-private fun String.trimStartingLineBreaks(): String {
- var i = 0
- while (i < this.length && (this[i] == '\n' || this[i] == '\r')) {
- i++
- }
- return this.substring(i)
-}
diff --git a/detekt-generator/src/test/kotlin/io/gitlab/arturbosch/detekt/generator/collection/RuleCollectorSpec.kt b/detekt-generator/src/test/kotlin/io/gitlab/arturbosch/detekt/generator/collection/RuleCollectorSpec.kt
index 3543df91c..af6b02bef 100644
--- a/detekt-generator/src/test/kotlin/io/gitlab/arturbosch/detekt/generator/collection/RuleCollectorSpec.kt
+++ b/detekt-generator/src/test/kotlin/io/gitlab/arturbosch/detekt/generator/collection/RuleCollectorSpec.kt
@@ -151,114 +151,422 @@ class RuleCollectorSpec : Spek({
assertThat(items[0].aliases).isEqualTo("RULE, RULE2")
}
- it("contains no configuration options by default") {
- val code = """
- /**
- * description
- */
- class SomeRandomClass : Rule
- """
- val items = subject.run(code)
- assertThat(items[0].configuration).isEmpty()
- assertThat(items[0].requiresTypeResolution).isFalse()
+ describe("collects configuration options") {
+ describe("using annotation") {
+ it("contains no configuration options by default") {
+ val code = """
+ /**
+ * description
+ */
+ class SomeRandomClass : Rule
+ """
+ val items = subject.run(code)
+ assertThat(items[0].configuration).isEmpty()
+ }
+
+ it("contains one configuration option with correct formatting") {
+ val code = """
+ /**
+ * description
+ */
+ class SomeRandomClass() : Rule {
+ @Configuration("description")
+ private val config: String by config("[A-Z$]")
+ }
+ """
+ val items = subject.run(code)
+ assertThat(items[0].configuration).hasSize(1)
+ val expectedConfiguration = Configuration(
+ name = "config",
+ description = "description",
+ defaultValue = "'[A-Z$]'",
+ deprecated = null
+ )
+ assertThat(items[0].configuration[0]).isEqualTo(expectedConfiguration)
+ }
+
+ it("contains one configuration option of type Int") {
+ val code = """
+ /**
+ * description
+ */
+ class SomeRandomClass() : Rule {
+ @Configuration("description")
+ private val config: Int by config(99)
+ }
+ """
+ val items = subject.run(code)
+ assertThat(items[0].configuration).hasSize(1)
+ assertThat(items[0].configuration[0].defaultValue).isEqualTo("99")
+ }
+
+ it("extracts default value when defined with named parameter") {
+ val code = """
+ /**
+ * description
+ */
+ class SomeRandomClass() : Rule {
+ @Configuration("description")
+ private val config: Int by config(defaultValue = 99)
+ }
+ """
+ val items = subject.run(code)
+ assertThat(items[0].configuration[0].defaultValue).isEqualTo("99")
+ }
+
+ it("extracts default value for list of strings") {
+ val code = """
+ /**
+ * description
+ */
+ class SomeRandomClass() : Rule {
+ @Configuration("description")
+ private val config: List by config(
+ listOf(
+ "a",
+ "b"
+ )
+ )
+ }
+ """
+ val items = subject.run(code)
+ assertThat(items[0].configuration[0].defaultValue).isEqualTo("['a', 'b']")
+ }
+
+ it("contains multiple configuration options") {
+ val code = """
+ /**
+ * description
+ */
+ class SomeRandomClass() : Rule {
+ @Configuration("description")
+ private val config: String by config("")
+
+ @Configuration("description")
+ private val config2: String by config("")
+ }
+ """
+ val items = subject.run(code)
+ assertThat(items[0].configuration).hasSize(2)
+ }
+
+ it("has description that is concatenated") {
+ val code = """
+ /**
+ * description
+ */
+ class SomeRandomClass() : Rule {
+ @Configuration(
+ "This is a " +
+ "multi line " +
+ "description")
+ private val config: String by config("a")
+ }
+ """
+ val items = subject.run(code)
+ assertThat(items[0].configuration[0].description).isEqualTo("This is a multi line description")
+ assertThat(items[0].configuration[0].defaultValue).isEqualTo("'a'")
+ }
+
+ it("extracts default value when it is an Int constant") {
+ val code = """
+ /**
+ * description
+ */
+ class SomeRandomClass() : Rule {
+ @Configuration("description")
+ private val config: Int by config(DEFAULT_CONFIG_VALUE)
+
+ companion object {
+ private const val DEFAULT_CONFIG_VALUE = 99
+ }
+ }
+ """
+ val items = subject.run(code)
+ assertThat(items[0].configuration[0].defaultValue).isEqualTo("99")
+ }
+
+ it("extracts default value when it is an Int constant as named parameter") {
+ val code = """
+ /**
+ * description
+ */
+ class SomeRandomClass() : Rule {
+ @Configuration("description")
+ private val config: Int by config(defaultValue = DEFAULT_CONFIG_VALUE)
+
+ companion object {
+ private const val DEFAULT_CONFIG_VALUE = 99
+ }
+ }
+ """
+ val items = subject.run(code)
+ assertThat(items[0].configuration[0].defaultValue).isEqualTo("99")
+ }
+ it("extracts default value when it is a String constant") {
+ val code = """
+ /**
+ * description
+ */
+ class SomeRandomClass() : Rule {
+ @Configuration("description")
+ private val config: String by config(DEFAULT_CONFIG_VALUE)
+
+ companion object {
+ private const val DEFAULT_CONFIG_VALUE = "a"
+ }
+ }
+ """
+ val items = subject.run(code)
+ assertThat(items[0].configuration[0].defaultValue).isEqualTo("'a'")
+ }
+ it("extracts default value for list of strings from constant") {
+ val code = """
+ /**
+ * description
+ */
+ class SomeRandomClass() : Rule {
+ @Configuration("description")
+ private val config1: List by config(DEFAULT_CONFIG_VALUE)
+
+ @Configuration("description")
+ private val config2: List by config(listOf(DEFAULT_CONFIG_VALUE_A, "b"))
+
+ companion object {
+ private val DEFAULT_CONFIG_VALUE = listOf("a", "b")
+ private val DEFAULT_CONFIG_VALUE_A = "a"
+ }
+ }
+ """
+ val items = subject.run(code)
+ val expected = "['a', 'b']"
+ assertThat(items[0].configuration[0].defaultValue).isEqualTo(expected)
+ assertThat(items[0].configuration[1].defaultValue).isEqualTo(expected)
+ }
+
+ it("extracts emptyList default value") {
+ val code = """
+ /**
+ * description
+ */
+ class SomeRandomClass() : Rule {
+ @Configuration("description")
+ private val config1: List by config(listOf())
+
+ @Configuration("description")
+ private val config2: List by config(emptyList())
+
+ companion object {
+ private val DEFAULT_CONFIG_VALUE_A = "a"
+ }
+ }
+ """
+ val items = subject.run(code)
+ assertThat(items[0].configuration[0].defaultValue).isEqualTo("[]")
+ assertThat(items[0].configuration[1].defaultValue).isEqualTo("[]")
+ }
+
+ it("is marked as deprecated as well") {
+ val code = """
+ /**
+ * description
+ */
+ class SomeRandomClass() : Rule {
+ @Deprecated("use config1 instead")
+ @Configuration("description")
+ private val config: String by config("")
+ }
+ """
+ val items = subject.run(code)
+ assertThat(items[0].configuration[0].deprecated).isEqualTo("use config1 instead")
+ }
+
+ it("fails if annotation and kdoc are used both to define configuration") {
+ val code = """
+ /**
+ * description
+ * @configuration config1 - description (default: `''`)
+ */
+ class SomeRandomClass() : Rule {
+ @Configuration("description")
+ private val config: String by config("")
+ }
+ """
+ assertThatExceptionOfType(InvalidDocumentationException::class.java).isThrownBy { subject.run(code) }
+ }
+
+ it("fails if not used in combination with delegate") {
+ val code = """
+ /**
+ * description
+ */
+ class SomeRandomClass() : Rule {
+ @Configuration("description")
+ private val config: String = "foo"
+ }
+ """
+ assertThatExceptionOfType(InvalidDocumentationException::class.java).isThrownBy { subject.run(code) }
+ }
+
+ it("fails if not used in combination with config delegate") {
+ val code = """
+ /**
+ * description
+ */
+ class SomeRandomClass() : Rule {
+ @Configuration("description")
+ private val config: String by lazy { "foo" }
+ }
+ """
+ assertThatExceptionOfType(InvalidDocumentationException::class.java).isThrownBy { subject.run(code) }
+ }
+
+ it("fails if config delegate is used without annotation") {
+ val code = """
+ /**
+ * description
+ */
+ class SomeRandomClass() : Rule {
+ private val config: String by config("")
+ }
+ """
+ assertThatExceptionOfType(InvalidDocumentationException::class.java).isThrownBy { subject.run(code) }
+ }
+
+ it("fails if config delegate is used with unsupported type") {
+ val code = """
+ /**
+ * description
+ */
+ class SomeRandomClass() : Rule {
+ @Configuration("description")
+ private val config: List by config(listOf(1, 2))
+ }
+ """
+ assertThatExceptionOfType(InvalidDocumentationException::class.java).isThrownBy { subject.run(code) }
+ }
+ }
+
+ describe("as part of kdoc") {
+ it("contains no configuration options by default") {
+ val code = """
+ /**
+ * description
+ */
+ class SomeRandomClass : Rule
+ """
+ val items = subject.run(code)
+ assertThat(items[0].configuration).isEmpty()
+ }
+
+ it("contains one configuration option with correct formatting") {
+ val code = """
+ /**
+ * description
+ * @configuration config - description (default: `'[A-Z$]'`)
+ */
+ class SomeRandomClass : Rule
+ """
+ val items = subject.run(code)
+ assertThat(items[0].configuration).hasSize(1)
+ assertThat(items[0].configuration[0].name).isEqualTo("config")
+ assertThat(items[0].configuration[0].description).isEqualTo("description")
+ assertThat(items[0].configuration[0].defaultValue).isEqualTo("'[A-Z$]'")
+ }
+
+ it("contains multiple configuration options") {
+ val code = """
+ /**
+ * description
+ * @configuration config - description (default: `''`)
+ * @configuration config2 - description2 (default: `''`)
+ */
+ class SomeRandomClass: Rule
+ """
+ val items = subject.run(code)
+ assertThat(items[0].configuration).hasSize(2)
+ }
+ it("config option doesn't have a default value") {
+ val code = """
+ /**
+ * description
+ * @configuration config - description
+ */
+ class SomeRandomClass : Rule
+ """
+ assertThatExceptionOfType(InvalidDocumentationException::class.java).isThrownBy { subject.run(code) }
+ }
+
+ it("has a blank default value") {
+ val code = """
+ /**
+ * description
+ * @configuration config - description (default: ``)
+ */
+ class SomeRandomClass : Rule
+ """
+ assertThatExceptionOfType(InvalidDocumentationException::class.java).isThrownBy { subject.run(code) }
+ }
+
+ it("has an incorrectly delimited default value") {
+ val code = """
+ /**
+ * description
+ * @configuration config - description (default: true)
+ */
+ class SomeRandomClass : Rule
+ """
+ assertThatExceptionOfType(InvalidDocumentationException::class.java).isThrownBy { subject.run(code) }
+ }
+
+ it("contains a misconfigured configuration option") {
+ val code = """
+ /**
+ * description
+ * @configuration something: description
+ */
+ class SomeRandomClass : Rule
+ """
+ assertThatExceptionOfType(InvalidDocumentationException::class.java).isThrownBy { subject.run(code) }
+ }
+ }
}
- it("contains one configuration option with correct formatting") {
- val code = """
- /**
- * description
- * @configuration config - description (default: `'[A-Z$]'`)
- */
- class SomeRandomClass : Rule
- """
- val items = subject.run(code)
- assertThat(items[0].configuration).hasSize(1)
- assertThat(items[0].configuration[0].name).isEqualTo("config")
- assertThat(items[0].configuration[0].description).isEqualTo("description")
- assertThat(items[0].configuration[0].defaultValue).isEqualTo("'[A-Z$]'")
- }
+ describe("collects type resolution information") {
+ it("has no type resolution by default") {
+ val code = """
+ /**
+ * description
+ */
+ class SomeRandomClass : Rule
+ """
+ val items = subject.run(code)
+ assertThat(items[0].requiresTypeResolution).isFalse()
+ }
- it("contains multiple configuration options") {
- val code = """
- /**
- * description
- * @configuration config - description (default: `''`)
- * @configuration config2 - description2 (default: `''`)
- */
- class SomeRandomClass: Rule
- """
- val items = subject.run(code)
- assertThat(items[0].configuration).hasSize(2)
- }
+ it("collects the flag that it requires type resolution") {
+ val code = """
+ /**
+ * description
+ */
+ @RequiresTypeResolution
+ class SomeRandomClass : Rule
+ """
+ val items = subject.run(code)
+ assertThat(items[0].requiresTypeResolution).isTrue()
+ }
- it("collects the flag that it requires type resolution") {
- val code = """
- import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
-
- /**
- * description
- */
- @RequiresTypeResolution
- class SomeRandomClass : Rule
- """
- val items = subject.run(code)
- assertThat(items[0].requiresTypeResolution).isTrue()
- }
-
- it("collects the flag that it requires type resolution from fully qualified annotation") {
- val code = """
- /**
- * description
- */
- @io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
- class SomeRandomClass : Rule
- """
- val items = subject.run(code)
- assertThat(items[0].requiresTypeResolution).isTrue()
- }
-
- it("config option doesn't have a default value") {
- val code = """
- /**
- * description
- * @configuration config - description
- */
- class SomeRandomClass : Rule
- """
- assertThatExceptionOfType(InvalidDocumentationException::class.java).isThrownBy { subject.run(code) }
- }
-
- it("has a blank default value") {
- val code = """
- /**
- * description
- * @configuration config - description (default: ``)
- */
- class SomeRandomClass : Rule
- """
- assertThatExceptionOfType(InvalidDocumentationException::class.java).isThrownBy { subject.run(code) }
- }
-
- it("has an incorrectly delimited default value") {
- val code = """
- /**
- * description
- * @configuration config - description (default: true)
- */
- class SomeRandomClass : Rule
- """
- assertThatExceptionOfType(InvalidDocumentationException::class.java).isThrownBy { subject.run(code) }
- }
-
- it("contains a misconfigured configuration option") {
- val code = """
- /**
- * description
- * @configuration something: description
- */
- class SomeRandomClass : Rule
- """
- assertThatExceptionOfType(InvalidDocumentationException::class.java).isThrownBy { subject.run(code) }
+ it("collects the flag that it requires type resolution from fully qualified annotation") {
+ val code = """
+ /**
+ * description
+ */
+ @io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
+ class SomeRandomClass : Rule
+ """
+ val items = subject.run(code)
+ assertThat(items[0].requiresTypeResolution).isTrue()
+ }
}
it("contains compliant and noncompliant code examples") {
diff --git a/detekt-rules-complexity/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/complexity/ComplexMethod.kt b/detekt-rules-complexity/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/complexity/ComplexMethod.kt
index ec1993fba..cee72b4d2 100644
--- a/detekt-rules-complexity/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/complexity/ComplexMethod.kt
+++ b/detekt-rules-complexity/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/complexity/ComplexMethod.kt
@@ -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 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 = 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"
+ )
}
}
diff --git a/detekt-rules-complexity/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/complexity/ComplexMethodSpec.kt b/detekt-rules-complexity/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/complexity/ComplexMethodSpec.kt
index 0eab62815..e1f893a0a 100644
--- a/detekt-rules-complexity/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/complexity/ComplexMethodSpec.kt
+++ b/detekt-rules-complexity/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/complexity/ComplexMethodSpec.kt
@@ -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 = 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))