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 index b0032f150..e2b7e2ca1 100644 --- 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 @@ -4,11 +4,25 @@ import io.gitlab.arturbosch.detekt.api.ConfigAware import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty -fun config(defaultValue: T): ReadOnlyProperty = - SimpleConfigProperty(defaultValue) +fun config( + defaultValue: T +): ReadOnlyProperty = config(defaultValue) { it } -fun configWithFallback(fallbackPropertyName: String, defaultValue: T): ReadOnlyProperty = - FallbackConfigProperty(fallbackPropertyName, defaultValue) +fun config( + defaultValue: T, + transformer: (T) -> U +): ReadOnlyProperty = TransformedConfigProperty(defaultValue, transformer) + +fun configWithFallback( + fallbackPropertyName: String, + defaultValue: T +): ReadOnlyProperty = configWithFallback(fallbackPropertyName, defaultValue) { it } + +fun configWithFallback( + fallbackPropertyName: String, + defaultValue: T, + transformer: (T) -> U +): ReadOnlyProperty = FallbackConfigProperty(fallbackPropertyName, defaultValue, transformer) private fun getValueOrDefault(configAware: ConfigAware, propertyName: String, defaultValue: T): T { @Suppress("UNCHECKED_CAST") @@ -23,26 +37,40 @@ private fun getValueOrDefault(configAware: ConfigAware, propertyName: } is String, is Boolean, - is Int, - is Long -> configAware.valueOrDefault(propertyName, defaultValue) + is Int -> configAware.valueOrDefault(propertyName, defaultValue) else -> error( "${defaultValue.javaClass} is not supported for delegated config property '$propertyName'. " + - "Use one of String, Boolean, Int, Long or List instead." + "Use one of String, Boolean, Int or List instead." ) } } -private class SimpleConfigProperty(private val defaultValue: T) : ReadOnlyProperty { - override fun getValue(thisRef: ConfigAware, property: KProperty<*>): T { - return getValueOrDefault(thisRef, property.name, defaultValue) +private abstract class MemoizedConfigProperty : ReadOnlyProperty { + private var value: U? = null + + override fun getValue(thisRef: ConfigAware, property: KProperty<*>): U { + return value ?: doGetValue(thisRef, property).also { value = it } + } + + abstract fun doGetValue(thisRef: ConfigAware, property: KProperty<*>): U +} + +private class TransformedConfigProperty( + private val defaultValue: T, + private val transform: (T) -> U +) : MemoizedConfigProperty() { + override fun doGetValue(thisRef: ConfigAware, property: KProperty<*>): U { + return transform(getValueOrDefault(thisRef, property.name, defaultValue)) } } -private class FallbackConfigProperty( +private class FallbackConfigProperty( private val fallbackPropertyName: String, - private val defaultValue: T -) : ReadOnlyProperty { - override fun getValue(thisRef: ConfigAware, property: KProperty<*>): T { - return getValueOrDefault(thisRef, property.name, getValueOrDefault(thisRef, fallbackPropertyName, defaultValue)) + private val defaultValue: T, + private val transform: (T) -> U +) : MemoizedConfigProperty() { + override fun doGetValue(thisRef: ConfigAware, property: KProperty<*>): U { + val fallbackValue = getValueOrDefault(thisRef, fallbackPropertyName, defaultValue) + return transform(getValueOrDefault(thisRef, property.name, fallbackValue)) } } diff --git a/detekt-api/src/test/kotlin/io/gitlab/arturbosch/detekt/api/internal/ConfigPropertySpec.kt b/detekt-api/src/test/kotlin/io/gitlab/arturbosch/detekt/api/internal/ConfigPropertySpec.kt index 415a058b2..427d6d116 100644 --- a/detekt-api/src/test/kotlin/io/gitlab/arturbosch/detekt/api/internal/ConfigPropertySpec.kt +++ b/detekt-api/src/test/kotlin/io/gitlab/arturbosch/detekt/api/internal/ConfigPropertySpec.kt @@ -8,152 +8,340 @@ import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.spekframework.spek2.Spek import org.spekframework.spek2.style.specification.describe - -private val A_REGEX = Regex("a-z") -private val A_LIST_OF_INTS = listOf(1) +import java.util.concurrent.atomic.AtomicInteger class ConfigPropertySpec : Spek({ describe("Config property delegate") { - context("string property") { - val configValue = "value" - val defaultValue = "default" - val subject by memoized { - object : TestConfigAware("present" to configValue) { - val present: String by config(defaultValue) - val notPresent: String by config(defaultValue) + context("simple property") { + context("String property") { + val configValue = "value" + val defaultValue = "default" + val subject by memoized { + object : TestConfigAware("present" to configValue) { + val present: String by config(defaultValue) + val notPresent: String by config(defaultValue) + } + } + it("uses the value provided in config if present") { + assertThat(subject.present).isEqualTo(configValue) + } + it("uses the default value if not present") { + assertThat(subject.notPresent).isEqualTo(defaultValue) } } - it("uses the value provided in config if present") { - assertThat(subject.present).isEqualTo(configValue) + context("Int property") { + val configValue = 99 + val defaultValue = -1 + + context("defined as number") { + val subject by memoized { + object : TestConfigAware("present" to configValue) { + val present: Int by config(defaultValue) + val notPresent: Int by config(defaultValue) + } + } + it("uses the value provided in config if present") { + assertThat(subject.present).isEqualTo(configValue) + } + it("uses the default value if not present") { + assertThat(subject.notPresent).isEqualTo(defaultValue) + } + } + context("defined as string") { + val subject by memoized { + object : TestConfigAware("present" to "$configValue") { + val present: Int by config(defaultValue) + } + } + it("uses the value provided in config if present") { + assertThat(subject.present).isEqualTo(configValue) + } + } } - it("uses the default value if not present") { - assertThat(subject.notPresent).isEqualTo(defaultValue) + context("Boolean property") { + val configValue by memoized { false } + val defaultValue by memoized { true } + + context("defined as Boolean") { + val subject by memoized { + object : TestConfigAware("present" to configValue) { + val present: Boolean by config(defaultValue) + val notPresent: Boolean by config(defaultValue) + } + } + it("uses the value provided in config if present") { + assertThat(subject.present).isEqualTo(configValue) + } + it("uses the default value if not present") { + assertThat(subject.notPresent).isEqualTo(defaultValue) + } + } + context("defined as string") { + val subject by memoized { + object : TestConfigAware("present" to "$configValue") { + val present: Boolean by config(defaultValue) + } + } + it("uses the value provided in config if present") { + assertThat(subject.present).isEqualTo(configValue) + } + } + } + context("List property") { + val defaultValue by memoized { listOf("x") } + context("defined as list") { + val subject by memoized { + object : TestConfigAware("present" to "a,b,c") { + val present: List by config(defaultValue) + val notPresent: List by config(defaultValue) + } + } + it("uses the value provided in config if present") { + assertThat(subject.present).isEqualTo(listOf("a", "b", "c")) + } + it("uses the default value if not present") { + assertThat(subject.notPresent).isEqualTo(defaultValue) + } + } + context("defined as comma separated string") { + val subject by memoized { + object : TestConfigAware("present" to "a,b,c") { + val present: List by config(defaultValue) + val notPresent: List by config(defaultValue) + } + } + it("uses the value provided in config if present") { + assertThat(subject.present).isEqualTo(listOf("a", "b", "c")) + } + it("uses the default value if not present") { + assertThat(subject.notPresent).isEqualTo(defaultValue) + } + } } } - context("Int property") { - val configValue = 99 - val defaultValue = -1 - val subject by memoized { - object : TestConfigAware("present" to configValue) { - val present: Int by config(defaultValue) - val notPresent: Int by config(defaultValue) + context("invalid type") { + context("Long") { + val defaultValue by memoized { 1L } + val subject by memoized { + object : TestConfigAware() { + val prop: Long by config(defaultValue) + } + } + it("throws") { + assertThatThrownBy { subject.prop } + .isInstanceOf(IllegalStateException::class.java) + .hasMessageContaining("is not supported") } } - it("uses the value provided in config if present") { - assertThat(subject.present).isEqualTo(configValue) + context("Regex") { + val defaultValue by memoized { Regex("a") } + val subject by memoized { + object : TestConfigAware() { + val prop: Regex by config(defaultValue) + } + } + it("throws") { + assertThatThrownBy { subject.prop } + .isInstanceOf(IllegalStateException::class.java) + .hasMessageContaining("is not supported") + } } - it("uses the default value if not present") { - assertThat(subject.notPresent).isEqualTo(defaultValue) + context("Set") { + val defaultValue by memoized { setOf("a") } + val subject by memoized { + object : TestConfigAware() { + val prop: Set by config(defaultValue) + } + } + it("throws") { + assertThatThrownBy { subject.prop } + .isInstanceOf(IllegalStateException::class.java) + .hasMessageContaining("is not supported") + } + } + context("List of Int") { + val defaultValue by memoized { listOf(1) } + val subject by memoized { + object : TestConfigAware() { + val prop: List by config(defaultValue) + } + } + it("throws") { + assertThatThrownBy { subject.prop } + .isInstanceOf(IllegalStateException::class.java) + .hasMessageContaining("lists of strings are supported") + } } } - context("Int property defined as string") { - val configValue = 99 - val subject by memoized { - object : TestConfigAware("present" to "$configValue") { - val present: Int by config(-1) + + context("transform") { + context("primitive") { + context("String property is transformed to regex") { + val defaultValue = ".*" + val configValue = "[a-z]+" + val subject by memoized { + object : TestConfigAware("present" to configValue) { + val present: Regex by config(defaultValue) { it.toRegex() } + val notPresent: Regex by config(defaultValue) { it.toRegex() } + } + } + it("applies the mapping function to the configured value") { + assertThat(subject.present.matches("abc")).isTrue + assertThat(subject.present.matches("123")).isFalse() + } + it("applies the mapping function to the default") { + assertThat(subject.notPresent.matches("abc")).isTrue + assertThat(subject.notPresent.matches("123")).isTrue + } + } + context("Int property is transformed to String") { + val configValue = 99 + val defaultValue = -1 + val subject by memoized { + object : TestConfigAware("present" to configValue) { + val present: String by config(defaultValue) { it.toString() } + val notPresent: String by config(defaultValue) { it.toString() } + } + } + it("applies the mapping function to the configured value") { + assertThat(subject.present).isEqualTo("$configValue") + } + it("applies the mapping function to the default") { + assertThat(subject.notPresent).isEqualTo("$defaultValue") + } + } + context("Boolean property is transformed to String") { + val configValue by memoized { true } + val defaultValue by memoized { false } + val subject by memoized { + object : TestConfigAware("present" to configValue) { + val present: String by config(defaultValue) { it.toString() } + val notPresent: String by config(defaultValue) { it.toString() } + } + } + it("applies the mapping function to the configured value") { + assertThat(subject.present).isEqualTo("$configValue") + } + it("applies the mapping function to the default") { + assertThat(subject.notPresent).isEqualTo("$defaultValue") + } + } + context("Boolean property is transformed to String with function reference") { + val defaultValue by memoized { false } + val subject by memoized { + object : TestConfigAware() { + val prop1: String by config(defaultValue, Boolean::toString) + val prop2: String by config(transformer = Boolean::toString, defaultValue = defaultValue) + } + } + it("transforms properties") { + assertThat(subject.prop1).isEqualTo("$defaultValue") + assertThat(subject.prop2).isEqualTo("$defaultValue") + } } } - it("uses the value provided in config if present") { - assertThat(subject.present).isEqualTo(configValue) + context("list of strings") { + val defaultValue by memoized { listOf("99") } + val subject by memoized { + object : TestConfigAware("present" to "1,2,3") { + val present: Int by config(defaultValue) { it.sumBy(String::toInt) } + val notPresent: Int by config(defaultValue) { it.sumBy(String::toInt) } + } + } + it("applies transformer to list configured") { + assertThat(subject.present).isEqualTo(6) + } + it("applies transformer to default list") { + assertThat(subject.notPresent).isEqualTo(99) + } + } + context("empty list of strings") { + val subject by memoized { + object : TestConfigAware() { + val defaultValue: List = emptyList() + val prop1: List by config(defaultValue) { it.map(String::toInt) } + val prop2: List by config(listOf()) { it.map(String::toInt) } + } + } + it("can be defined as variable") { + assertThat(subject.prop1).isEmpty() + } + it("can be defined using listOf()") { + assertThat(subject.prop2).isEmpty() + } + } + context("memoization") { + val subject by memoized { + object : TestConfigAware() { + val counter = AtomicInteger(0) + val prop: String by config(1) { + counter.getAndIncrement() + it.toString() + } + + fun useProperty(): String { + return "something with $prop" + } + } + } + it("transformer is called only once") { + repeat(5) { + assertThat(subject.useProperty()).isEqualTo("something with 1") + } + assertThat(subject.counter.get()).isEqualTo(1) + } } } - context("Boolean property") { - val subject by memoized { - object : TestConfigAware("present" to false) { - val present: Boolean by config(true) - val notPresent: Boolean by config(true) + context("configWithFallback") { + context("primitive") { + val configValue = 99 + val defaultValue = 0 + val fallbackValue = -1 + val subject by memoized { + object : TestConfigAware("present" to "$configValue", "fallback" to fallbackValue) { + val present: Int by configWithFallback("fallback", defaultValue) + val notPresentWithFallback: Int by configWithFallback("fallback", defaultValue) + val notPresentFallbackMissing: Int by configWithFallback("missing", defaultValue) + } + } + it("uses the value provided in config if present") { + assertThat(subject.present).isEqualTo(configValue) + } + it("uses the value from fallback property if value is missing and fallback exists") { + assertThat(subject.notPresentWithFallback).isEqualTo(fallbackValue) + } + it("uses the default value if not present") { + assertThat(subject.notPresentFallbackMissing).isEqualTo(defaultValue) } } - it("uses the value provided in config if present") { - assertThat(subject.present).isEqualTo(false) - } - it("uses the default value if not present") { - assertThat(subject.notPresent).isEqualTo(true) - } - } - context("Boolean property defined as string") { - val subject by memoized { - object : TestConfigAware("present" to "false") { - val present: Boolean by config(true) - val notPresent: Boolean by config(true) + context("with transformation") { + val configValue = 99 + val defaultValue = 0 + val fallbackValue = -1 + val subject by memoized { + object : TestConfigAware("present" to configValue, "fallback" to fallbackValue) { + val present: String by configWithFallback("fallback", defaultValue) { v -> + v.toString() + } + val notPresentWithFallback: String by configWithFallback("fallback", defaultValue) { v -> + v.toString() + } + val notPresentFallbackMissing: String by configWithFallback("missing", defaultValue) { v -> + v.toString() + } + } } - } - it("uses the value provided in config if present") { - assertThat(subject.present).isEqualTo(false) - } - it("uses the default value if not present") { - assertThat(subject.notPresent).isEqualTo(true) - } - } - context("String list property") { - val defaultValue by memoized { listOf("x") } - val subject by memoized { - object : TestConfigAware("present" to listOf("a", "b", "c")) { - val present: List by config(defaultValue) - val notPresent: List by config(defaultValue) + it("uses the value provided in config if present") { + assertThat(subject.present).isEqualTo("$configValue") } - } - it("uses the value provided in config if present") { - assertThat(subject.present).isEqualTo(listOf("a", "b", "c")) - } - it("uses the default value if not present") { - assertThat(subject.notPresent).isEqualTo(defaultValue) - } - } - context("String list property defined as comma separated string") { - val defaultValue by memoized { listOf("x") } - val subject by memoized { - object : TestConfigAware("present" to "a,b,c") { - val present: List by config(defaultValue) - val notPresent: List by config(defaultValue) + it("uses the value from fallback property if value is missing and fallback exists") { + assertThat(subject.notPresentWithFallback).isEqualTo("$fallbackValue") } - } - it("uses the value provided in config if present") { - assertThat(subject.present).isEqualTo(listOf("a", "b", "c")) - } - it("uses the default value if not present") { - assertThat(subject.notPresent).isEqualTo(defaultValue) - } - } - context("Int property with fallback") { - val configValue = 99 - val defaultValue = 0 - val fallbackValue = -1 - val subject by memoized { - object : TestConfigAware("present" to "$configValue", "fallback" to fallbackValue) { - val present: Int by configWithFallback("fallback", defaultValue) - val notPresentWithFallback: Int by configWithFallback("fallback", defaultValue) - val notPresentFallbackMissing: Int by configWithFallback("missing", defaultValue) + it("uses the default value if not present") { + assertThat(subject.notPresentFallbackMissing).isEqualTo("$defaultValue") } } - it("uses the value provided in config if present") { - assertThat(subject.present).isEqualTo(configValue) - } - it("uses the value from fallback property if value is missing and fallback exists") { - assertThat(subject.notPresentWithFallback).isEqualTo(fallbackValue) - } - it("uses the default value if not present") { - assertThat(subject.notPresentFallbackMissing).isEqualTo(defaultValue) - } - } - context("Invalid property type") { - val subject by memoized { - object : TestConfigAware() { - val regexProp: Regex by config(A_REGEX) - val listProp: List by config(A_LIST_OF_INTS) - } - } - it("fails when invalid regex property is accessed") { - assertThatThrownBy { subject.regexProp } - .isInstanceOf(IllegalStateException::class.java) - .hasMessageContaining("kotlin.text.Regex is not supported") - } - it("fails when invalid list property is accessed") { - assertThatThrownBy { subject.listProp } - .isInstanceOf(IllegalStateException::class.java) - .hasMessageContaining("Only lists of strings are supported") - } } } }) 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 index 76c41d6e9..2939241ab 100644 --- 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 @@ -9,6 +9,7 @@ import org.jetbrains.kotlin.psi.KtCallExpression import org.jetbrains.kotlin.psi.KtConstantExpression import org.jetbrains.kotlin.psi.KtElement import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.psi.KtLambdaArgument import org.jetbrains.kotlin.psi.KtObjectDeclaration import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.KtPropertyDelegate @@ -75,12 +76,6 @@ class ConfigurationCollector { } 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 one of the config property delegates ($DELEGATE_NAMES)" } } @@ -123,14 +118,14 @@ class ConfigurationCollector { } private fun KtPropertyDelegate.getDefaultValueExpression(): KtExpression { - val callExpression = expression as KtCallExpression - val arguments = callExpression.valueArguments + val arguments = (expression as KtCallExpression).valueArguments.filterNot { it is KtLambdaArgument } if (arguments.size == 1) { return checkNotNull(arguments[0].getArgumentExpression()) } val defaultArgument = arguments .find { it.getArgumentName()?.text == DEFAULT_VALUE_ARGUMENT_NAME } - ?: arguments.last() + ?: if (property.isFallbackConfigDelegate()) arguments[1] else arguments.first() + return checkNotNull(defaultArgument.getArgumentExpression()) } @@ -151,7 +146,9 @@ class ConfigurationCollector { } fun isUsingInvalidFallbackReference(properties: List, fallbackPropertyName: String) = - properties.filter { it.isInitializedWithConfigDelegate() }.none { it.name == fallbackPropertyName } + properties + .filter { it.isInitializedWithConfigDelegate() } + .none { it.name == fallbackPropertyName } } companion object { @@ -163,11 +160,8 @@ class ConfigurationCollector { 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 const val TYPE_REGEX = "Regex" + private val TYPES_THAT_NEED_QUOTATION_FOR_DEFAULT = listOf(TYPE_STRING, TYPE_REGEX) private val KtPropertyDelegate.property: KtProperty get() = parent as KtProperty @@ -184,16 +178,10 @@ class ConfigurationCollector { private fun KtProperty.isInitializedWithConfigDelegate(): Boolean = delegate?.expression?.referenceExpression()?.text in DELEGATE_NAMES - 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'") - } + val needsQuotes = declaredTypeOrNull in TYPES_THAT_NEED_QUOTATION_FOR_DEFAULT + return if (needsQuotes) "'$defaultValue'" else defaultValue } private fun KtProperty.hasListDeclaration(): Boolean = 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 dd2ff3570..1a0ea16e9 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 @@ -10,7 +10,7 @@ import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.spekframework.spek2.Spek import org.spekframework.spek2.style.specification.describe -class RuleCollectorSpec : Spek({ +object RuleCollectorSpec : Spek({ val subject by memoized { RuleCollector() } @@ -369,9 +369,27 @@ class RuleCollectorSpec : Spek({ @Configuration("description") private val config2: List by config(emptyList()) + } + """ + val items = subject.run(code) + assertThat(items[0].configuration[0].defaultValue).isEqualTo("[]") + assertThat(items[0].configuration[1].defaultValue).isEqualTo("[]") + } + + it("extracts emptyList default value of transformed list") { + val code = """ + /** + * description + */ + class SomeRandomClass() : Rule { + @Configuration("description") + private val config1: List by config(listOf()) { it.map(String::toInt) } + + @Configuration("description") + private val config2: List by config(DEFAULT_CONFIG_VALUE) { it.map(String::toInt) } companion object { - private val DEFAULT_CONFIG_VALUE_A = "a" + private val DEFAULT_CONFIG_VALUE: List = emptyList() } } """ @@ -447,18 +465,6 @@ class RuleCollectorSpec : Spek({ 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) } - } context("fallback property") { it("extracts default value") { val code = """ @@ -499,6 +505,87 @@ class RuleCollectorSpec : Spek({ } } + it("reports an error if the property to fallback on exists but is not a config property") { + val code = """ + /** + * description + */ + class SomeRandomClass() : Rule { + private val prop: Int = 1 + @Configuration("description") + private val config: Int by configWithFallback("prop", 99) + } + """ + assertThatExceptionOfType(InvalidDocumentationException::class.java).isThrownBy { + subject.run( + code + ) + } + } + } + context("transformed property") { + val code = """ + /** + * description + */ + class SomeRandomClass() : Rule { + @Configuration("description") + private val config1: Regex by config("[a-z]+") { it.toRegex() } + @Configuration("description") + private val config2: String by config(false, Boolean::toString) + } + """ + it("extracts default value with transformer function") { + val items = subject.run(code) + assertThat(items[0].configuration[0].defaultValue).isEqualTo("'[a-z]+'") + } + it("extracts default value with method reference") { + val items = subject.run(code) + assertThat(items[0].configuration[1].defaultValue).isEqualTo("'false'") + } + } + context("fallback property") { + it("extracts default value") { + val code = """ + /** + * description + */ + class SomeRandomClass() : Rule { + @Configuration("description") + private val prop: Int by config(1) + @Configuration("description") + private val config1: Int by configWithFallback("prop", 99) + @Configuration("description") + private val config2: Int by configWithFallback(fallbackPropertyName = "prop", defaultValue = 99) + @Configuration("description") + private val config3: Int by configWithFallback(defaultValue = 99, fallbackPropertyName = "prop") + @Configuration("description") + private val config4: Long by configWithFallback("prop", 99, Int::toLong) + } + """ + val items = subject.run(code) + val fallbackProperties = items[0].configuration.filter { it.name.startsWith("config") } + assertThat(fallbackProperties).hasSize(4) + assertThat(fallbackProperties.map { it.defaultValue }).containsOnly("99") + } + + it("reports an error if the property to fallback on does not exist") { + val code = """ + /** + * description + */ + class SomeRandomClass() : Rule { + @Configuration("description") + private val config: Int by configWithFallback("prop", 99) + } + """ + assertThatExceptionOfType(InvalidDocumentationException::class.java).isThrownBy { + subject.run( + code + ) + } + } + it("reports an error if the property to fallback on exists but is not a config property") { val code = """ /** 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 cee72b4d2..e0c34fa67 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 @@ -53,7 +53,7 @@ class ComplexMethod(config: Config = Config.empty) : Rule(config) { 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) + private val nestingFunctions: Set by config(DEFAULT_NESTING_FUNCTIONS) { it.toSet() } override val issue = Issue( "ComplexMethod", @@ -62,8 +62,6 @@ class ComplexMethod(config: Config = Config.empty) : Rule(config) { Debt.TWENTY_MINS ) - private val nestingFunctionsAsSet: Set = nestingFunctions.toSet() - override fun visitNamedFunction(function: KtNamedFunction) { if (ignoreSingleWhenExpression && hasSingleWhenExpression(function.bodyExpression)) { return @@ -72,7 +70,7 @@ class ComplexMethod(config: Config = Config.empty) : Rule(config) { val complexity = CyclomaticComplexity.calculate(function) { this.ignoreSimpleWhenEntries = this@ComplexMethod.ignoreSimpleWhenEntries this.ignoreNestingFunctions = this@ComplexMethod.ignoreNestingFunctions - this.nestingFunctions = this@ComplexMethod.nestingFunctionsAsSet + this.nestingFunctions = this@ComplexMethod.nestingFunctions } if (complexity >= threshold) {