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

Adding new logic for suppressing inspection #1328

Merged
merged 10 commits into from
May 30, 2022
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` annotation on individual code blocks.
orchestr7 marked this conversation as resolved.
Show resolved Hide resolved

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 ]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file will be bundled into the jar, is it worth adding this here and not into examples?

Copy link
Member Author

@orchestr7 orchestr7 May 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that users copy-paste this one 🤦
But yes, let's remove

# 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