Insert suppression for class-signature, function-signature and parameter-list-wrapping at a class or function level

Closes #2588
diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/api/KtlintRuleEngineSuppression.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/api/KtlintRuleEngineSuppression.kt
index 8ba6536..9a4a275 100644
--- a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/api/KtlintRuleEngineSuppression.kt
+++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/api/KtlintRuleEngineSuppression.kt
@@ -3,8 +3,10 @@
 import com.pinterest.ktlint.rule.engine.core.api.RuleId
 import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpace
 import com.pinterest.ktlint.rule.engine.internal.RuleExecutionContext
+import com.pinterest.ktlint.rule.engine.internal.findSuppressionTargetNodeFinder
 import com.pinterest.ktlint.rule.engine.internal.insertKtlintRuleSuppression
 import org.jetbrains.kotlin.com.intellij.lang.ASTNode
+import org.jetbrains.kotlin.utils.addToStdlib.applyIf
 
 /**
  * A [Suppress] annotation can only be inserted at specific locations. This function is intended for API Consumers. It updates given [code]
@@ -39,13 +41,14 @@
 
         is KtlintSuppressionAtOffset ->
             findLeafElementAt(suppression.offsetFromStartOf(text))
-                ?.let {
-                    if (it.isWhiteSpace()) {
-                        // A suppression can not be added at a whitespace element. Insert it at the parent instead
-                        it.treeParent
-                    } else {
-                        it
-                    }
+                ?.let { leafElement ->
+                    leafElement
+                        .applyIf(leafElement.isWhiteSpace()) {
+                            // A suppression can not be added at a whitespace element. Insert it at the parent instead
+                            leafElement.treeParent
+                        }
+                }?.let { leafElement ->
+                    suppression.findSuppressionTargetNodeFinder().findSuppressionTargetNode(leafElement)
                 }
                 ?: throw KtlintSuppressionNoElementFoundException(suppression)
     }
diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/KtlintSuppressionTargetNodeFinder.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/KtlintSuppressionTargetNodeFinder.kt
new file mode 100644
index 0000000..2e16a7d
--- /dev/null
+++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/KtlintSuppressionTargetNodeFinder.kt
@@ -0,0 +1,52 @@
+package com.pinterest.ktlint.rule.engine.internal
+
+import com.pinterest.ktlint.rule.engine.api.KtlintSuppression
+import com.pinterest.ktlint.rule.engine.core.api.ElementType
+import com.pinterest.ktlint.rule.engine.core.api.RuleId
+import com.pinterest.ktlint.rule.engine.core.api.parent
+import org.jetbrains.kotlin.com.intellij.lang.ASTNode
+
+private class DefaultSuppressionTargetNodeFinder : SuppressionTargetNodeFinder {
+    override fun findSuppressionTargetNode(astNode: ASTNode): ASTNode = astNode
+}
+
+private val defaultSuppressionTargetNodeFinder = DefaultSuppressionTargetNodeFinder()
+
+private class FunctionSuppressionTargetNodeFinder : SuppressionTargetNodeFinder {
+    override fun findSuppressionTargetNode(astNode: ASTNode) =
+        if (astNode.elementType == ElementType.FUN) {
+            astNode
+        } else {
+            astNode.parent { it.elementType == ElementType.FUN }
+        }
+}
+
+private val functionSuppressionTargetNodeFinder = FunctionSuppressionTargetNodeFinder()
+
+private class ClassSuppressionTargetNodeFinder : SuppressionTargetNodeFinder {
+    override fun findSuppressionTargetNode(astNode: ASTNode) =
+        if (astNode.elementType == ElementType.CLASS) {
+            astNode
+        } else {
+            astNode.parent { it.elementType == ElementType.CLASS }
+        }
+}
+
+private val classSuppressionTargetNodeFinder = ClassSuppressionTargetNodeFinder()
+
+// TODO: Decide in Ktlint 2.x whether it is worth to move the SuppressionTargetNodeFinder into the Rule class. The KtlintRuleEngine should
+//  not have any knowledge about how to suppress specific rules.
+private val ruleSuppressionTargetNodeFinder: Map<RuleId, SuppressionTargetNodeFinder> =
+    mapOf(
+        RuleId("standard:class-signature") to classSuppressionTargetNodeFinder,
+        RuleId("standard:function-signature") to functionSuppressionTargetNodeFinder,
+        RuleId("standard:parameter-list-wrapping") to functionSuppressionTargetNodeFinder,
+    )
+
+internal fun KtlintSuppression.findSuppressionTargetNodeFinder() =
+    ruleSuppressionTargetNodeFinder[ruleId]
+        ?: defaultSuppressionTargetNodeFinder
+
+internal interface SuppressionTargetNodeFinder {
+    fun findSuppressionTargetNode(astNode: ASTNode): ASTNode?
+}
diff --git a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/api/KtlintRuleEngineSuppressionKtTest.kt b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/api/KtlintRuleEngineSuppressionKtTest.kt
index c196f8f..69ead82 100644
--- a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/api/KtlintRuleEngineSuppressionKtTest.kt
+++ b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/api/KtlintRuleEngineSuppressionKtTest.kt
@@ -3,9 +3,12 @@
 import com.pinterest.ktlint.rule.engine.core.api.Rule
 import com.pinterest.ktlint.rule.engine.core.api.RuleId
 import com.pinterest.ktlint.rule.engine.core.api.RuleProvider
+import com.pinterest.ktlint.ruleset.standard.rules.CLASS_SIGNATURE_RULE_ID
 import com.pinterest.ktlint.ruleset.standard.rules.CONDITION_WRAPPING_RULE_ID
+import com.pinterest.ktlint.ruleset.standard.rules.FUNCTION_SIGNATURE_RULE_ID
 import com.pinterest.ktlint.ruleset.standard.rules.NO_CONSECUTIVE_BLANK_LINES_RULE_ID
 import com.pinterest.ktlint.ruleset.standard.rules.NO_LINE_BREAK_BEFORE_ASSIGNMENT_RULE_ID
+import com.pinterest.ktlint.ruleset.standard.rules.PARAMETER_LIST_WRAPPING_RULE_ID
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.jupiter.api.Nested
 import org.junit.jupiter.api.Test
@@ -362,6 +365,85 @@
         }
     }
 
+    @Test
+    fun `Issue 2588 - Given a class-signature violation then add the suppression at the class`() {
+        val code =
+            """
+            class FooBar : Foo, Bar {
+                // some body
+            }
+            """.trimIndent()
+        val formattedCode =
+            """
+            @Suppress("ktlint:standard:class-signature")
+            class FooBar : Foo, Bar {
+                // some body
+            }
+            """.trimIndent()
+        val actual =
+            ktLintRuleEngine
+                .insertSuppression(
+                    Code.fromSnippet(code, false),
+                    KtlintSuppressionAtOffset(1, 16, CLASS_SIGNATURE_RULE_ID),
+                )
+
+        assertThat(actual).isEqualTo(formattedCode)
+    }
+
+    @Test
+    fun `Issue 2588 - Given a function-signature violation then add the suppression at the function`() {
+        val code =
+            """
+            fun foo(
+               row1: Int, col1: Int,
+               row2: Int, col2: Int,
+            ) = "foo"
+            """.trimIndent()
+        val formattedCode =
+            """
+            @Suppress("ktlint:standard:function-signature")
+            fun foo(
+               row1: Int, col1: Int,
+               row2: Int, col2: Int,
+            ) = "foo"
+            """.trimIndent()
+        val actual =
+            ktLintRuleEngine
+                .insertSuppression(
+                    Code.fromSnippet(code, false),
+                    KtlintSuppressionAtOffset(2, 15, FUNCTION_SIGNATURE_RULE_ID),
+                )
+
+        assertThat(actual).isEqualTo(formattedCode)
+    }
+
+    @Test
+    fun `Issue 2588 - Given a parameter-list-wrapping violation then add the suppression at the function`() {
+        val code =
+            """
+            fun foo(
+               row1: Int, col1: Int,
+               row2: Int, col2: Int,
+            ) = "foo"
+            """.trimIndent()
+        val formattedCode =
+            """
+            @Suppress("ktlint:standard:parameter-list-wrapping")
+            fun foo(
+               row1: Int, col1: Int,
+               row2: Int, col2: Int,
+            ) = "foo"
+            """.trimIndent()
+        val actual =
+            ktLintRuleEngine
+                .insertSuppression(
+                    Code.fromSnippet(code, false),
+                    KtlintSuppressionAtOffset(2, 15, PARAMETER_LIST_WRAPPING_RULE_ID),
+                )
+
+        assertThat(actual).isEqualTo(formattedCode)
+    }
+
     private companion object {
         val SOME_RULE_ID = RuleId("standard:some-rule-id")
     }
diff --git a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/KtlintSuppressionKtTest.kt b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/KtlintSuppressionTest.kt
similarity index 99%
rename from ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/KtlintSuppressionKtTest.kt
rename to ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/KtlintSuppressionTest.kt
index 85630d7..9a75334 100644
--- a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/KtlintSuppressionKtTest.kt
+++ b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/KtlintSuppressionTest.kt
@@ -9,7 +9,7 @@
 import org.junit.jupiter.api.Nested
 import org.junit.jupiter.api.Test
 
-class KtlintSuppressionKtTest {
+class KtlintSuppressionTest {
     @Nested
     inner class `Given a file suppression to be inserted` {
         @Test