Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for transformer function in config property delegate #3676

Merged
merged 2 commits into from
May 12, 2021

Conversation

marschwar
Copy link
Contributor

@marschwar marschwar commented Apr 11, 2021

This relates to a comment by @BraisGabin in #3670 addressing situations in which the configured values must be transformed.

This PR:

  • allows config values to be converted into anything else using a transformer function (by lambda or method reference)
  • adds memoization to all config values (similar to LazyRegex)
  • increases test coverage for ConfigurationCollector and ConfigProperty
  • removes Long from supported config types as it is not supported by BaseConfig

Examples:

val aSet: Set<String> by config(listOf("a", "b")) { it.toSet() }

val aRegex: Regex by config("[0-9]*", String::toRegex)
    
val anotherRegex: Regex by configWithFallback("fallback", "[0-9]*") { it.toRegex() }

This would also resolve #3672

@codecov
Copy link

codecov bot commented Apr 11, 2021

Codecov Report

Merging #3676 (c23a7c7) into main (e41de94) will increase coverage by 0.04%.
The diff coverage is 100.00%.

Impacted file tree graph

@@             Coverage Diff              @@
##               main    #3676      +/-   ##
============================================
+ Coverage     78.75%   78.80%   +0.04%     
- Complexity     2900     2901       +1     
============================================
  Files           473      473              
  Lines          9336     9335       -1     
  Branches       1722     1723       +1     
============================================
+ Hits           7353     7356       +3     
+ Misses         1075     1073       -2     
+ Partials        908      906       -2     
Impacted Files Coverage Δ Complexity Δ
...b/arturbosch/detekt/api/internal/ConfigProperty.kt 100.00% <100.00%> (+5.55%) 0.00 <0.00> (ø)
...ekt/generator/collection/ConfigurationCollector.kt 77.10% <100.00%> (+1.82%) 29.00 <8.00> (+1.00)
...rturbosch/detekt/rules/complexity/ComplexMethod.kt 95.23% <100.00%> (-0.12%) 13.00 <0.00> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e41de94...c23a7c7. Read the comment docs.

@BraisGabin
Copy link
Member

I think that we need a it more flexibility. Something like:

fun <T : Any> config(
    defaultValue: String,
    map: ((String) -> T)? = null
): ReadOnlyProperty<ConfigAware, T> = SimpleConfigProperty(defaultValue)

And we need to think about caching these values. toRegex() can be a costly call if we call it a lot of times.

@marschwar
Copy link
Contributor Author

I think that should be possible. As long as we do not expand the types that are used for defaultValue we should be fine when it comes to inferring the default value.

@chao2zhang
Copy link
Member

And we need to think about caching these values. toRegex() can be a costly call if we call it a lot of times.

+1

@BraisGabin
Copy link
Member

What do you think about this:

fun config(
    defaultValue: List<String>
): ReadOnlyProperty<ConfigAware, List<String>> = ListConfigProperty(defaultValue) { it }

fun <T : Any> config(
    defaultValue: List<String>,
    transform: (List<String>) -> T
): ReadOnlyProperty<ConfigAware, T> = ListConfigProperty(defaultValue, transform)


fun <T : Any> config(
    defaultValue: T
): ReadOnlyProperty<ConfigAware, T> = TransformedConfigProperty(defaultValue) { it }

fun <T : Any, U : Any> config(
    defaultValue: T,
    transform: (T) -> U,
): ReadOnlyProperty<ConfigAware, U> = TransformedConfigProperty(defaultValue, transform)

private class TransformedConfigProperty<T : Any, U : Any>(
    private val defaultValue: T,
    private val transform: (T) -> U
) : ReadOnlyProperty<ConfigAware, U> {
    override fun getValue(thisRef: ConfigAware, property: KProperty<*>): U {
        return transform(thisRef.valueOrDefault(property.name, defaultValue))
    }
}

private class ListConfigProperty<T : Any>(
    private val defaultValue: List<String>,
    private val transform: (List<String>) -> T
) : ReadOnlyProperty<ConfigAware, T> {
    override fun getValue(thisRef: ConfigAware, property: KProperty<*>): T {
        return transform(thisRef.valueOrDefaultCommaSeparated(property.name, defaultValue))
    }
}

It would be nice to use default values intead of overloads but in this case it's not possible. But, for the consumer, they are the same.

@marschwar
Copy link
Contributor Author

I really like the idea, especially after renaming them to config, listConfig and fallbackConfig.

@marschwar
Copy link
Contributor Author

Thank you for pointing out that we need to make sure that the transformer is only evaluated once. I adapted the code from LazyRegex to the config property delegate.

@marschwar marschwar marked this pull request as ready for review May 8, 2021 22:08
@marschwar marschwar changed the title [DRAFT] Add support for transformer function in list property Add support for transformer function in config property delegate May 8, 2021
Comment on lines +73 to +74
val fallbackValue = getValueOrDefault(thisRef, fallbackPropertyName, defaultValue)
return transform(getValueOrDefault(thisRef, property.name, fallbackValue))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not something in this PR, but I will file an issue myself on changing the fallback value of having type () -> T instead of T - The benefit is that if the value is present, we do not need to initialize the fallback property.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth it? We will be initializing booleans, strings, List. They will have more less the same cost to instantiate than the lambda itself and we will need to run the lambda too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On top of that please consider that We have to extract the default value for the documentation without the benefit of a BindingContext. This might end up really complicated.

Copy link
Member

@chao2zhang chao2zhang May 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth it? We will be initializing booleans, strings, List.

I am looking for semantic correctness: If a property exists, we shouldn't read from the fallback property at all.

On top of that please consider that We have to extract the default value for the documentation without the benefit of a BindingContext. This might end up really complicated.

I don't fully get the benefit of a BindingContext, would you mind elaborate it more?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never mind. Reading your comment on my mobile I thought you were talking about something else. You were talking about something like this? I could add it to this PR or feel free to create an issue afterwards.

    override fun doGetValue(thisRef: ConfigAware, property: KProperty<*>): U {
        val fallbackValueSupplier = { getValueOrDefault(thisRef, fallbackPropertyName) { defaultValue } }
        return transform(getValueOrDefault(thisRef, property.name, fallbackValueSupplier))
    }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Yes that's what I meant.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am afraid I can't find a solution without actually invoking the supplier in getValueOrDefault which makes this exercise useless as we evaluate the fallback no matter what. I rather keep it the way it is if that is ok.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem. We can table this for now

}
it("extracts default value with method reference") {
val items = subject.run(code)
assertThat(items[0].configuration[1].defaultValue).isEqualTo("'false'")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to val needsQuotes = declaredTypeOrNull in TYPES_THAT_NEED_QUOTATION_FOR_DEFAULT, should we add quotes only when the source type is String?

I imagine the default value should be false without quotes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. The problem here is that without a binding context or reflection I don't now how to reliably determine if the default value is a string or not. This is "best effort" by looking at the property type which (with transformi) might be something else.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One possible alternative is using reified types for ConfigProperty.

@marschwar
Copy link
Contributor Author

Is there anything that needs to be done here before merging? I would like to continue with #3670 if possible.

@chao2zhang chao2zhang merged commit f6a15f7 into detekt:main May 12, 2021
@chao2zhang
Copy link
Member

All good. Thank you for the groundwork.

@marschwar marschwar deleted the mapping-config-property branch May 12, 2021 19:50
@chao2zhang chao2zhang added this to the 1.17.0 milestone May 12, 2021
chao2zhang pushed a commit to chao2zhang/detekt that referenced this pull request May 13, 2021
…ekt#3676)

Co-authored-by: Markus Schwarz <post@markus-schwarz.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add regex support for config delegate
3 participants