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 exhaustiveness check for config validation #5089

Merged
merged 10 commits into from Aug 21, 2022
@@ -1,6 +1,8 @@
package io.gitlab.arturbosch.detekt.core.config

import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.core.config.validation.ValidatableConfiguration
import io.gitlab.arturbosch.detekt.core.config.validation.validateConfig

@Suppress("UNCHECKED_CAST")
internal data class AllRulesConfig(
Expand Down
Expand Up @@ -2,6 +2,8 @@ package io.gitlab.arturbosch.detekt.core.config

import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Notification
import io.gitlab.arturbosch.detekt.core.config.validation.ValidatableConfiguration
import io.gitlab.arturbosch.detekt.core.config.validation.validateConfig

/**
* Wraps two different configuration which should be considered when retrieving properties.
Expand Down

This file was deleted.

Expand Up @@ -2,6 +2,8 @@ package io.gitlab.arturbosch.detekt.core.config

import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Notification
import io.gitlab.arturbosch.detekt.core.config.validation.ValidatableConfiguration
import io.gitlab.arturbosch.detekt.core.config.validation.validateConfig

@Suppress("UNCHECKED_CAST")
class DisabledAutoCorrectConfig(private val wrapped: Config) : Config, ValidatableConfiguration {
Expand Down

This file was deleted.

Expand Up @@ -5,6 +5,8 @@ package io.gitlab.arturbosch.detekt.core.config
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Config.Companion.CONFIG_SEPARATOR
import io.gitlab.arturbosch.detekt.api.Notification
import io.gitlab.arturbosch.detekt.core.config.validation.ValidatableConfiguration
import io.gitlab.arturbosch.detekt.core.config.validation.validateConfig
import org.yaml.snakeyaml.Yaml
import java.io.Reader
import java.nio.file.Path
Expand Down
@@ -0,0 +1,28 @@
package io.gitlab.arturbosch.detekt.core.config.validation

import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.ConfigValidator
import io.gitlab.arturbosch.detekt.api.Notification
import io.gitlab.arturbosch.detekt.core.config.YamlConfig

internal abstract class AbstractYamlConfigValidator : ConfigValidator {

override fun validate(config: Config): Collection<Notification> {
require(config is YamlConfig) {
val yamlConfigClass = YamlConfig::class.simpleName
val actualClass = config.javaClass.simpleName

"Only supported config is the $yamlConfigClass. Actual type is $actualClass"
}
val settings = ValidationSettings(
config.subConfig("config").valueOrDefault("checkExhaustiveness", false),
)

return validate(config, settings)
}

abstract fun validate(
configToValidate: YamlConfig,
settings: ValidationSettings
): Collection<Notification>
}
@@ -0,0 +1,118 @@
package io.gitlab.arturbosch.detekt.core.config.validation

import io.github.detekt.tooling.api.InvalidConfig
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.ConfigValidator
import io.gitlab.arturbosch.detekt.api.Notification
import io.gitlab.arturbosch.detekt.api.Notification.Level
import io.gitlab.arturbosch.detekt.api.internal.SimpleNotification
import io.gitlab.arturbosch.detekt.core.NL
import io.gitlab.arturbosch.detekt.core.ProcessingSettings
import io.gitlab.arturbosch.detekt.core.config.YamlConfig
import io.gitlab.arturbosch.detekt.core.extensions.loadExtensions
import io.gitlab.arturbosch.detekt.core.reporting.red
import io.gitlab.arturbosch.detekt.core.reporting.yellow

/**
* Known existing properties on rule's which my be absent in the default-detekt-config.yml.
*
* We need to predefine them as the user may not have already declared an 'config'-block
* in the configuration and we want to validate the config by default.
*/
internal val DEFAULT_PROPERTY_EXCLUDES = setOf(
".*>excludes",
".*>includes",
".*>active",
".*>.*>excludes",
".*>.*>includes",
".*>.*>active",
".*>.*>autoCorrect",
".*>severity",
".*>.*>severity",
"build>weights.*",
".*>.*>ignoreAnnotated",
".*>.*>ignoreFunction",
).joinToString(",")

internal fun checkConfiguration(settings: ProcessingSettings, baseline: Config) {
var shouldValidate = settings.spec.configSpec.shouldValidateBeforeAnalysis
if (shouldValidate == null) {
val props = settings.config.subConfig("config")
shouldValidate = props.valueOrDefault("validation", true)
}
if (shouldValidate) {
val validators =
loadExtensions<ConfigValidator>(settings) + DefaultPropertiesConfigValidator(settings, baseline)
val notifications = validators.flatMap { it.validate(settings.config) }
notifications.map(Notification::message).forEach(settings::info)
val errors = notifications.filter(Notification::isError)
if (errors.isNotEmpty()) {
val problems = notifications.joinToString(NL) { "\t- ${it.renderMessage()}" }
val propsString = if (errors.size == 1) "property" else "properties"
val title = "Run failed with ${errors.size} invalid config $propsString.".red()
throw InvalidConfig("$title$NL$problems")
}
}
}

internal fun validateConfig(
config: Config,
baseline: Config,
excludePatterns: Set<Regex>
): List<Notification> {
require(baseline != Config.empty) { "Cannot validate configuration based on an empty baseline config." }
require(baseline is YamlConfig) {
val yamlConfigClass = YamlConfig::class.simpleName
val actualClass = baseline.javaClass.simpleName

"Only supported baseline config is the $yamlConfigClass. Actual type is $actualClass"
}

if (config == Config.empty) {
return emptyList()
}

return when (config) {
is YamlConfig -> validateYamlConfig(config, baseline, excludePatterns)
is ValidatableConfiguration -> config.validate(baseline, excludePatterns)
else -> error("Unsupported config type for validation: '${config::class}'.")
}
}

private fun validateYamlConfig(
configToValidate: YamlConfig,
baseline: YamlConfig,
excludePatterns: Set<Regex>
): List<Notification> {
val deprecatedProperties = loadDeprecations()
val warningsAsErrors = configToValidate
.subConfig("config")
.valueOrDefault("warningsAsErrors", false)

val validators: List<ConfigValidator> = listOf(
InvalidPropertiesConfigValidator(baseline, deprecatedProperties.keys, excludePatterns),
DeprecatedPropertiesConfigValidator(deprecatedProperties),
MissingRulesConfigValidator(baseline, excludePatterns)
)

return validators
.flatMap { it.validate(configToValidate) }
.map { notification ->
notification.transformIf(warningsAsErrors && notification.level == Level.Warning) {
SimpleNotification(
message = notification.message,
level = Level.Error
)
}
}
}

private fun <T> T.transformIf(condition: Boolean, transform: () -> T): T =
if (condition) transform() else this

internal fun Notification.renderMessage(): String =
when (level) {
Level.Error -> message.red()
Level.Warning -> message.yellow()
Level.Info -> message
}
@@ -1,4 +1,4 @@
package io.gitlab.arturbosch.detekt.core.config
package io.gitlab.arturbosch.detekt.core.config.validation

import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.ConfigValidator
Expand All @@ -8,7 +8,7 @@ import io.gitlab.arturbosch.detekt.api.internal.DefaultRuleSetProvider
import io.gitlab.arturbosch.detekt.core.ProcessingSettings
import io.gitlab.arturbosch.detekt.core.rules.RuleSetLocator

class DefaultPropertiesConfigValidator(
internal class DefaultPropertiesConfigValidator(
private val settings: ProcessingSettings,
private val baseline: Config,
) : ConfigValidator {
Expand Down