Skip to content

Commit

Permalink
Adding new logic for suppressing inspection (#1328)
Browse files Browse the repository at this point in the history
### What's done:
- ignoreAnnotated logic to disable annotated code blocks
- adding mechanism for suppression of ALL rules for the selected code block with Suppress("diktat")
  • Loading branch information
orchestr7 authored May 30, 2022
1 parent 0706885 commit 8aae73e
Show file tree
Hide file tree
Showing 11 changed files with 199 additions and 21 deletions.
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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(),
)

/**
Expand Down Expand Up @@ -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
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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() }
Expand All @@ -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()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions diktat-rules/src/main/resources/diktat-analysis-huawei.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions diktat-rules/src/main/resources/diktat-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
)
}
}
2 changes: 2 additions & 0 deletions examples/gradle-groovy-dsl/diktat-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions examples/gradle-kotlin-dsl-multiproject/diktat-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions examples/gradle-kotlin-dsl/diktat-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions examples/maven/diktat-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 8aae73e

Please sign in to comment.