Add support for transformer function in config property delegate (#3676)

Co-authored-by: Markus Schwarz <post@markus-schwarz.net>
This commit is contained in:
marschwar
2021-05-12 21:26:57 +02:00
committed by GitHub
parent 869bd42109
commit f6a15f73e0
5 changed files with 466 additions and 177 deletions

View File

@@ -4,11 +4,25 @@ import io.gitlab.arturbosch.detekt.api.ConfigAware
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
fun <T : Any> config(defaultValue: T): ReadOnlyProperty<ConfigAware, T> =
SimpleConfigProperty(defaultValue)
fun <T : Any> config(
defaultValue: T
): ReadOnlyProperty<ConfigAware, T> = config(defaultValue) { it }
fun <T : Any> configWithFallback(fallbackPropertyName: String, defaultValue: T): ReadOnlyProperty<ConfigAware, T> =
FallbackConfigProperty(fallbackPropertyName, defaultValue)
fun <T : Any, U : Any> config(
defaultValue: T,
transformer: (T) -> U
): ReadOnlyProperty<ConfigAware, U> = TransformedConfigProperty(defaultValue, transformer)
fun <T : Any> configWithFallback(
fallbackPropertyName: String,
defaultValue: T
): ReadOnlyProperty<ConfigAware, T> = configWithFallback(fallbackPropertyName, defaultValue) { it }
fun <T : Any, U : Any> configWithFallback(
fallbackPropertyName: String,
defaultValue: T,
transformer: (T) -> U
): ReadOnlyProperty<ConfigAware, U> = FallbackConfigProperty(fallbackPropertyName, defaultValue, transformer)
private fun <T : Any> getValueOrDefault(configAware: ConfigAware, propertyName: String, defaultValue: T): T {
@Suppress("UNCHECKED_CAST")
@@ -23,26 +37,40 @@ private fun <T : Any> 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<String> instead."
"Use one of String, Boolean, Int or List<String> instead."
)
}
}
private class SimpleConfigProperty<T : Any>(private val defaultValue: T) : ReadOnlyProperty<ConfigAware, T> {
override fun getValue(thisRef: ConfigAware, property: KProperty<*>): T {
return getValueOrDefault(thisRef, property.name, defaultValue)
private abstract class MemoizedConfigProperty<U : Any> : ReadOnlyProperty<ConfigAware, U> {
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<T : Any, U : Any>(
private val defaultValue: T,
private val transform: (T) -> U
) : MemoizedConfigProperty<U>() {
override fun doGetValue(thisRef: ConfigAware, property: KProperty<*>): U {
return transform(getValueOrDefault(thisRef, property.name, defaultValue))
}
}
private class FallbackConfigProperty<T : Any>(
private class FallbackConfigProperty<T : Any, U : Any>(
private val fallbackPropertyName: String,
private val defaultValue: T
) : ReadOnlyProperty<ConfigAware, T> {
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<U>() {
override fun doGetValue(thisRef: ConfigAware, property: KProperty<*>): U {
val fallbackValue = getValueOrDefault(thisRef, fallbackPropertyName, defaultValue)
return transform(getValueOrDefault(thisRef, property.name, fallbackValue))
}
}

View File

@@ -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<String> by config(defaultValue)
val notPresent: List<String> 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<String> by config(defaultValue)
val notPresent: List<String> 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<String> 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<Int> 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<String> = emptyList()
val prop1: List<Int> by config(defaultValue) { it.map(String::toInt) }
val prop2: List<Int> by config(listOf<String>()) { it.map(String::toInt) }
}
}
it("can be defined as variable") {
assertThat(subject.prop1).isEmpty()
}
it("can be defined using listOf<String>()") {
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<String> by config(defaultValue)
val notPresent: List<String> 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<String> by config(defaultValue)
val notPresent: List<String> 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<Int> 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")
}
}
}
})

View File

@@ -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<KtProperty>, 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<String>"
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 =

View File

@@ -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<String> 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<Int> by config(listOf<String>()) { it.map(String::toInt) }
@Configuration("description")
private val config2: List<Int> by config(DEFAULT_CONFIG_VALUE) { it.map(String::toInt) }
companion object {
private val DEFAULT_CONFIG_VALUE_A = "a"
private val DEFAULT_CONFIG_VALUE: List<String> = 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<Int> 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 = """
/**

View File

@@ -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<String> by config(DEFAULT_NESTING_FUNCTIONS)
private val nestingFunctions: Set<String> 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<String> = 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) {