Repository: analysis-dev/diktat (kotlin)

Issue:
Implement ignoreAnnotated suppressor like detekt
There are a lot of false positive findings regarding **Jetpack Compose**. _Detekt_ handles this gently with a `ignoreAnnotated` suppressor. It's a List of Strings. Code that is annotated with an annotation of this list is ignored.

This would come really handy for following rules:
TOO_MANY_PARAMETERS
LAMBDA_IS_NOT_LAST_PARAMETER
TOO_LONG_FUNCTION
FUNCTION_NAME_INCORRECT_CASE
VARIABLE_NAME_INCORRECT_FORMAT
MISSING_KDOC_ON_FUNCTION

Pull request:
### What's done:
- `ignoreAnnotated` logic that helps to suppress inspections that could have triggered on the block of code annotated with some specific annotations (useful for Jetpack)
- adding suppressing for ALL rules for the code block

Patch:
diff --git a/README.md b/README.md
index e2c35703fe..c5dc41b7a4 100644
--- a/README.md
+++ b/README.md
@@ -374,7 +374,8 @@ Also see [the list of all rules supported by diKTat](info/available-rules.md).
 
 <details>
 <summary>Suppress warnings on individual code blocks</summary>
-In addition to enabling/disabling warning globally via config file (`enable = false`), you can suppress warnings by adding `@Suppress` annotation on individual code blocks
+In addition to enabling/disabling warning globally via config file (`enable = false`), you can suppress warnings
+by adding `@Suppress` annotation on individual code blocks or `@file:Suppress()` annotation on a file-level.
 
 For example:
 
@@ -389,7 +390,35 @@ class SomeClass {
 </details>
 
 <details>
-<summary>Suppress groups of inspections</summary>
+<summary>Disable all inspections on selected code blocks</summary>
+Also you can suppress **all** warnings by adding `@Suppress("diktat")` annotation on individual code blocks.
+
+For example:
+
+``` kotlin
+@Suppress("diktat")
+class SomeClass {
+    fun methODTREE(): String {
+
+    }
+}
+```
+</details>
+
+<details>
+<summary>ignoreAnnotated: disable inspections on blocks with predefined annotation</summary>
+In the `diktat-analysis.yml` file for each inspection it is possible to define a list of annotations that will cause
+disabling of the inspection on that particular code block:
+
+```yaml
+- name: HEADER_NOT_BEFORE_PACKAGE
+  enabled: true
+  ignoreAnnotated: [MyAnnotation, Compose, Controller]
+```
+</details>
+
+<details>
+<summary>Suppress groups of inspections by chapters</summary>
 It is easy to suppress even groups of inspections in diKTat.
 
 These groups are linked to chapters of [Codestyle](info/guide/diktat-coding-convention.md). 
diff --git a/diktat-common/src/main/kotlin/org/cqfn/diktat/common/config/rules/RulesConfigReader.kt b/diktat-common/src/main/kotlin/org/cqfn/diktat/common/config/rules/RulesConfigReader.kt
index ee3c5a054b..0cd23a9369 100644
--- a/diktat-common/src/main/kotlin/org/cqfn/diktat/common/config/rules/RulesConfigReader.kt
+++ b/diktat-common/src/main/kotlin/org/cqfn/diktat/common/config/rules/RulesConfigReader.kt
@@ -26,6 +26,12 @@ import kotlinx.serialization.decodeFromString
  */
 const val DIKTAT_COMMON = "DIKTAT_COMMON"
 
+/**
+ * Common application name, that is used in plugins and can be used to Suppress all diktat inspections on the
+ * particular code block with @Suppress("diktat")
+ */
+const val DIKTAT = "diktat"
+
 /**
  * This interface represents individual inspection in rule set.
  */
@@ -41,12 +47,14 @@ interface Rule {
  * @property name name of the rule
  * @property enabled
  * @property configuration a map of strings with configuration options
+ * @property ignoreAnnotated if a code block is marked with this annotations - it will not be checked by this rule
  */
 @Serializable
 data class RulesConfig(
     val name: String,
     val enabled: Boolean = true,
-    val configuration: Map<String, String> = emptyMap()
+    val configuration: Map<String, String> = emptyMap(),
+    val ignoreAnnotated: Set<String> = emptySet(),
 )
 
 /**
@@ -188,6 +196,20 @@ fun List<RulesConfig>.isRuleEnabled(rule: Rule): Boolean {
     return ruleMatched?.enabled ?: true
 }
 
+/**
+ * @param rule diktat inspection
+ * @param annotations set of annotations that are annotating a block of code
+ * @return true if the code block is marked with annotation that is in `ignored list` in the rule
+ */
+fun List<RulesConfig>.isAnnotatedWithIgnoredAnnotation(rule: Rule, annotations: Set<String>): Boolean =
+        getRuleConfig(rule)
+            ?.ignoreAnnotated
+            ?.map { it.trim() }
+            ?.map { it.trim('"') }
+            ?.intersect(annotations)
+            ?.isNotEmpty()
+            ?: false
+
 /**
  * Parse string into KotlinVersion
  *
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/constants/Warnings.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/constants/Warnings.kt
index 2425f7be42..ee4bb67c64 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/constants/Warnings.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/constants/Warnings.kt
@@ -3,7 +3,7 @@ package org.cqfn.diktat.ruleset.constants
 import org.cqfn.diktat.common.config.rules.Rule
 import org.cqfn.diktat.common.config.rules.RulesConfig
 import org.cqfn.diktat.common.config.rules.isRuleEnabled
-import org.cqfn.diktat.ruleset.utils.hasSuppress
+import org.cqfn.diktat.ruleset.utils.isSuppressed
 import org.jetbrains.kotlin.com.intellij.lang.ASTNode
 
 typealias EmitType = ((offset: Int,
@@ -232,7 +232,7 @@ enum class Warnings(
              freeText: String,
              offset: Int,
              node: ASTNode) {
-        if (isRuleFromActiveChapter(configs) && configs.isRuleEnabled(this) && !node.hasSuppress(name)) {
+        if (isRuleFromActiveChapter(configs) && configs.isRuleEnabled(this) && !node.isSuppressed(name, this, configs)) {
             val trimmedFreeText = freeText
                 .lines()
                 .run { if (size > 1) "${first()}..." else first() }
@@ -248,7 +248,7 @@ enum class Warnings(
         isFix: Boolean,
         node: ASTNode,
         autoFix: () -> Unit) {
-        if (isRuleFromActiveChapter(configs) && configs.isRuleEnabled(this) && isFix && !node.hasSuppress(name)) {
+        if (isRuleFromActiveChapter(configs) && configs.isRuleEnabled(this) && isFix && !node.isSuppressed(name, this, configs)) {
             autoFix()
         }
     }
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtils.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtils.kt
index 8a5ef64524..2fb9170d58 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtils.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtils.kt
@@ -11,6 +11,10 @@
 
 package org.cqfn.diktat.ruleset.utils
 
+import org.cqfn.diktat.common.config.rules.DIKTAT
+import org.cqfn.diktat.common.config.rules.Rule
+import org.cqfn.diktat.common.config.rules.RulesConfig
+import org.cqfn.diktat.common.config.rules.isAnnotatedWithIgnoredAnnotation
 import org.cqfn.diktat.ruleset.rules.chapter1.PackageNaming
 
 import com.pinterest.ktlint.core.KtLint
@@ -508,21 +512,12 @@ fun ASTNode?.isAccessibleOutside(): Boolean =
  * @param warningName a name of the warning which is checked
  * @return boolean result
  */
-fun ASTNode.hasSuppress(warningName: String) = parent({ node ->
-    val annotationNode = if (node.elementType != FILE) {
-        node.findChildByType(MODIFIER_LIST) ?: node.findChildByType(ANNOTATED_EXPRESSION)
-    } else {
-        node.findChildByType(FILE_ANNOTATION_LIST)
-    }
-    annotationNode?.findAllDescendantsWithSpecificType(ANNOTATION_ENTRY)
-        ?.map { it.psi as KtAnnotationEntry }
-        ?.any {
-            it.shortName.toString() == Suppress::class.simpleName &&
-                    it.valueArgumentList?.arguments
-                        ?.any { annotationName -> annotationName.text.trim('"', ' ') == warningName }
-                        ?: false
-        } ?: false
-}, strict = false) != null
+fun ASTNode.isSuppressed(
+    warningName: String,
+    rule: Rule,
+    configs: List<RulesConfig>
+) =
+        this.parent(hasAnySuppressorForInspection(warningName, rule, configs), strict = false) != null
 
 /**
  * Checks node has `override` modifier
@@ -826,6 +821,15 @@ fun ASTNode.takeByChainOfTypes(vararg types: IElementType): ASTNode? {
     return node
 }
 
+private fun Collection<KtAnnotationEntry>.containSuppressWithName(name: String) =
+        this.any {
+            it.shortName.toString() == (Suppress::class.simpleName) &&
+                    (it.valueArgumentList
+                        ?.arguments
+                        ?.any { annotation -> annotation.text.trim('"') == name }
+                        ?: false)
+        }
+
 private fun ASTNode.findOffsetByLine(line: Int, positionByOffset: (Int) -> Pair<Int, Int>): Int {
     val currentLine = this.getLineNumber()
     val currentOffset = this.startOffset
@@ -967,6 +971,30 @@ fun doesLambdaContainIt(lambdaNode: ASTNode): Boolean {
     return hasNoParameters(lambdaNode) && hasIt
 }
 
+private fun hasAnySuppressorForInspection(
+    warningName: String,
+    rule: Rule,
+    configs: List<RulesConfig>
+) = { node: ASTNode ->
+    val annotationsForNode = if (node.elementType != FILE) {
+        node.findChildByType(MODIFIER_LIST) ?: node.findChildByType(ANNOTATED_EXPRESSION)
+    } else {
+        node.findChildByType(FILE_ANNOTATION_LIST)
+    }
+        ?.findAllDescendantsWithSpecificType(ANNOTATION_ENTRY)
+        ?.map { it.psi as KtAnnotationEntry }
+        ?: emptySet()
+
+    val foundSuppress = annotationsForNode.containSuppressWithName(warningName)
+
+    val foundIgnoredAnnotation =
+            configs.isAnnotatedWithIgnoredAnnotation(rule, annotationsForNode.map { it.shortName.toString() }.toSet())
+
+    val isCompletelyIgnoredBlock = annotationsForNode.containSuppressWithName(DIKTAT)
+
+    foundSuppress || foundIgnoredAnnotation || isCompletelyIgnoredBlock
+}
+
 private fun hasNoParameters(lambdaNode: ASTNode): Boolean {
     require(lambdaNode.elementType == LAMBDA_EXPRESSION) { "Method can be called only for lambda" }
     return null == lambdaNode
diff --git a/diktat-rules/src/main/resources/diktat-analysis-huawei.yml b/diktat-rules/src/main/resources/diktat-analysis-huawei.yml
index 9e6da6fe0a..1d6301997f 100644
--- a/diktat-rules/src/main/resources/diktat-analysis-huawei.yml
+++ b/diktat-rules/src/main/resources/diktat-analysis-huawei.yml
@@ -12,6 +12,8 @@
 # Checks that the Class/Enum/Interface name does not match Pascal case
 - name: CLASS_NAME_INCORRECT
   enabled: true
+  # all code blocks with MyAnnotation will be ignored and not checked
+  ignoreAnnotated: [ MyAnnotation ]
 # Checks that CONSTANT (treated as const val from companion object or class level) is in non UPPER_SNAKE_CASE
 - name: CONSTANT_UPPERCASE
   enabled: true
diff --git a/diktat-rules/src/main/resources/diktat-analysis.yml b/diktat-rules/src/main/resources/diktat-analysis.yml
index 3164079e88..4fead92a14 100644
--- a/diktat-rules/src/main/resources/diktat-analysis.yml
+++ b/diktat-rules/src/main/resources/diktat-analysis.yml
@@ -12,6 +12,8 @@
 # Checks that the Class/Enum/Interface name does not match Pascal case
 - name: CLASS_NAME_INCORRECT
   enabled: true
+  # all code blocks with MyAnnotation will be ignored and not checked
+  ignoreAnnotated: [ MyAnnotation ]
 # Checks that CONSTANT (treated as const val from companion object or class level) is in non UPPER_SNAKE_CASE
 - name: CONSTANT_UPPERCASE
   enabled: true
diff --git a/examples/gradle-groovy-dsl/diktat-analysis.yml b/examples/gradle-groovy-dsl/diktat-analysis.yml
index c5945beb5b..464d7674d0 100644
--- a/examples/gradle-groovy-dsl/diktat-analysis.yml
+++ b/examples/gradle-groovy-dsl/diktat-analysis.yml
@@ -12,6 +12,8 @@
 # Checks that the Class/Enum/Interface name does not match Pascal case
 - name: CLASS_NAME_INCORRECT
   enabled: true
+  # all code blocks with MyAnnotation will be ignored and not checked
+  ignoreAnnotated: [ MyAnnotation ]
 # Checks that CONSTANT (treated as const val from companion object or class level) is in non UPPER_SNAKE_CASE
 - name: CONSTANT_UPPERCASE
   enabled: true
diff --git a/examples/gradle-kotlin-dsl-multiproject/diktat-analysis.yml b/examples/gradle-kotlin-dsl-multiproject/diktat-analysis.yml
index 354c4d0fc4..a2f0b92333 100644
--- a/examples/gradle-kotlin-dsl-multiproject/diktat-analysis.yml
+++ b/examples/gradle-kotlin-dsl-multiproject/diktat-analysis.yml
@@ -5,6 +5,8 @@
     testDirs: test
 - name: CLASS_NAME_INCORRECT
   enabled: true
+  # all code blocks with MyAnnotation will be ignored and not checked
+  ignoreAnnotated: [ MyAnnotation ]
 - name: CONSTANT_UPPERCASE
   enabled: true
 - name: ENUM_VALUE
diff --git a/examples/gradle-kotlin-dsl/diktat-analysis.yml b/examples/gradle-kotlin-dsl/diktat-analysis.yml
index c5945beb5b..464d7674d0 100644
--- a/examples/gradle-kotlin-dsl/diktat-analysis.yml
+++ b/examples/gradle-kotlin-dsl/diktat-analysis.yml
@@ -12,6 +12,8 @@
 # Checks that the Class/Enum/Interface name does not match Pascal case
 - name: CLASS_NAME_INCORRECT
   enabled: true
+  # all code blocks with MyAnnotation will be ignored and not checked
+  ignoreAnnotated: [ MyAnnotation ]
 # Checks that CONSTANT (treated as const val from companion object or class level) is in non UPPER_SNAKE_CASE
 - name: CONSTANT_UPPERCASE
   enabled: true
diff --git a/examples/maven/diktat-analysis.yml b/examples/maven/diktat-analysis.yml
index c5945beb5b..464d7674d0 100644
--- a/examples/maven/diktat-analysis.yml
+++ b/examples/maven/diktat-analysis.yml
@@ -12,6 +12,8 @@
 # Checks that the Class/Enum/Interface name does not match Pascal case
 - name: CLASS_NAME_INCORRECT
   enabled: true
+  # all code blocks with MyAnnotation will be ignored and not checked
+  ignoreAnnotated: [ MyAnnotation ]
 # Checks that CONSTANT (treated as const val from companion object or class level) is in non UPPER_SNAKE_CASE
 - name: CONSTANT_UPPERCASE
   enabled: true


Test patch:
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/SuppressingTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/SuppressingTest.kt
new file mode 100644
index 0000000000..c1d5bc4ab1
--- /dev/null
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/SuppressingTest.kt
@@ -0,0 +1,87 @@
+package org.cqfn.diktat.util
+
+import org.cqfn.diktat.common.config.rules.RulesConfig
+import org.cqfn.diktat.ruleset.constants.Warnings.BACKTICKS_PROHIBITED
+import org.cqfn.diktat.ruleset.constants.Warnings.CLASS_NAME_INCORRECT
+import org.cqfn.diktat.ruleset.constants.Warnings.CONFUSING_IDENTIFIER_NAMING
+import org.cqfn.diktat.ruleset.constants.Warnings.CONSTANT_UPPERCASE
+import org.cqfn.diktat.ruleset.constants.Warnings.ENUM_VALUE
+import org.cqfn.diktat.ruleset.constants.Warnings.EXCEPTION_SUFFIX
+import org.cqfn.diktat.ruleset.constants.Warnings.FUNCTION_BOOLEAN_PREFIX
+import org.cqfn.diktat.ruleset.constants.Warnings.GENERIC_NAME
+import org.cqfn.diktat.ruleset.constants.Warnings.IDENTIFIER_LENGTH
+import org.cqfn.diktat.ruleset.constants.Warnings.OBJECT_NAME_INCORRECT
+import org.cqfn.diktat.ruleset.constants.Warnings.VARIABLE_HAS_PREFIX
+import org.cqfn.diktat.ruleset.constants.Warnings.VARIABLE_NAME_INCORRECT
+import org.cqfn.diktat.ruleset.constants.Warnings.VARIABLE_NAME_INCORRECT_FORMAT
+import org.cqfn.diktat.ruleset.rules.DIKTAT_RULE_SET_ID
+import org.cqfn.diktat.ruleset.rules.chapter1.IdentifierNaming
+
+import com.pinterest.ktlint.core.LintError
+import generated.WarningNames
+import org.junit.jupiter.api.Tag
+import org.junit.jupiter.api.Tags
+import org.junit.jupiter.api.Test
+
+class SuppressingTest : LintTestBase(::IdentifierNaming) {
+    private val ruleId: String = "$DIKTAT_RULE_SET_ID:${IdentifierNaming.NAME_ID}"
+    private val rulesConfigBooleanFunctions: List<RulesConfig> = listOf(
+        RulesConfig(IDENTIFIER_LENGTH.name, true, emptyMap(), setOf("MySuperSuppress"))
+    )
+
+    @Test
+    fun `checking that suppression with ignoredAnnotation works`() {
+        val code =
+                """
+                    @MySuperSuppress()
+                    fun foo() {
+                        val a = 1
+                    }
+                """.trimIndent()
+        lintMethod(code, rulesConfigList = rulesConfigBooleanFunctions)
+    }
+
+    @Test
+    fun `checking that suppression with ignore everything works`() {
+        val code =
+                """
+                    @Suppress("diktat")
+                    fun foo() {
+                        val a = 1
+                    }
+                """.trimIndent()
+        lintMethod(code)
+    }
+
+    @Test
+    fun `checking that suppression with a targeted inspection name works`() {
+        val code =
+                """
+                    @Suppress("IDENTIFIER_LENGTH")
+                    fun foo() {
+                        val a = 1
+                    }
+                """.trimIndent()
+        lintMethod(code)
+    }
+
+    @Test
+    fun `negative scenario for other annotation`() {
+        val code =
+                """
+                    @MySuperSuppress111()
+                    fun foo() {
+                        val a = 1
+                    }
+                """.trimIndent()
+        lintMethod(
+            code,
+            LintError(3,
+                9,
+                ruleId,
+                "[IDENTIFIER_LENGTH] identifier's length is incorrect, it" +
+                        " should be in range of [2, 64] symbols: a", false),
+            rulesConfigList = rulesConfigBooleanFunctions,
+        )
+    }
+}