Repository: detekt/detekt (kotlin)

Issue:
Add the option to instantiate the same rule more than once
This is a feature that we want to have for detekt 2.0

## Needed changes
There are multiple changes that should be made to make this possible. Here a list with all the changes that we need:

1. `RuleSet`s shouldn't instantiate the rules. They should provider factories to pass a confguration to create that rule.
    - **State:** #6733
1. `Rule`s shouldn't pick their own configuration from its parent configuration. We should provide their configuration directly.
    - **State:** #6767
1. The `Rule` id can't be hardcoded. If a rule can have multiple instances the id should change between those instances.
    - **State:** No started
1. The `Config` class should provide an API to read all the subconfigurations inside it.
    - **State:** #6842
1. The core should read the `Config` and instantiate the rules from there.
    - **State:** This should be the easiest. We need to align on the new yaml key to find the rule but the change should be as easy as this:
```kotlin
.flatMap { (ruleSet, ruleSetConfig) ->
    ruleSetConfig.subconfigs
        .map { (configId, config) ->
            val ruleId = config.valueOrNull("rule") ?: configId
            val ruleProvider = checkNotNull(ruleSet.rules[ruleId]) {
                "Unknown rule ${ruleSet.id}:$ruleId"
            }
            ruleProvider.invoke(config)
        }
}
```

----

This is a huge change. There is code that assumes that the rules have a hardcoded id or that the there is only one instance of each. And this is a breaking change, we need to change the API for 3 main classes/interfaces: `RuleSet`, `Rule` and `Config`. This will impact every single third party rule out there.

Also, those are the big "chunks" that should be done, then a lot of minor changes should be made on the way to be able to make those possible.

Pull request:
With this PR we finally are able to run the same run multiple times with different configurations.

The most important part to discuss, I think, is if we like how those new instances are configured.

Right now if you want to create a new instance of the same rule you should add on your `detekt.yml` something like this:

```yaml
styles:
  ForbiddenImport/compose:
    active: true
    ...
```

Close #6738

Patch:
diff --git a/detekt-cli/src/main/kotlin/io/gitlab/arturbosch/detekt/cli/CliArgs.kt b/detekt-cli/src/main/kotlin/io/gitlab/arturbosch/detekt/cli/CliArgs.kt
index e290a603da6e..7dc5b97311e8 100644
--- a/detekt-cli/src/main/kotlin/io/gitlab/arturbosch/detekt/cli/CliArgs.kt
+++ b/detekt-cli/src/main/kotlin/io/gitlab/arturbosch/detekt/cli/CliArgs.kt
@@ -166,7 +166,7 @@ class CliArgs {
 
     @Parameter(
         names = ["--run-rule"],
-        description = "Specify a rule by [RuleSet:Rule] pattern and run it on input.",
+        description = "Specify a rule by [RuleSet:RuleId] pattern and run it on input.",
         hidden = true
     )
     var runRule: String? = null
diff --git a/detekt-cli/src/main/kotlin/io/gitlab/arturbosch/detekt/cli/Spec.kt b/detekt-cli/src/main/kotlin/io/gitlab/arturbosch/detekt/cli/Spec.kt
index 22b533f091e1..46f8db05ee06 100644
--- a/detekt-cli/src/main/kotlin/io/gitlab/arturbosch/detekt/cli/Spec.kt
+++ b/detekt-cli/src/main/kotlin/io/gitlab/arturbosch/detekt/cli/Spec.kt
@@ -5,7 +5,6 @@ import io.github.detekt.tooling.api.spec.RulesSpec
 import io.github.detekt.tooling.api.spec.RulesSpec.RunPolicy.DisableDefaultRuleSets
 import io.github.detekt.tooling.api.spec.RulesSpec.RunPolicy.NoRestrictions
 import io.github.detekt.utils.PathFilters
-import io.gitlab.arturbosch.detekt.api.Rule
 import io.gitlab.arturbosch.detekt.api.RuleSet
 import java.nio.file.Path
 import kotlin.io.path.ExperimentalPathApi
@@ -103,5 +102,5 @@ private fun CliArgs.toRunPolicy(): RulesSpec.RunPolicy {
     val parts = runRule?.split(":")
         ?: return if (disableDefaultRuleSets) DisableDefaultRuleSets else NoRestrictions
     require(parts.size == 2) { "Pattern 'RuleSetId:RuleName' expected." }
-    return RulesSpec.RunPolicy.RestrictToSingleRule(RuleSet.Id(parts[0]), Rule.Name(parts[1]))
+    return RulesSpec.RunPolicy.RestrictToSingleRule(RuleSet.Id(parts[0]), parts[1])
 }
diff --git a/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/Analyzer.kt b/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/Analyzer.kt
index 0582114a1ab1..f6aef08329b9 100644
--- a/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/Analyzer.kt
+++ b/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/Analyzer.kt
@@ -97,15 +97,23 @@ internal class Analyzer(
             .flatMap { (ruleSet, ruleSetConfig) ->
                 ruleSetConfig.subConfigKeys()
                     .asSequence()
-                    .mapNotNull { runCatching { Rule.Name(it) }.getOrNull() }
-                    .mapNotNull { ruleName ->
-                        ruleSet.rules[ruleName]?.let { it to ruleSetConfig.subConfig(ruleName.value) }
+                    .mapNotNull { ruleId ->
+                        extractRuleName(ruleId)?.let { ruleName -> ruleId to ruleName }
                     }
-                    .filter { (_, config) -> config.isActiveOrDefault(false) }
-                    .filter { (_, config) -> config.shouldAnalyzeFile(file, settings.spec.projectSpec.basePath) }
-                    .map { (ruleProvider, config) ->
+                    .mapNotNull { (ruleId, ruleName) ->
+                        ruleSet.rules[ruleName]?.let { ruleProvider ->
+                            RuleDescriptor(
+                                ruleProvider = ruleProvider,
+                                config = ruleSetConfig.subConfig(ruleId),
+                                ruleId = ruleId,
+                            )
+                        }
+                    }
+                    .filter { (_, config, _) -> config.isActiveOrDefault(false) }
+                    .filter { (_, config, _) -> config.shouldAnalyzeFile(file, settings.spec.projectSpec.basePath) }
+                    .map { (ruleProvider, config, ruleId) ->
                         val rule = ruleProvider(config)
-                        rule.toRuleInstance(rule.ruleName.value, ruleSet.id) to rule
+                        rule.toRuleInstance(ruleId, ruleSet.id) to rule
                     }
             }
             .filterNot { (ruleInstance, rule) ->
@@ -145,6 +153,11 @@ internal class Analyzer(
     }
 }
 
+internal fun extractRuleName(key: String): Rule.Name? =
+    runCatching { Rule.Name(key.split("/", limit = 2).first()) }.getOrNull()
+
+private data class RuleDescriptor(val ruleProvider: (Config) -> Rule, val config: Config, val ruleId: String)
+
 private fun List<Finding>.filterSuppressedFindings(rule: Rule, bindingContext: BindingContext): List<Finding> {
     val suppressors = buildSuppressors(rule, bindingContext)
     return if (suppressors.isNotEmpty()) {
diff --git a/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/config/validation/InvalidPropertiesConfigValidator.kt b/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/config/validation/InvalidPropertiesConfigValidator.kt
index 79b0395f3714..f3b7bc0bdc69 100644
--- a/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/config/validation/InvalidPropertiesConfigValidator.kt
+++ b/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/config/validation/InvalidPropertiesConfigValidator.kt
@@ -2,6 +2,7 @@ package io.gitlab.arturbosch.detekt.core.config.validation
 
 import io.gitlab.arturbosch.detekt.api.Notification
 import io.gitlab.arturbosch.detekt.core.config.YamlConfig
+import io.gitlab.arturbosch.detekt.core.extractRuleName
 import io.gitlab.arturbosch.detekt.core.util.SimpleNotification
 
 internal class InvalidPropertiesConfigValidator(
@@ -46,7 +47,7 @@ internal class InvalidPropertiesConfigValidator(
         return notifications
     }
 
-    @Suppress("UNCHECKED_CAST")
+    @Suppress("UNCHECKED_CAST", "ReturnCount")
     private fun checkProp(
         propertyName: String,
         propertyPath: String,
@@ -54,7 +55,10 @@ internal class InvalidPropertiesConfigValidator(
         baseline: Map<String, Any>
     ): List<Notification> {
         if (!baseline.contains(propertyName)) {
-            return listOf(propertyDoesNotExists(propertyPath))
+            val ruleName = extractRuleName(propertyName)
+            if (ruleName == null || !baseline.contains(ruleName.value)) {
+                return listOf(propertyDoesNotExists(propertyPath))
+            }
         }
         if (configToValidate[propertyName] is String && baseline[propertyName] is List<*>) {
             return listOf(propertyShouldBeAnArray(propertyPath))
diff --git a/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/rules/RuleSets.kt b/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/rules/RuleSets.kt
index aa1acf784788..bf88ee138a46 100644
--- a/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/rules/RuleSets.kt
+++ b/detekt-core/src/main/kotlin/io/gitlab/arturbosch/detekt/core/rules/RuleSets.kt
@@ -5,6 +5,7 @@ import io.gitlab.arturbosch.detekt.api.RuleSetProvider
 import io.gitlab.arturbosch.detekt.api.internal.DefaultRuleSetProvider
 import io.gitlab.arturbosch.detekt.core.ProcessingSettings
 import io.gitlab.arturbosch.detekt.core.extensions.LIST_ITEM_SPACING
+import io.gitlab.arturbosch.detekt.core.extractRuleName
 import java.util.ServiceLoader
 
 fun ProcessingSettings.createRuleProviders(): List<RuleSetProvider> {
@@ -20,10 +21,13 @@ fun ProcessingSettings.createRuleProviders(): List<RuleSetProvider> {
 
         is RulesSpec.RunPolicy.RestrictToSingleRule -> {
             val ruleSetId = runPolicy.ruleSetId
-            val ruleName = runPolicy.ruleName
+            val ruleId = runPolicy.ruleId
             val realProvider = requireNotNull(ruleSetProviders.find { it.ruleSetId == ruleSetId }) {
                 "There was no rule set with id '$ruleSetId'."
             }
+            val ruleName = requireNotNull(extractRuleName(ruleId)) {
+                "There was not rule '$ruleId' in rule set '$ruleSetId'."
+            }
             listOf(SingleRuleProvider(ruleName, realProvider))
         }
     }
diff --git a/detekt-tooling/api/detekt-tooling.api b/detekt-tooling/api/detekt-tooling.api
index 50164e7618a9..4bb2daae1611 100644
--- a/detekt-tooling/api/detekt-tooling.api
+++ b/detekt-tooling/api/detekt-tooling.api
@@ -213,8 +213,8 @@ public final class io/github/detekt/tooling/api/spec/RulesSpec$RunPolicy$NoRestr
 }
 
 public final class io/github/detekt/tooling/api/spec/RulesSpec$RunPolicy$RestrictToSingleRule : io/github/detekt/tooling/api/spec/RulesSpec$RunPolicy {
-	public fun <init> (Lio/gitlab/arturbosch/detekt/api/RuleSet$Id;Lio/gitlab/arturbosch/detekt/api/Rule$Name;)V
-	public final fun getRuleName ()Lio/gitlab/arturbosch/detekt/api/Rule$Name;
+	public fun <init> (Lio/gitlab/arturbosch/detekt/api/RuleSet$Id;Ljava/lang/String;)V
+	public final fun getRuleId ()Ljava/lang/String;
 	public final fun getRuleSetId ()Lio/gitlab/arturbosch/detekt/api/RuleSet$Id;
 }
 
diff --git a/detekt-tooling/src/main/kotlin/io/github/detekt/tooling/api/spec/RulesSpec.kt b/detekt-tooling/src/main/kotlin/io/github/detekt/tooling/api/spec/RulesSpec.kt
index 494e3135d5d8..c12f1460f361 100644
--- a/detekt-tooling/src/main/kotlin/io/github/detekt/tooling/api/spec/RulesSpec.kt
+++ b/detekt-tooling/src/main/kotlin/io/github/detekt/tooling/api/spec/RulesSpec.kt
@@ -67,6 +67,6 @@ interface RulesSpec {
         /**
          * Run a single rule.
          */
-        class RestrictToSingleRule(val ruleSetId: RuleSet.Id, val ruleName: Rule.Name) : RunPolicy()
+        class RestrictToSingleRule(val ruleSetId: RuleSet.Id, val ruleId: String) : RunPolicy()
     }
 }


Test patch:
diff --git a/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/AnalyzerSpec.kt b/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/AnalyzerSpec.kt
index 5557e2d26294..2e8e74b2b15f 100644
--- a/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/AnalyzerSpec.kt
+++ b/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/AnalyzerSpec.kt
@@ -135,6 +135,30 @@ class AnalyzerSpec(val env: KotlinCoreEnvironment) {
             )
         }
 
+        @Test
+        fun `with multiple instances of same rule`() {
+            val testFile = path.resolve("Test.kt")
+            val output = StringPrintStream()
+            val settings = createProcessingSettings(
+                testFile,
+                yamlConfigFromContent(
+                    """
+                        custom:
+                          MaxLineLength/foo:
+                            active: true
+                            maxLineLength: 30
+                          MaxLineLength/bar:
+                            active: true
+                            maxLineLength: 30
+                    """.trimIndent()
+                ),
+                outputChannel = output,
+            )
+            val analyzer = Analyzer(settings, listOf(CustomRuleSetProvider()), emptyList())
+
+            assertThat(settings.use { analyzer.run(listOf(compileForTest(testFile))) }).hasSize(2)
+        }
+
         @Test
         fun `with findings and context binding`() {
             val testFile = path.resolve("Test.kt")
diff --git a/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/config/validation/InvalidPropertiesConfigValidatorSpec.kt b/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/config/validation/InvalidPropertiesConfigValidatorSpec.kt
index b15454008b80..703a4af11e47 100644
--- a/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/config/validation/InvalidPropertiesConfigValidatorSpec.kt
+++ b/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/config/validation/InvalidPropertiesConfigValidatorSpec.kt
@@ -39,6 +39,12 @@ internal class InvalidPropertiesConfigValidatorSpec {
         assertThat(result).isEmpty()
     }
 
+    @Test
+    fun `passes for rules defined with rule id`() {
+        val result = subject.validate(yamlConfig("config_validation/rule-id.yml"))
+        assertThat(result).isEmpty()
+    }
+
     @Test
     fun `reports different rule set name`() {
         val result = subject.validate(yamlConfig("config_validation/other-ruleset-name.yml"))
diff --git a/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/rules/RuleSetsSpec.kt b/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/rules/RuleSetsSpec.kt
index 95464c359e9c..2c04360623d7 100644
--- a/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/rules/RuleSetsSpec.kt
+++ b/detekt-core/src/test/kotlin/io/gitlab/arturbosch/detekt/core/rules/RuleSetsSpec.kt
@@ -10,6 +10,8 @@ import io.gitlab.arturbosch.detekt.core.tooling.withSettings
 import org.assertj.core.api.Assertions.assertThat
 import org.assertj.core.api.Assertions.assertThatThrownBy
 import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
 
 class RuleSetsSpec {
 
@@ -45,10 +47,11 @@ class RuleSetsSpec {
             .containsExactlyInAnyOrder("sample-rule-set")
     }
 
-    @Test
-    fun `only loads the provider with the selected rule`() {
+    @ParameterizedTest
+    @ValueSource(strings = ["MagicNumber", "MagicNumber/id"])
+    fun `only loads the provider with the selected rule`(ruleId: String) {
         val providers = createNullLoggingSpec {
-            rules { runPolicy = RestrictToSingleRule(RuleSet.Id("style"), Rule.Name("MagicNumber")) }
+            rules { runPolicy = RestrictToSingleRule(RuleSet.Id("style"), ruleId) }
         }
             .withSettings { createRuleProviders() }
 
@@ -58,11 +61,12 @@ class RuleSetsSpec {
             .containsOnlyKeys(Rule.Name("MagicNumber"))
     }
 
-    @Test
-    fun `throws when rule set doesn't exist`() {
+    @ParameterizedTest
+    @ValueSource(strings = ["MagicNumber", "MagicNumber/id"])
+    fun `throws when rule set doesn't exist`(ruleId: String) {
         assertThatThrownBy {
             createNullLoggingSpec {
-                rules { runPolicy = RestrictToSingleRule(RuleSet.Id("foo"), Rule.Name("MagicNumber")) }
+                rules { runPolicy = RestrictToSingleRule(RuleSet.Id("foo"), ruleId) }
             }
                 .withSettings { createRuleProviders() }
         }
diff --git a/detekt-core/src/test/resources/config_validation/rule-id.yml b/detekt-core/src/test/resources/config_validation/rule-id.yml
new file mode 100644
index 000000000000..c37c4c1e9c0e
--- /dev/null
+++ b/detekt-core/src/test/resources/config_validation/rule-id.yml
@@ -0,0 +1,8 @@
+style:
+  active: true
+  WildcardImport:
+    active: true
+  WildcardImport/extra:
+    active: true
+  WildcardImport/extra2:
+    active: true