mirror of
https://github.com/jlengrand/detekt.git
synced 2026-03-10 08:11:23 +00:00
Add support for transformer function in config property delegate (#3676)
Co-authored-by: Markus Schwarz <post@markus-schwarz.net>
This commit is contained in:
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 = """
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user