diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f32c709d1..855e2486fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ An AssertJ style API for testing KtLint rules ([#1444](https://github.com/pinter - Add experimental rule for unexpected spaces in a nullable type (`nullable-type-spacing`) ([#1341](https://github.com/pinterest/ktlint/issues/1341)) - Do not add a space after the typealias name (`type-parameter-list-spacing`) ([#1435](https://github.com/pinterest/ktlint/issues/1435)) - Add experimental rule for consistent spacing before the start of the function body (`function-start-of-body-spacing`) ([#1341](https://github.com/pinterest/ktlint/issues/1341)) +- Suppress ktlint rules using `@Suppress` ([more information](https://github.com/pinterest/ktlint#disabling-for-a-statement-using-suppress)) ([#765](https://github.com/pinterest/ktlint/issues/765)) - Add experimental rule for rewriting the function signature (`function-signature`) ([#1341](https://github.com/pinterest/ktlint/issues/1341)) ### Fixed diff --git a/README.md b/README.md index 3655c43e62..71560aa73c 100644 --- a/README.md +++ b/README.md @@ -616,29 +616,97 @@ Absolutely, "no configuration" doesn't mean "no extensibility". You can add your See [Creating A Ruleset](#creating-a-ruleset). -### How do I suppress an error for a line/block/file? +### How do I suppress an errors for a line/block/file? > This is meant primarily as an escape latch for the rare cases when **ktlint** is not able to produce the correct result (please report any such instances using [GitHub Issues](https://github.com/pinterest/ktlint/issues)). -To disable a specific rule you'll need to turn on the verbose mode (`ktlint --verbose ...`). At the end of each line -you'll see an error code. Use it as an argument for `ktlint-disable` directive (shown below). +To disable a specific rule you'll need the rule identifier which is displayed at the end of the lint error. Note that when the rule id is prefixed with a rule set id like `experimental`, you will need to use that fully qualified rule id. + +An error can be suppressed using: + +* EOL comments +* Block comments +* @Suppress annotations + +From a consistency perspective seen, it might be best to **not** mix the (EOL/Block) comment style with the annotation style in the same project. + +Important notice: some rules like the `indent` rule do not yet support disabling of the rule per line of block. + +#### Disabling for one specific line using EOL comment + +An error for a specific rule on a specific line can be disabled with an EOL comment on that line: ```kotlin import package.* // ktlint-disable no-wildcard-imports +``` + +In case lint errors for different rules on the same line need to be ignored, then specify multiple rule ids (separated by a space): + +```kotlin +import package.* // ktlint-disable no-wildcard-imports other-rule-id +``` + +In case all lint errors on a line need to be ignored, then do not specify the rule id at all: + +```kotlin +import package.* // ktlint-disable +``` + +#### Disabling for a block of lines using Block comments + +An error for a specific rule in a block of lines can be disabled with an block comment like: +```kotlin /* ktlint-disable no-wildcard-imports */ import package.a.* import package.b.* /* ktlint-enable no-wildcard-imports */ ``` -To disable all checks: +In case lint errors for different rules in the same block of lines need to be ignored, then specify multiple rule ids (separated by a space): ```kotlin -import package.* // ktlint-disable +/* ktlint-disable no-wildcard-imports other-rule-id */ +import package.a.* +import package.b.* +/* ktlint-enable no-wildcard-imports,other-rule-id */ +``` + +Note that the `ktlint-enable` directive needs to specify the exact same rule-id's and in the same order as the `ktlint-disable` directive. + +In case all lint errors in a block of lines needs to be ignored, then do not specify the rule id at all: + +```kotlin +/* ktlint-disable */ +import package.a.* +import package.b.* +/* ktlint-enable */ +``` + +#### Disabling for a statement using @Suppress + +> As of ktlint version 0.46, it is possible to specify any ktlint rule id via the `@Suppress` annotation in order to suppress errors found by that rule. Note that some rules like `indent` still do not support disabling for parts of a file. + +An error for a specific rule on a specific line can be disabled with a `@Suppress` annotation: + +```kotlin +@Suppress("ktlint:max-line-length","ktlint:experimental:trailing-comma") +val foo = listOf( + "some really looooooooooooooooong string exceeding the max line length", + ) ``` +Note that when using `@Suppress` each qualified rule id needs to be prefixed with `ktlint:`. + +To suppress the violations of all ktlint rules, use: +```kotlin +@Suppress("ktlint") +val foo = "some really looooooooooooooooong string exceeding the max line length" +``` + +Like with other `@Suppress` annotations, it can be placed on targets supported by the annotation. + ### How do I globally disable a rule? See the [EditorConfig section](https://github.com/pinterest/ktlint#editorconfig) for details on how to use the `disabled_rules` property. diff --git a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/SupressedRegionLocator.kt b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/SuppressedRegionLocator.kt similarity index 75% rename from ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/SupressedRegionLocator.kt rename to ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/SuppressedRegionLocator.kt index f5e464c430..0e32a8c730 100644 --- a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/SupressedRegionLocator.kt +++ b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/SuppressedRegionLocator.kt @@ -7,6 +7,7 @@ import org.jetbrains.kotlin.com.intellij.psi.PsiComment import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace import org.jetbrains.kotlin.psi.KtAnnotated import org.jetbrains.kotlin.psi.KtAnnotationEntry +import org.jetbrains.kotlin.psi.ValueArgument import org.jetbrains.kotlin.psi.psiUtil.endOffset import org.jetbrains.kotlin.psi.psiUtil.startOffset @@ -24,6 +25,8 @@ private val suppressAnnotationRuleMap = mapOf( "RemoveCurlyBracesFromTemplate" to "string-template" ) private val suppressAnnotations = setOf("Suppress", "SuppressWarnings") +private const val suppresAllKtlintRules = "ktlint-all" + private val commentRegex = Regex("\\s") /** @@ -150,24 +153,48 @@ private fun createSuppressionHintFromAnnotations( psi: KtAnnotated, targetAnnotations: Collection, annotationValueToRuleMapping: Map -): SuppressionHint? = psi.annotationEntries - .filter { - it.calleeExpression - ?.constructorReferenceExpression - ?.getReferencedName() in targetAnnotations - } - .flatMap(KtAnnotationEntry::getValueArguments) - .mapNotNull { - it.getArgumentExpression()?.text?.removeSurrounding("\"") - } - .mapNotNull(annotationValueToRuleMapping::get) - .let { suppressedRules -> - if (suppressedRules.isNotEmpty()) { - SuppressionHint( - IntRange(psi.startOffset, psi.endOffset), - suppressedRules.toSet() - ) - } else { - null +): SuppressionHint? = + psi + .annotationEntries + .filter { + it.calleeExpression + ?.constructorReferenceExpression + ?.getReferencedName() in targetAnnotations + }.flatMap(KtAnnotationEntry::getValueArguments) + .mapNotNull { it.toRuleId(annotationValueToRuleMapping) } + .let { suppressedRules -> + when { + suppressedRules.isEmpty() -> null + suppressedRules.contains(suppresAllKtlintRules) -> + SuppressionHint( + IntRange(psi.startOffset, psi.endOffset), + emptySet() + ) + else -> + SuppressionHint( + IntRange(psi.startOffset, psi.endOffset), + suppressedRules.toSet() + ) + } + } + +private fun ValueArgument.toRuleId(annotationValueToRuleMapping: Map): String? = + getArgumentExpression() + ?.text + ?.removeSurrounding("\"") + ?.let { + when { + it == "ktlint" -> { + // Disable all rules + suppresAllKtlintRules + } + it.startsWith("ktlint:") -> { + // Disable specific rule + it.removePrefix("ktlint:") + } + else -> { + // Disable specific rule if it the annotion value is mapped to a specific rule + annotationValueToRuleMapping.get(it) + } + } } - } diff --git a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/ErrorSuppressionTest.kt b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/ErrorSuppressionTest.kt deleted file mode 100644 index 0fb4c63ab1..0000000000 --- a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/ErrorSuppressionTest.kt +++ /dev/null @@ -1,91 +0,0 @@ -package com.pinterest.ktlint.core - -import com.pinterest.ktlint.core.ast.ElementType -import com.pinterest.ktlint.core.ast.isPartOf -import java.util.ArrayList -import org.assertj.core.api.Assertions.assertThat -import org.jetbrains.kotlin.com.intellij.lang.ASTNode -import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement -import org.junit.jupiter.api.Test - -class ErrorSuppressionTest { - - @Test - fun testErrorSuppression() { - class NoWildcardImportsRule : Rule("no-wildcard-imports") { - override fun visit( - node: ASTNode, - autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, corrected: Boolean) -> Unit - ) { - if (node is LeafPsiElement && node.textMatches("*") && node.isPartOf(ElementType.IMPORT_DIRECTIVE)) { - emit(node.startOffset, "Wildcard import", false) - } - } - } - fun lint(text: String) = - ArrayList().apply { - KtLint.lint( - KtLint.Params( - text = text, - ruleSets = listOf(RuleSet("standard", NoWildcardImportsRule())), - cb = { e, _ -> add(e) } - ) - ) - } - assertThat( - lint( - """ - import a.* // ktlint-disable - import a.* // will trigger an error - """.trimIndent() - ) - ).isEqualTo( - listOf( - LintError(2, 10, "no-wildcard-imports", "Wildcard import") - ) - ) - assertThat( - lint( - """ - import a.* // ktlint-disable no-wildcard-imports - import a.* // will trigger an error - """.trimIndent() - ) - ).isEqualTo( - listOf( - LintError(2, 10, "no-wildcard-imports", "Wildcard import") - ) - ) - assertThat( - lint( - """ - /* ktlint-disable */ - import a.* - import a.* - /* ktlint-enable */ - import a.* // will trigger an error - """.trimIndent() - ) - ).isEqualTo( - listOf( - LintError(5, 10, "no-wildcard-imports", "Wildcard import") - ) - ) - assertThat( - lint( - """ - /* ktlint-disable no-wildcard-imports */ - import a.* - import a.* - /* ktlint-enable no-wildcard-imports */ - import a.* // will trigger an error - """.trimIndent() - ) - ).isEqualTo( - listOf( - LintError(5, 10, "no-wildcard-imports", "Wildcard import") - ) - ) - } -} diff --git a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/SuppressedRegionLocatorKtTest.kt b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/SuppressedRegionLocatorKtTest.kt new file mode 100644 index 0000000000..732a216e33 --- /dev/null +++ b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/SuppressedRegionLocatorKtTest.kt @@ -0,0 +1,207 @@ +package com.pinterest.ktlint.core.internal + +import com.pinterest.ktlint.core.KtLint +import com.pinterest.ktlint.core.LintError +import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.RuleSet +import com.pinterest.ktlint.core.api.FeatureInAlphaState +import com.pinterest.ktlint.core.ast.ElementType +import java.util.ArrayList +import org.assertj.core.api.Assertions.assertThat +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.junit.jupiter.api.Test + +@OptIn(FeatureInAlphaState::class) +class SuppressedRegionLocatorKtTest { + @Test + fun `Given that NoFooIdentifierRule finds a violation (eg verifying that the test rules actually works)`() { + val code = + """ + val foo = "foo" + val fooWithSuffix = "fooWithSuffix" + """.trimIndent() + assertThat(lint(code)).containsExactly( + LintError(1, 5, "no-foo-identifier-standard", "Line should not contain a foo identifier"), + LintError(1, 5, "custom:no-foo-identifier", "Line should not contain a foo identifier"), + LintError(2, 5, "no-foo-identifier-standard", "Line should not contain a foo identifier"), + LintError(2, 5, "custom:no-foo-identifier", "Line should not contain a foo identifier") + ) + } + + @Test + fun `Given that a NoFooIdentifierRule violation is suppressed with an EOL-comment to disable all rules then do not find a violation`() { + val code = + """ + val foo = "foo" // ktlint-disable + """.trimIndent() + assertThat(lint(code)).isEmpty() + } + + @Test + fun `Given that a NoFooIdentifierRule violation is suppressed with an EOL-comment for the specific rule then do not find a violation`() { + val code = + """ + val foo = "foo" // ktlint-disable no-foo-identifier-standard custom:no-foo-identifier + """.trimIndent() + assertThat(lint(code)).isEmpty() + } + + @Test + fun `Given that a NoFooIdentifierRule violation is suppressed with a block comment for all rules then do not find a violation in that block`() { + val code = + """ + /* ktlint-disable */ + val fooNotReported = "foo" + /* ktlint-enable */ + val fooReported = "foo" + """.trimIndent() + assertThat(lint(code)).containsExactly( + LintError(4, 5, "no-foo-identifier-standard", "Line should not contain a foo identifier"), + LintError(4, 5, "custom:no-foo-identifier", "Line should not contain a foo identifier") + ) + } + + @Test + fun `Given that a NoFooIdentifierRule violation is suppressed with a block comment for a specific rule then do not find a violation for that rule in that block`() { + val code = + """ + /* ktlint-disable no-foo-identifier-standard custom:no-foo-identifier */ + val fooNotReported = "foo" + /* ktlint-enable no-foo-identifier-standard custom:no-foo-identifier */ + val fooReported = "foo" + """.trimIndent() + assertThat(lint(code)).containsExactly( + LintError(4, 5, "no-foo-identifier-standard", "Line should not contain a foo identifier"), + LintError(4, 5, "custom:no-foo-identifier", "Line should not contain a foo identifier") + ) + } + + @Test + fun `Given that a NoFooIdentifierRule violation is suppressed with @Suppress at statement level for all rules then do not find a violation`() { + val code = + """ + @Suppress("ktlint") + val fooNotReported = "foo" + + val fooReported = "foo" + """.trimIndent() + assertThat(lint(code)).containsExactly( + LintError(4, 5, "no-foo-identifier-standard", "Line should not contain a foo identifier"), + LintError(4, 5, "custom:no-foo-identifier", "Line should not contain a foo identifier") + ) + } + + @Test + fun `Given that a NoFooIdentifierRule violation is suppressed with @Suppress at statement level for a specific rule then do not find a violation for that rule`() { + val code = + """ + @Suppress("ktlint:no-foo-identifier-standard", "ktlint:custom:no-foo-identifier") + val fooNotReported = "foo" + + val fooReported = "foo" + """.trimIndent() + assertThat(lint(code)).containsExactly( + LintError(4, 5, "no-foo-identifier-standard", "Line should not contain a foo identifier"), + LintError(4, 5, "custom:no-foo-identifier", "Line should not contain a foo identifier") + ) + } + + @Test + fun `Given that a NoFooIdentifierRule violation is suppressed with @Suppress at function level then do not find a violation for that rule in that function`() { + val code = + """ + @Suppress("ktlint:no-foo-identifier-standard", "ktlint:custom:no-foo-identifier") + fun foo() { + val fooNotReported = "foo" + } + + val fooReported = "foo" + """.trimIndent() + assertThat(lint(code)).containsExactly( + LintError(6, 5, "no-foo-identifier-standard", "Line should not contain a foo identifier"), + LintError(6, 5, "custom:no-foo-identifier", "Line should not contain a foo identifier") + ) + } + + @Test + fun `Given that a NoFooIdentifierRule violation is suppressed with @Suppress at function level for all rules then do not find violations in that function`() { + val code = + """ + @Suppress("ktlint") + fun foo() { + val fooNotReported = "foo" + } + + val fooReported = "foo" + """.trimIndent() + assertThat(lint(code)).containsExactly( + LintError(6, 5, "no-foo-identifier-standard", "Line should not contain a foo identifier"), + LintError(6, 5, "custom:no-foo-identifier", "Line should not contain a foo identifier") + ) + } + + @Test + fun `Given that a NoFooIdentifierRule violation is suppressed with @Suppress at class level then do not find a violation for that rule in that class`() { + val code = + """ + @Suppress("ktlint:no-foo-identifier-standard", "ktlint:custom:no-foo-identifier") + class Foo { + fun foo() { + val fooNotReported = "foo" + } + + val foo = "foo" + } + """.trimIndent() + assertThat(lint(code)).isEmpty() + } + + @Test + private fun `Given that a NoFooIdentifierRule violation is suppressed with @Suppress for all rules at class level then do not find a violation for that rule in that class`() { + val code = + """ + @Suppress("ktlint") + class Foo { + fun foo() { + val fooNotReported = "foo" + } + + val foo = "foo" + } + """.trimIndent() + assertThat(lint(code)).isEmpty() + } + + private class NoFooIdentifierRule(id: String) : Rule(id) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node.elementType == ElementType.IDENTIFIER && node.text.startsWith("foo")) { + emit(node.startOffset, "Line should not contain a foo identifier", false) + } + } + } + + private fun lint(code: String) = + ArrayList().apply { + KtLint.lint( + KtLint.ExperimentalParams( + text = code, + ruleSets = listOf( + // The same rule is supplied once a standard rule and once as non-standard rule. Note that the + // ruleIds are different. + RuleSet(STANDARD_RULE_SET_ID, NoFooIdentifierRule("no-foo-identifier-standard")), + RuleSet(NON_STANDARD_RULE_SET_ID, NoFooIdentifierRule("no-foo-identifier")) + ), + cb = { e, _ -> add(e) } + ) + ) + } + + private companion object { + const val STANDARD_RULE_SET_ID = "standard" // Value may not be changed + const val NON_STANDARD_RULE_SET_ID = "custom" // Can be any value other than "standard" + } +}