From 4f6ed155cad0d0a651f476bd19270a8222147a47 Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Tue, 27 Jun 2023 10:40:43 +0200 Subject: [PATCH] Add rule to migrate away from ktlint-disable directives (#2068) * Add rule to migrate ktlint-disable directives from comments to @Suppress or @SuppressWarnings annotations Note that the SuppressionLocatorBuilder never ignore this rule as otherwise the suppression hint to ignore *all* rules would also ignore the rule which has to migrate this suppression hint. Closes #1947 * Fix max line length for markdown files * Fix wildcard imports settings * Improve trace logging by printing a lint error directly when it is emitted The reports print the lint violations in which they occur in the source code. This is not necessarily the order in which they are detected and fixed. For debugging some type of problems, the latter order is important. * Add "libs.logback" as runtime only dependency so that logging is visible in unit tests --- .editorconfig | 6 + CHANGELOG.md | 9 + documentation/snapshot/docs/faq.md | 49 +- .../snapshot/docs/rules/experimental.md | 4 - documentation/snapshot/docs/rules/standard.md | 48 + .../checkstyle/CheckStyleReporterTest.kt | 10 +- .../cli/reporter/format/FormatReporterTest.kt | 6 +- .../cli/reporter/html/HtmlReporterTest.kt | 4 +- .../cli/reporter/json/JsonReporterTest.kt | 10 +- .../plain/PlainSummaryReporterTest.kt | 16 +- .../cli/reporter/plain/PlainReporterTest.kt | 28 +- ktlint-rule-engine/build.gradle.kts | 1 + .../rule/engine/api/KtLintRuleEngine.kt | 47 +- .../engine/internal/RuleExecutionContext.kt | 6 +- .../engine/internal/RuleProviderSorter.kt | 9 + .../internal/SuppressionLocatorBuilder.kt | 94 +- .../rulefilter/InternalRuleProvidersFilter.kt | 38 + .../engine/internal/rulefilter/RuleFilter.kt | 2 +- .../engine/internal/rules/InternalRule.kt | 26 + .../internal/rules/KtlintSuppressionRule.kt | 685 +++++++++ .../internal/SuppressionLocatorBuilderTest.kt | 174 ++- .../InternalRuleProvidersFilterTest.kt | 62 + .../internal/rulefilter/RuleFilterKtTest.kt | 8 +- .../rules/KtlintSuppressionRuleTest.kt | 1329 +++++++++++++++++ .../standard/rules/MaxLineLengthRule.kt | 21 +- .../rules/NoSingleLineBlockCommentRule.kt | 11 - .../standard/rules/AnnotationRuleTest.kt | 6 +- .../rules/ArgumentListWrappingRuleTest.kt | 2 +- .../standard/rules/ClassNamingRuleTest.kt | 8 +- .../standard/rules/CommentWrappingRuleTest.kt | 6 +- .../rules/EnumEntryNameCaseRuleTest.kt | 4 +- .../standard/rules/FilenameRuleTest.kt | 6 +- .../standard/rules/FunctionNamingRuleTest.kt | 8 +- .../rules/FunctionSignatureRuleTest.kt | 5 +- .../standard/rules/IfElseBracingRuleTest.kt | 16 +- .../standard/rules/IfElseWrappingRuleTest.kt | 8 +- .../standard/rules/IndentationRuleTest.kt | 13 +- .../standard/rules/KdocWrappingRuleTest.kt | 4 +- .../standard/rules/MaxLineLengthRuleTest.kt | 28 +- .../rules/NoConsecutiveCommentsRuleTest.kt | 10 +- .../rules/NoSingleLineBlockCommentRuleTest.kt | 17 +- .../standard/rules/PackageNameRuleTest.kt | 11 +- .../rules/ParameterListSpacingRuleTest.kt | 20 +- .../rules/ParameterListWrappingRuleTest.kt | 4 +- .../standard/rules/PropertyNamingRuleTest.kt | 8 +- .../ImportOrderingRuleAsciiTest.kt | 4 +- .../ImportOrderingRuleIdeaTest.kt | 6 +- .../ktlint/test/KtlintDocumentationTest.kt | 11 + 48 files changed, 2540 insertions(+), 368 deletions(-) create mode 100644 ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/InternalRuleProvidersFilter.kt create mode 100644 ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rules/InternalRule.kt create mode 100644 ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rules/KtlintSuppressionRule.kt create mode 100644 ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/InternalRuleProvidersFilterTest.kt create mode 100644 ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/rules/KtlintSuppressionRuleTest.kt create mode 100644 ktlint-test/src/main/kotlin/com/pinterest/ktlint/test/KtlintDocumentationTest.kt diff --git a/.editorconfig b/.editorconfig index 00a10e0581..01626914c0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,6 +4,7 @@ root = true [*] indent_style = space indent_size = 2 +max_line_length = 140 end_of_line = lf charset = utf-8 @@ -26,8 +27,13 @@ ktlint_experimental = enabled # Don't allow any wildcard imports ij_kotlin_packages_to_use_import_on_demand = unset +# Prevent wildcard imports +ij_kotlin_name_count_to_use_star_import = 99 +ij_kotlin_name_count_to_use_star_import_for_members = 99 + [*.md] trim_trailing_whitespace = false +max_line_length = unset [gradle/verification-metadata.xml] indent_size = 3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 435c78789e..fe4aba945a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## Unreleased +### Deprecation of ktlint-enable and ktlint-disable directives + +The `ktlint-disable` and `ktlint-enable` directives are no longer supported. Ktlint rules can now only be suppressed using the `@Suppress` or `@SuppressWarnings` annotations. A new rule, `internal:ktlint-suppression`, is provided to replace the directives with annotations. + +API consumers do not need to provide this rule, but it does no harm when done. + +The `internal:ktlint-suppression` rule can not be disabled via the `.editorconfig` nor via `@Suppress` or `@SuppressWarnings` annotations. + ### Custom Rule Providers need to prepare for Kotlin 1.9 In Kotlin 1.9 the extension points of the embedded kotlin compiler will change. Ktlint only uses the `org.jetbrains.kotlin.com.intellij.treeCopyHandler` extension point. This extension is not yet supported in 1.9, neither is it documented ([#KT-58704](https://youtrack.jetbrains.com/issue/KT-58704/Support-and-document-extension-point-org.jetbrains.kotlin.com.intellij.treeCopyHandler)). Without this extension point it might happen that your custom rules will throw exceptions during runtime. See [#1981](https://github.com/pinterest/ktlint/issues/1981). @@ -32,6 +40,7 @@ At this point in time, it is not yet decided what the next steps will be. Ktlint * Add new experimental rule `statement-wrapping` which ensures function, class, or other blocks statement body doesn't start or end at starting or ending braces of the block ([#1938](https://github.com/pinterest/ktlint/issues/1938)) * Add new experimental rule `blank-line-before-declaration`. This rule requires a blank line before class, function or property declarations ([#1939](https://github.com/pinterest/ktlint/issues/1939)) * Wrap multiple statements on same line `wrapping` ([#1078](https://github.com/pinterest/ktlint/issues/1078)) +* Add new rule `ktlint-suppression` to replace the `ktlint-disable` and `ktlint-enable` directives with annotations. This rule can not be disabled via the `.editorconfig` ([#1947](https://github.com/pinterest/ktlint/issues/1947)) * Inform user about using `--format` option of KtLint CLI when finding a violation that can be autocorrected ([#1071](https://github.com/pinterest/ktlint/issues/1071)) ### Removed diff --git a/documentation/snapshot/docs/faq.md b/documentation/snapshot/docs/faq.md index c7343c8f45..31a6601b82 100644 --- a/documentation/snapshot/docs/faq.md +++ b/documentation/snapshot/docs/faq.md @@ -43,21 +43,17 @@ See [adding a custom rule set](../api/custom-rule-set/) for more information. !!! tip Suppressing a `ktlint` violation 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 the rule identifier which is displayed at the end of the lint error. +To disable a specific rule you'll need the fully qualified rule identifier. This identifier is displayed at the end of the lint error. In case your code was autocorrected, you need to revert the code and run the `lint` task instead of the `format` to find the rule identifier. -An error can be suppressed using: +As of Ktlint 0.50, an error can only be suppressed using @Suppress or @SuppressWarnings annotations -* EOL comments -* Block comments -* @Suppress annotations - -=== "[:material-heart:](#) Suppress annotation" +=== "[:material-heart:](#) Allowed" ```kotlin // Suppressing all rules for the entire file @file:Suppress("ktlint") - // Suppress a single rule (with id 'rule-id', defined in rule set with id 'rule-set-id') from the annotated construct + // Suppress a single rule (with id 'rule-id', defined in rule set with id 'rule-set-id') in the scope of the annotated construct @Suppress("ktlint:rule-set-id:rule-id") class Foo {} @@ -69,43 +65,6 @@ An error can be suppressed using: @Suppress("ktlint") import foo.* ``` -=== "[:material-heart:](#) EOL comments" - - ```kotlin - // Suppress a single rule for the commented line - import foo.* // ktlint-disable standard_no-wildcard-imports - - // Suppress multiple rules for the commented line - import foo.* // ktlint-disable standard_no-wildcard-imports standard_other-rule-id - - // Suppress all rules for the commented line - import foo.* // ktlint-disable - ``` - -=== "[:material-heart-off-outline:](#) Block comments" - - ```kotlin - // Suppress a single rule for all code between the start and end tag - /* ktlint-disable standard_no-wildcard-imports */ - import foo.* - /* ktlint-disable standard_no-wildcard-imports */ - - // Suppress multiple rules for all code between the start and end tag - /* ktlint-disable standard_no-wildcard-imports standard_no-wildcard-imports */ - import foo.* - /* ktlint-enable standard_no-wildcard-imports standard_no-wildcard-imports */ - - // Suppress all rules for all code between the start and end tag - /* ktlint-disable */ - import foo.* - /* ktlint-enable */ - ``` - -!!! important - When using the block comments, the `ktlint-enable` directive needs to specify the exact same rule-id's and in the same order as the `ktlint-disable` directive. - -!!! warning - 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. ## How do I globally disable a rule without `.editorconfig`? diff --git a/documentation/snapshot/docs/rules/experimental.md b/documentation/snapshot/docs/rules/experimental.md index b7b92ac4f6..95de4adc34 100644 --- a/documentation/snapshot/docs/rules/experimental.md +++ b/documentation/snapshot/docs/rules/experimental.md @@ -465,10 +465,6 @@ A single line block comment should be replaced with an EOL comment when possible */ val foo = "foo" // Some comment val foo = { /* no-op */ } - - /* ktlint-disable foo-rule-id bar-rule-id */ - val foo = "foo" - /* ktlint-enable foo-rule-id bar-rule-id */ ``` === "[:material-heart-off-outline:](#) Disallowed" diff --git a/documentation/snapshot/docs/rules/standard.md b/documentation/snapshot/docs/rules/standard.md index 8539f5cfef..1711bcfe42 100644 --- a/documentation/snapshot/docs/rules/standard.md +++ b/documentation/snapshot/docs/rules/standard.md @@ -251,6 +251,54 @@ Indentation formatting - respects `.editorconfig` `indent_size` with no continua Rule id: `indent` (`standard` rule set) +## Ktlint-suppression rule + +The `ktlint-disable` and `ktlint-enable` directives are no longer supported as of ktlint version `0.50.0`. This rule migrates the directives to Suppress or SuppressWarnings annotations. + +Identifiers in the @Suppress and @SuppressWarnings annotations to suppress ktlint rules are checked for validity and autocorrected when possible. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + @file:Suppress("ktlint:standard:no-wildcard-imports") + + class FooBar { + @Suppress("ktlint:standard:max-line-length") + val foo = "some longggggggggggggggggggg text" + + fun bar() = + @Suppress("ktlint:standard:no-multi-spaces") + listOf( + "1 One", + "10 Ten", + "100 Hundred", + ) + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + /* ktlint-disable standard:no-wildcard-imports */ + + class FooBar { + val foo = "some longggggggggggggggggggg text" // ktlint-disable standard:max-line-length + + fun bar() = + listOf( + /* ktlint-disable standard:no-multi-spaces */ + "1 One", + "10 Ten", + "100 Hundred", + /* ktlint-enable standard:no-multi-spaces */ + ) + } + ``` + +Rule id: `ktlint-suppression` (`standard` rule set) + +!!! note + This rule can not be disabled in the `.editorconfig`. + ## Max line length Ensures that lines do not exceed the given length of `.editorconfig` property `max_line_length` (see [EditorConfig](../configuration-ktlint/) section for more). This rule does not apply in a number of situations. For example, in the case a line exceeds the maximum line length due to a comment that disables ktlint rules then that comment is being ignored when validating the length of the line. The `.editorconfig` property `ktlint_ignore_back_ticked_identifier` can be set to ignore identifiers which are enclosed in backticks, which for example is very useful when you want to allow longer names for unit tests. diff --git a/ktlint-cli-reporter-checkstyle/src/test/kotlin/com/pinterest/ktlint/cli/reporter/checkstyle/CheckStyleReporterTest.kt b/ktlint-cli-reporter-checkstyle/src/test/kotlin/com/pinterest/ktlint/cli/reporter/checkstyle/CheckStyleReporterTest.kt index b890fc2986..bbc817cf86 100644 --- a/ktlint-cli-reporter-checkstyle/src/test/kotlin/com/pinterest/ktlint/cli/reporter/checkstyle/CheckStyleReporterTest.kt +++ b/ktlint-cli-reporter-checkstyle/src/test/kotlin/com/pinterest/ktlint/cli/reporter/checkstyle/CheckStyleReporterTest.kt @@ -15,29 +15,29 @@ class CheckStyleReporterTest { val reporter = CheckStyleReporter(PrintStream(out, true)) reporter.onLintError( "/one-fixed-and-one-not.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(1, 1, "rule-1", "<\"&'>", LINT_CAN_BE_AUTOCORRECTED), ) reporter.onLintError( "/one-fixed-and-one-not.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(2, 1, "rule-2", "And if you see my friend", FORMAT_IS_AUTOCORRECTED), ) reporter.onLintError( "/two-not-fixed.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(1, 10, "rule-1", "I thought I would again", LINT_CAN_BE_AUTOCORRECTED), ) reporter.onLintError( "/two-not-fixed.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(2, 20, "rule-2", "A single thin straight line", LINT_CAN_BE_AUTOCORRECTED), ) reporter.onLintError( "/all-corrected.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(1, 1, "rule-1", "I thought we had more time", FORMAT_IS_AUTOCORRECTED), ) reporter.afterAll() diff --git a/ktlint-cli-reporter-format/src/test/kotlin/com/pinterest/ktlint/cli/reporter/format/FormatReporterTest.kt b/ktlint-cli-reporter-format/src/test/kotlin/com/pinterest/ktlint/cli/reporter/format/FormatReporterTest.kt index 35e146f44a..cb36fc32c8 100644 --- a/ktlint-cli-reporter-format/src/test/kotlin/com/pinterest/ktlint/cli/reporter/format/FormatReporterTest.kt +++ b/ktlint-cli-reporter-format/src/test/kotlin/com/pinterest/ktlint/cli/reporter/format/FormatReporterTest.kt @@ -123,15 +123,15 @@ class FormatReporterTest { } companion object { - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") val SOME_LINT_ERROR_CAN_BE_AUTOCORRECTED = KtlintCliError(1, 1, "some-rule", "This error can be autocorrected", LINT_CAN_BE_AUTOCORRECTED) - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") val SOME_LINT_ERROR_CAN_NOT_BE_AUTOCORRECTED = KtlintCliError(1, 1, "rule-1", "This error can *not* be autocorrected", LINT_CAN_NOT_BE_AUTOCORRECTED) - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") val SOME_FORMAT_ERROR_IS_AUTOCORRECTED = KtlintCliError(1, 1, "rule-1", "This error can *not* be autocorrected", FORMAT_IS_AUTOCORRECTED) diff --git a/ktlint-cli-reporter-html/src/test/kotlin/com/pinterest/ktlint/cli/reporter/html/HtmlReporterTest.kt b/ktlint-cli-reporter-html/src/test/kotlin/com/pinterest/ktlint/cli/reporter/html/HtmlReporterTest.kt index 2a93880cda..51b8b78187 100644 --- a/ktlint-cli-reporter-html/src/test/kotlin/com/pinterest/ktlint/cli/reporter/html/HtmlReporterTest.kt +++ b/ktlint-cli-reporter-html/src/test/kotlin/com/pinterest/ktlint/cli/reporter/html/HtmlReporterTest.kt @@ -157,13 +157,13 @@ class HtmlReporterTest { val out = ByteArrayOutputStream() val reporter = HtmlReporter(PrintStream(out, true)) - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") reporter.onLintError( "/file1.kt", KtlintCliError(1, 1, "rule-1", "Error message contains a generic type like List (cannot be auto-corrected)", LINT_CAN_BE_AUTOCORRECTED), ) - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") reporter.onLintError( "/file1.kt", KtlintCliError(2, 1, "rule-2", "Error message contains special html symbols like ac\"d'e&f (cannot be auto-corrected)", LINT_CAN_BE_AUTOCORRECTED), diff --git a/ktlint-cli-reporter-json/src/test/kotlin/com/pinterest/ktlint/cli/reporter/json/JsonReporterTest.kt b/ktlint-cli-reporter-json/src/test/kotlin/com/pinterest/ktlint/cli/reporter/json/JsonReporterTest.kt index 7f22039b16..ea8f095977 100644 --- a/ktlint-cli-reporter-json/src/test/kotlin/com/pinterest/ktlint/cli/reporter/json/JsonReporterTest.kt +++ b/ktlint-cli-reporter-json/src/test/kotlin/com/pinterest/ktlint/cli/reporter/json/JsonReporterTest.kt @@ -15,29 +15,29 @@ class JsonReporterTest { val reporter = JsonReporter(PrintStream(out, true)) reporter.onLintError( "/one-fixed-and-one-not.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(1, 1, "rule-1", "<\"&'>", LINT_CAN_BE_AUTOCORRECTED), ) reporter.onLintError( "/one-fixed-and-one-not.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(2, 1, "rule-2", "And if you see my friend", FORMAT_IS_AUTOCORRECTED), ) reporter.onLintError( "/two-not-fixed.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(1, 10, "rule-1", "I thought I would again", LINT_CAN_BE_AUTOCORRECTED), ) reporter.onLintError( "/two-not-fixed.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(2, 20, "rule-2", "A single thin straight line", LINT_CAN_BE_AUTOCORRECTED), ) reporter.onLintError( "/all-corrected.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(1, 1, "rule-1", "I thought we had more time", FORMAT_IS_AUTOCORRECTED), ) reporter.afterAll() diff --git a/ktlint-cli-reporter-plain-summary/src/test/kotlin/com/pinterest/ktlint/cli/reporter/plain/PlainSummaryReporterTest.kt b/ktlint-cli-reporter-plain-summary/src/test/kotlin/com/pinterest/ktlint/cli/reporter/plain/PlainSummaryReporterTest.kt index 45ffbdb88a..121bc9d391 100644 --- a/ktlint-cli-reporter-plain-summary/src/test/kotlin/com/pinterest/ktlint/cli/reporter/plain/PlainSummaryReporterTest.kt +++ b/ktlint-cli-reporter-plain-summary/src/test/kotlin/com/pinterest/ktlint/cli/reporter/plain/PlainSummaryReporterTest.kt @@ -19,29 +19,29 @@ class PlainSummaryReporterTest { ).apply { onLintError( "file-1.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(1, 1, "rule-1", "description-error-at-position-1:1 (cannot be auto-corrected)", LINT_CAN_BE_AUTOCORRECTED), ) onLintError( "file-1.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(2, 1, "rule-2", "description-error-at-position-2:1", FORMAT_IS_AUTOCORRECTED), ) onLintError( "file-2.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(1, 10, "rule-1", "description-error-at-position-1:10 (cannot be auto-corrected)", LINT_CAN_BE_AUTOCORRECTED), ) onLintError( "file-2.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(2, 20, "rule-2", "description-error-at-position-2:20 (cannot be auto-corrected)", LINT_CAN_BE_AUTOCORRECTED), ) onLintError( "file-3.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(1, 1, "rule-1", "description-error-at-position-1:1", FORMAT_IS_AUTOCORRECTED), ) @@ -72,17 +72,17 @@ class PlainSummaryReporterTest { val reporter = PlainSummaryReporter(PrintStream(out, true)) reporter.onLintError( "file-1.kt", - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") KtlintCliError(18, 51, "", "Not a valid Kotlin file (18:51 unexpected tokens (use ';' to separate expressions on the same line)) (cannot be auto-corrected) ()", KOTLIN_PARSE_EXCEPTION), ) reporter.onLintError( "file-2.kt", - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") KtlintCliError(18, 51, "", "Not a valid Kotlin file (18:51 unexpected tokens (use ';' to separate expressions on the same line)) (cannot be auto-corrected) ()", KOTLIN_PARSE_EXCEPTION), ) reporter.onLintError( "file-3.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(18, 51, "", "Something else", LINT_CAN_BE_AUTOCORRECTED), ) reporter.afterAll() diff --git a/ktlint-cli-reporter-plain/src/test/kotlin/com/pinterest/ktlint/cli/reporter/plain/PlainReporterTest.kt b/ktlint-cli-reporter-plain/src/test/kotlin/com/pinterest/ktlint/cli/reporter/plain/PlainReporterTest.kt index 100ef42aa6..cf207b61cb 100644 --- a/ktlint-cli-reporter-plain/src/test/kotlin/com/pinterest/ktlint/cli/reporter/plain/PlainReporterTest.kt +++ b/ktlint-cli-reporter-plain/src/test/kotlin/com/pinterest/ktlint/cli/reporter/plain/PlainReporterTest.kt @@ -17,29 +17,29 @@ class PlainReporterTest { val reporter = PlainReporter(PrintStream(out, true)) reporter.onLintError( "file-1.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(1, 1, "rule-1", "description-error-at-position-1:1", LINT_CAN_BE_AUTOCORRECTED), ) reporter.onLintError( "file-1.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(2, 1, "rule-2", "description-error-at-position-2:1", FORMAT_IS_AUTOCORRECTED), ) reporter.onLintError( "file-2.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(1, 10, "rule-1", "description-error-at-position-1:10", LINT_CAN_BE_AUTOCORRECTED), ) reporter.onLintError( "file-2.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(2, 20, "rule-2", "description-error-at-position-2:20", LINT_CAN_BE_AUTOCORRECTED), ) reporter.onLintError( "file-3.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(1, 1, "rule-1", "description-error-at-position-1:1", FORMAT_IS_AUTOCORRECTED), ) @@ -65,17 +65,17 @@ class PlainReporterTest { val reporter = PlainReporter(PrintStream(out, true)) reporter.onLintError( "file-1.kt", - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") KtlintCliError(18, 51, "", "Not a valid Kotlin file (18:51 unexpected tokens (use ';' to separate expressions on the same line)) (cannot be auto-corrected) ()", KOTLIN_PARSE_EXCEPTION), ) reporter.onLintError( "file-2.kt", - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") KtlintCliError(18, 51, "", "Not a valid Kotlin file (18:51 unexpected tokens (use ';' to separate expressions on the same line)) (cannot be auto-corrected) ()", KOTLIN_PARSE_EXCEPTION), ) reporter.onLintError( "file-3.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(18, 51, "", "Something else", LINT_CAN_BE_AUTOCORRECTED), ) reporter.afterAll() @@ -106,7 +106,7 @@ class PlainReporterTest { ) reporter.onLintError( File.separator + "one-fixed-and-one-not.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(1, 1, "rule-1", "<\"&'>", LINT_CAN_BE_AUTOCORRECTED), ) val outputString = String(out.toByteArray()) @@ -130,29 +130,29 @@ class PlainReporterTest { val reporter = PlainReporter(PrintStream(out, true), groupByFile = true) reporter.onLintError( "/one-fixed-and-one-not.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(1, 1, "rule-1", "<\"&'>", LINT_CAN_BE_AUTOCORRECTED), ) reporter.onLintError( "/one-fixed-and-one-not.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(2, 1, "rule-2", "And if you see my friend", FORMAT_IS_AUTOCORRECTED), ) reporter.onLintError( "/two-not-fixed.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(1, 10, "rule-1", "I thought I would again", LINT_CAN_BE_AUTOCORRECTED), ) reporter.onLintError( "/two-not-fixed.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(2, 20, "rule-2", "A single thin straight line", LINT_CAN_BE_AUTOCORRECTED), ) reporter.onLintError( "/all-corrected.kt", - @Suppress("ktlint:argument-list-wrapping") + @Suppress("ktlint:standard:argument-list-wrapping") KtlintCliError(1, 1, "rule-1", "I thought we had more time", FORMAT_IS_AUTOCORRECTED), ) reporter.after("/one-fixed-and-one-not.kt") diff --git a/ktlint-rule-engine/build.gradle.kts b/ktlint-rule-engine/build.gradle.kts index 1c4daf4bb0..b9be8c80ca 100644 --- a/ktlint-rule-engine/build.gradle.kts +++ b/ktlint-rule-engine/build.gradle.kts @@ -11,4 +11,5 @@ dependencies { testImplementation(projects.ktlintTest) testImplementation(projects.ktlintRulesetStandard) + testRuntimeOnly(libs.logback) } diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/api/KtLintRuleEngine.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/api/KtLintRuleEngine.kt index f8c897033e..3fdbdc1129 100644 --- a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/api/KtLintRuleEngine.kt +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/api/KtLintRuleEngine.kt @@ -25,7 +25,6 @@ import org.ec4j.core.Resource import org.ec4j.core.model.PropertyType.EndOfLineValue.crlf import org.ec4j.core.model.PropertyType.EndOfLineValue.lf import org.jetbrains.kotlin.com.intellij.lang.FileASTNode -import org.jetbrains.kotlin.utils.addToStdlib.ifFalse import java.nio.charset.StandardCharsets import java.nio.file.FileSystem import java.nio.file.FileSystems @@ -108,7 +107,14 @@ public class KtLintRuleEngine( .invoke { rule -> ruleExecutionContext.executeRule(rule, false) { offset, errorMessage, canBeAutoCorrected -> val (line, col) = ruleExecutionContext.positionInTextLocator(offset) - errors.add(LintError(line, col, rule.ruleId, errorMessage, canBeAutoCorrected)) + LintError(line, col, rule.ruleId, errorMessage, canBeAutoCorrected) + .let { lintError -> + errors.add(lintError) + // In trace mode report the violation immediately. The order in which violations are actually found might be + // different from the order in which they are reported. For debugging purposes it can be helpful to know the + // exact order in which violations are being solved. + LOGGER.trace { "Lint violation: ${lintError.logMessage(code)}" } + } } } @@ -155,21 +161,20 @@ public class KtLintRuleEngine( ruleExecutionContext.rebuildSuppressionLocator() } val (line, col) = ruleExecutionContext.positionInTextLocator(offset) - errors.add( - Pair( - LintError(line, col, rule.ruleId, errorMessage, canBeAutoCorrected), - // It is assumed that a rule that emits that an error can be autocorrected, also - // does correct the error. - canBeAutoCorrected, - ), - ) - // In trace mode report the violation immediately. The order in which violations are actually found might be different - // from the order in which they are reported. For debugging purposes it cn be helpful to know the exact order in which - // violations are being solved. - LOGGER.trace { - "Format violation: ${code.fileNameOrStdin()}:$line:$col: $errorMessage (${rule.ruleId})" + - canBeAutoCorrected.ifFalse { " [cannot be autocorrected]" } - } + LintError(line, col, rule.ruleId, errorMessage, canBeAutoCorrected) + .let { lintError -> + errors.add( + Pair( + lintError, + // It is assumed that a rule that emits that an error can be autocorrected, also does correct the error. + canBeAutoCorrected, + ), + ) + // In trace mode report the violation immediately. The order in which violations are actually found might be + // different from the order in which they are reported. For debugging purposes it can be helpful to know the + // exact order in which violations are being solved. + LOGGER.trace { "Format violation: ${lintError.logMessage(code)}" } + } } } } while (mutated && formatRunCount < MAX_FORMAT_RUNS_PER_FILE) @@ -270,6 +275,14 @@ public class KtLintRuleEngine( public fun transformToAst(code: Code): FileASTNode = createRuleExecutionContext(this, code).rootNode + private fun LintError.logMessage(code: Code) = + "${code.fileNameOrStdin()}:$line:$col: $detail ($ruleId)" + + if (canBeAutoCorrected) { + "" + } else { + " [cannot be autocorrected]" + } + public companion object { internal const val UTF8_BOM = "\uFEFF" diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/RuleExecutionContext.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/RuleExecutionContext.kt index fee8e1c763..f5ec6eec62 100644 --- a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/RuleExecutionContext.kt +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/RuleExecutionContext.kt @@ -10,9 +10,10 @@ import com.pinterest.ktlint.rule.engine.core.api.Rule import com.pinterest.ktlint.rule.engine.core.api.RuleProvider import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig +import com.pinterest.ktlint.rule.engine.internal.rulefilter.InternalRuleProvidersFilter import com.pinterest.ktlint.rule.engine.internal.rulefilter.RuleExecutionRuleFilter import com.pinterest.ktlint.rule.engine.internal.rulefilter.RunAfterRuleFilter -import com.pinterest.ktlint.rule.engine.internal.rulefilter.ruleProviders +import com.pinterest.ktlint.rule.engine.internal.rulefilter.applyRuleFilters import mu.KotlinLogging import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.lang.FileASTNode @@ -180,7 +181,8 @@ internal class RuleExecutionContext private constructor( val ruleProviders = ktLintRuleEngine - .ruleProviders( + .applyRuleFilters( + InternalRuleProvidersFilter(ktLintRuleEngine), RuleExecutionRuleFilter(editorConfig), RunAfterRuleFilter(), ) diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/RuleProviderSorter.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/RuleProviderSorter.kt index f2e8cab23a..df3d121595 100644 --- a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/RuleProviderSorter.kt +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/RuleProviderSorter.kt @@ -5,6 +5,7 @@ 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.rule.engine.core.api.RuleSetId +import com.pinterest.ktlint.rule.engine.internal.rules.KTLINT_SUPPRESSION_RULE_ID import mu.KotlinLogging private val LOGGER = KotlinLogging.logger {}.initKtLintKLogger() @@ -104,6 +105,14 @@ internal class RuleProviderSorter { // The sort order below should guarantee a stable order of the rule between multiple invocations of KtLint given // the same set of input parameters. There should be no dependency on data ordering outside this class. compareBy { + if (it.ruleId == KTLINT_SUPPRESSION_RULE_ID) { + // This rule replaces the old ktlint-disable directives with @Suppress or @SuppressWarnings annotations. It should run as + // first rule as the SuppressionLocatorBuilder no longer transforms the ktlint-disable directives to suppressions. + 0 + } else { + 1 + } + }.thenBy { if (it.runAsLateAsPossible) { 1 } else { diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/SuppressionLocatorBuilder.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/SuppressionLocatorBuilder.kt index 3a70af99ac..fe2d2d1a1d 100644 --- a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/SuppressionLocatorBuilder.kt +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/SuppressionLocatorBuilder.kt @@ -1,6 +1,5 @@ package com.pinterest.ktlint.rule.engine.internal -import com.pinterest.ktlint.logger.api.initKtLintKLogger import com.pinterest.ktlint.rule.engine.core.api.RuleId import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpaceWithNewline @@ -9,7 +8,7 @@ import com.pinterest.ktlint.rule.engine.core.util.safeAs import com.pinterest.ktlint.rule.engine.internal.SuppressionLocatorBuilder.CommentSuppressionHint.Type.BLOCK_END import com.pinterest.ktlint.rule.engine.internal.SuppressionLocatorBuilder.CommentSuppressionHint.Type.BLOCK_START import com.pinterest.ktlint.rule.engine.internal.SuppressionLocatorBuilder.CommentSuppressionHint.Type.EOL -import mu.KotlinLogging +import com.pinterest.ktlint.rule.engine.internal.rules.KTLINT_SUPPRESSION_RULE_ID import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiComment import org.jetbrains.kotlin.com.intellij.psi.PsiElement @@ -30,8 +29,6 @@ internal object SuppressionLocatorBuilder { */ private val NO_SUPPRESSION: SuppressionLocator = { _, _ -> false } - private val LOGGER = KotlinLogging.logger {}.initKtLintKLogger() - /** * Mapping of non-ktlint annotations to ktlint-annotation so that ktlint rules will be suppressed automatically * when specific non-ktlint annotations are found. The prevents that developers have to specify multiple annotations @@ -39,17 +36,17 @@ internal object SuppressionLocatorBuilder { */ private val SUPPRESS_ANNOTATION_RULE_MAP = mapOf( - // It would have been nice if the official rule id's as defined in the Rules themselves could have been used here. But that would - // introduce a circular dependency between the ktlint-rule-engine and the ktlint-ruleset-standard modules. - "EnumEntryName" to RuleId("standard:enum-entry-name-case"), - "RemoveCurlyBracesFromTemplate" to RuleId("standard:string-template"), - "ClassName" to RuleId("standard:class-naming"), - "FunctionName" to RuleId("standard:function-naming"), - "PackageName" to RuleId("standard:package-name"), - "PropertyName" to RuleId("standard:property-naming"), + // It would have been nice if the official rule id's as defined in the Rules themselves could have been used here. But that + // would introduce a circular dependency between the ktlint-rule-engine and the ktlint-ruleset-standard modules. + "EnumEntryName" to "standard:enum-entry-name-case", + "RemoveCurlyBracesFromTemplate" to "standard:string-template", + "ClassName" to "standard:class-naming", + "FunctionName" to "standard:function-naming", + "PackageName" to "standard:package-name", + "PropertyName" to "standard:property-naming", ) private val SUPPRESS_ANNOTATIONS = setOf("Suppress", "SuppressWarnings") - private val SUPPRESS_ALL_KTLINT_RULES_RULE_ID = RuleId("ktlint:suppress-all-rules") + private const val ALL_KTLINT_RULES_SUPPRESSION_ID = "ktlint:suppress-all-rules" /** * Builds [SuppressionLocator] for given [rootNode] of AST tree. @@ -68,9 +65,15 @@ internal object SuppressionLocatorBuilder { private fun toSuppressedRegionsLocator(hintsList: List): SuppressionLocator = { offset, ruleId -> - hintsList - .filter { offset in it.range } - .any { hint -> hint.disabledRuleIds.isEmpty() || hint.disabledRuleIds.contains(ruleId) } + if (ruleId == KTLINT_SUPPRESSION_RULE_ID) { + // The rule to detect deprecated rule directives may not be disabled itself as otherwise the directives + // will not be reported and fixed. + false + } else { + hintsList + .filter { offset in it.range } + .any { hint -> hint.disabledRuleIds.isEmpty() || hint.disabledRuleIds.contains(ruleId.value) } + } } private fun collect( @@ -118,11 +121,11 @@ internal object SuppressionLocatorBuilder { .trim() .split(" ") .takeIf { it.isNotEmpty() } - ?.takeIf { it[0] == KTLINT_DISABLE || it[0] == formatterTags.formatterTagOff } + ?.takeIf { it[0] == formatterTags.formatterTagOff } ?.let { parts -> CommentSuppressionHint( this, - HashSet(parts.tailToRuleIds()), + HashSet(parts.tail()), EOL, ) } @@ -135,16 +138,16 @@ internal object SuppressionLocatorBuilder { .split(" ") .takeIf { it.isNotEmpty() } ?.let { parts -> - if (parts[0] == KTLINT_DISABLE || parts[0] == formatterTags.formatterTagOff) { + if (parts[0] == formatterTags.formatterTagOff) { CommentSuppressionHint( this, - HashSet(parts.tailToRuleIds()), + HashSet(parts.tail()), BLOCK_START, ) - } else if (parts[0] == KTLINT_ENABLE || parts[0] == formatterTags.formatterTagOn) { + } else if (parts[0] == formatterTags.formatterTagOn) { CommentSuppressionHint( this, - HashSet(parts.tailToRuleIds()), + HashSet(parts.tail()), BLOCK_END, ) } else { @@ -173,7 +176,6 @@ internal object SuppressionLocatorBuilder { BLOCK_END -> { // match open hint - Unit blockCommentSuppressionHints .lastOrNull { it.disabledRuleIds == commentSuppressionHint.disabledRuleIds } ?.let { openHint -> @@ -204,8 +206,6 @@ internal object SuppressionLocatorBuilder { ?.let { it.startOffset + it.text.lastIndexOf('\n') + 1 } ?: 0 - private fun List.tailToRuleIds() = tail().mapNotNull { createRuleIdOrNull(it) } - private fun List.tail() = this.subList(1, this.size) /** @@ -225,22 +225,22 @@ internal object SuppressionLocatorBuilder { .let { suppressedRuleIds -> when { suppressedRuleIds.isEmpty() -> null - suppressedRuleIds.contains(SUPPRESS_ALL_KTLINT_RULES_RULE_ID) -> + suppressedRuleIds.contains(ALL_KTLINT_RULES_SUPPRESSION_ID) -> SuppressionHint( - IntRange(ktAnnotated.startOffset, ktAnnotated.endOffset), + IntRange(ktAnnotated.startOffset, ktAnnotated.endOffset - 1), emptySet(), ) else -> SuppressionHint( - IntRange(ktAnnotated.startOffset, ktAnnotated.endOffset), + IntRange(ktAnnotated.startOffset, ktAnnotated.endOffset - 1), suppressedRuleIds.toSet(), ) } } } - private fun ValueArgument.toRuleId(annotationValueToRuleMapping: Map): RuleId? = + private fun ValueArgument.toRuleId(annotationValueToRuleMapping: Map): String? = getArgumentExpression() ?.text ?.removeSurrounding("\"") @@ -248,13 +248,15 @@ internal object SuppressionLocatorBuilder { when { argumentExpressionText == "ktlint" -> { // Disable all rules - SUPPRESS_ALL_KTLINT_RULES_RULE_ID + ALL_KTLINT_RULES_SUPPRESSION_ID } argumentExpressionText.startsWith("ktlint:") -> { - // Disable specific rule + // Disable specific rule. For backwards compatibility prefix rules without rule set id with the "standard" rule set + // id. Note that the KtlintSuppressionRule will emit a lint violation on the id. So this fix is only applicable for + // code bases in which the rule and suppression id's have not yet been fixed. argumentExpressionText .removePrefix("ktlint:") - .let { createRuleIdOrNull(it) } + .let { RuleId.prefixWithStandardRuleSetIdWhenMissing(it) } } else -> { // Disable specific rule if the annotation value is mapped to a specific rule @@ -263,39 +265,18 @@ internal object SuppressionLocatorBuilder { } } - private fun createRuleIdOrNull(ruleId: String): RuleId? = - try { - // For backwards compatibility the suppression hints have to be prefixed with the standard rule set id when the rule id is - // not prefixed with any rule set id. - RuleId - .prefixWithStandardRuleSetIdWhenMissing(ruleId) - .let { RuleId(it) } - } catch (illegalArgumentException: IllegalArgumentException) { - // Ktlint should not terminate with an exception in case the code being scanned contains a suppression for a non-existing rule. - // Instead, a warning should be printed and the invalid reference is to be ignored. The original ruleId is printed in the - // warning message so that user will not go searching for the fully qualified rule id while the code actually contained an - // unqualified ruleId. - LOGGER.warn { - """ - Can not suppress rule with id '$ruleId'. Please check and fix references to this rule in your code. - Underlying cause: ${illegalArgumentException.message} - """.trimIndent() - } - null - } - /** * @param range zero-based range of lines where lint errors should be suppressed * @param disabledRuleIds empty set means "all" */ private data class SuppressionHint( val range: IntRange, - val disabledRuleIds: Set = emptySet(), + val disabledRuleIds: Set = emptySet(), ) private data class CommentSuppressionHint( val node: ASTNode, - val disabledRuleIds: Set = emptySet(), + val disabledRuleIds: Set = emptySet(), val type: Type, ) { enum class Type { @@ -304,7 +285,4 @@ internal object SuppressionLocatorBuilder { BLOCK_END, } } - - private const val KTLINT_DISABLE = "ktlint-disable" - private const val KTLINT_ENABLE = "ktlint-enable" } diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/InternalRuleProvidersFilter.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/InternalRuleProvidersFilter.kt new file mode 100644 index 0000000000..c806ec1a65 --- /dev/null +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/InternalRuleProvidersFilter.kt @@ -0,0 +1,38 @@ +package com.pinterest.ktlint.rule.engine.internal.rulefilter + +import com.pinterest.ktlint.logger.api.initKtLintKLogger +import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine +import com.pinterest.ktlint.rule.engine.core.api.RuleProvider +import com.pinterest.ktlint.rule.engine.internal.rules.KtlintSuppressionRule +import mu.KotlinLogging + +private val LOGGER = KotlinLogging.logger {}.initKtLintKLogger() + +/** + * Add internal [RuleProvider]s. These rule providers always have to run regardless of the rules providers which are provided by the API + * consumer. In case the API consumer tries to provide a rule with the same rule id as an internal rule provider than it will be ignored. + */ +internal class InternalRuleProvidersFilter(private val ktLintRuleEngine: KtLintRuleEngine) : RuleFilter { + private val internalRuleProviders = + setOf( + RuleProvider { + KtlintSuppressionRule( + ktLintRuleEngine.ruleProviders.map { it.ruleId }, + ) + }, + ) + + override fun filter(ruleProviders: Set): Set { + val internalRuleIds = internalRuleProviders.map { it.ruleId } + return ruleProviders + .mapNotNullTo(mutableSetOf()) { + if (it.ruleId in internalRuleIds) { + LOGGER.error { "The provided rule with id '${it.ruleId}' is ignored in favour of Ktlint's rule with same id" } + null + } else { + it + } + }.plus(internalRuleProviders) + .toSet() + } +} diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RuleFilter.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RuleFilter.kt index 8d4ba3e137..49f4d04675 100644 --- a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RuleFilter.kt +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RuleFilter.kt @@ -7,7 +7,7 @@ import com.pinterest.ktlint.rule.engine.core.api.RuleProvider * Gets the rule provider for the [KtLintRuleEngine] by applying the [ruleFilters] in the given order on the set of [RuleProvider]s provided * by the previous (or the initial list of [RuleProvider]s). */ -internal fun KtLintRuleEngine.ruleProviders(vararg ruleFilters: RuleFilter): Set { +internal fun KtLintRuleEngine.applyRuleFilters(vararg ruleFilters: RuleFilter): Set { var ruleProviders = initialRuleProviders() val ruleFilterIterator = ruleFilters.iterator() while (ruleFilterIterator.hasNext()) { diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rules/InternalRule.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rules/InternalRule.kt new file mode 100644 index 0000000000..963e302aec --- /dev/null +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rules/InternalRule.kt @@ -0,0 +1,26 @@ +package com.pinterest.ktlint.rule.engine.internal.rules + +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.editorconfig.EditorConfigProperty + +internal val INTERNAL_RULE_ABOUT = + Rule.About( + maintainer = "KtLint", + repositoryUrl = "https://github.com/pinterest/ktlint", + issueTrackerUrl = "https://github.com/pinterest/ktlint/issues", + ) + +/** + * Internal rules can only be declared and instantiated in the 'ktlint-rule-engine'. + */ +public open class InternalRule internal constructor( + id: String, + override val visitorModifiers: Set = emptySet(), + override val usesEditorConfigProperties: Set> = emptySet(), +) : Rule( + ruleId = RuleId("internal:$id"), + visitorModifiers = visitorModifiers, + usesEditorConfigProperties = usesEditorConfigProperties, + about = INTERNAL_RULE_ABOUT, +) diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rules/KtlintSuppressionRule.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rules/KtlintSuppressionRule.kt new file mode 100644 index 0000000000..dfa0c295fa --- /dev/null +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rules/KtlintSuppressionRule.kt @@ -0,0 +1,685 @@ +package com.pinterest.ktlint.rule.engine.internal.rules + +import com.pinterest.ktlint.rule.engine.core.api.ElementType +import com.pinterest.ktlint.rule.engine.core.api.ElementType.ANNOTATION +import com.pinterest.ktlint.rule.engine.core.api.ElementType.ANNOTATION_ENTRY +import com.pinterest.ktlint.rule.engine.core.api.ElementType.BLOCK_COMMENT +import com.pinterest.ktlint.rule.engine.core.api.ElementType.CLASS +import com.pinterest.ktlint.rule.engine.core.api.ElementType.EOL_COMMENT +import com.pinterest.ktlint.rule.engine.core.api.ElementType.FILE +import com.pinterest.ktlint.rule.engine.core.api.ElementType.FILE_ANNOTATION_LIST +import com.pinterest.ktlint.rule.engine.core.api.ElementType.FUN +import com.pinterest.ktlint.rule.engine.core.api.ElementType.MODIFIER_LIST +import com.pinterest.ktlint.rule.engine.core.api.ElementType.PACKAGE_DIRECTIVE +import com.pinterest.ktlint.rule.engine.core.api.ElementType.PROPERTY +import com.pinterest.ktlint.rule.engine.core.api.ElementType.PROPERTY_ACCESSOR +import com.pinterest.ktlint.rule.engine.core.api.ElementType.STRING_TEMPLATE +import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_ARGUMENT +import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_ARGUMENT_LIST +import com.pinterest.ktlint.rule.engine.core.api.RuleId +import com.pinterest.ktlint.rule.engine.core.api.indent +import com.pinterest.ktlint.rule.engine.core.api.isRoot +import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpace +import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpaceWithNewline +import com.pinterest.ktlint.rule.engine.core.api.nextLeaf +import com.pinterest.ktlint.rule.engine.core.api.nextSibling +import com.pinterest.ktlint.rule.engine.core.api.parent +import com.pinterest.ktlint.rule.engine.core.api.prevLeaf +import com.pinterest.ktlint.rule.engine.internal.rules.KtLintDirective.KtlintDirectiveType.KTLINT_DISABLE +import com.pinterest.ktlint.rule.engine.internal.rules.KtLintDirective.KtlintDirectiveType.KTLINT_ENABLE +import com.pinterest.ktlint.rule.engine.internal.rules.KtLintDirective.SuppressionIdChange.InvalidSuppressionId +import com.pinterest.ktlint.rule.engine.internal.rules.KtLintDirective.SuppressionIdChange.ValidSuppressionId +import com.pinterest.ktlint.rule.engine.internal.rules.KtlintSuppressionRule.SuppressAnnotationType.SUPPRESS +import com.pinterest.ktlint.rule.engine.internal.rules.KtlintSuppressionRule.SuppressAnnotationType.SUPPRESS_WARNINGS +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.com.intellij.psi.PsiFileFactory +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl +import org.jetbrains.kotlin.idea.KotlinLanguage +import org.jetbrains.kotlin.psi.KtAnnotationEntry +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtClassInitializer +import org.jetbrains.kotlin.psi.KtCollectionLiteralExpression +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtDeclarationModifierList +import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtFileAnnotationList +import org.jetbrains.kotlin.psi.KtFunctionLiteral +import org.jetbrains.kotlin.psi.KtLambdaExpression +import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.KtPrimaryConstructor +import org.jetbrains.kotlin.psi.KtScript +import org.jetbrains.kotlin.psi.KtScriptInitializer +import org.jetbrains.kotlin.psi.KtStringTemplateExpression +import org.jetbrains.kotlin.psi.KtValueArgument +import org.jetbrains.kotlin.psi.KtValueArgumentList +import org.jetbrains.kotlin.psi.psiUtil.children +import org.jetbrains.kotlin.psi.psiUtil.findDescendantOfType +import org.jetbrains.kotlin.psi.psiUtil.getChildOfType +import org.jetbrains.kotlin.psi.psiUtil.siblings +import org.jetbrains.kotlin.util.prefixIfNot +import org.jetbrains.kotlin.utils.addToStdlib.applyIf + +private const val KTLINT_SUPPRESSION_ID_PREFIX = "ktlint:" +private const val KTLINT_SUPPRESSION_ID_ALL_RULES = "\"ktlint\"" +private const val DOUBLE_QUOTE = "\"" + +/** + * Disallow usage of the old "ktlint-disable" and "ktlint-enable" directives. + * + * A ktlint-disable directive is replaced with an annotation on the closest parent declaration or expression, or as annotation on the file + * level in case the directive is associated with a top level element. Ktlint-disable directives placed in block comments are only + * autocorrected when placed as top level element or in case a matching Ktlint-enable directive is found in the same parent as the disable + * directive. + * + * If the target element is annotated with a [Suppress] (or, if missing, is annotated with a [SuppressWarnings] annotation) then the + * ktlint-directive will be matched against this annotation. If this annotation already contains a suppression for *all* ktlint rules, or + * for the specific rule id, then it is not added to annotation as it would be redundant. In case a suppression identifier is added to an + * existing annotation then all identifiers in the annotation are alphabetically sorted. + * + * If the target element is not annotated with [Suppress] or [SuppressWarnings] then a [Suppress] annotation is added. + * + * Ktlint-enable directives are removed as annotations have a scope in which the suppression will be active. + */ +public class KtlintSuppressionRule(private val allowedRuleIds: List) : InternalRule("ktlint-suppression") { + private val allowedRuleIdAsStrings = allowedRuleIds.map { it.value } + + private val ruleIdValidator: (String) -> Boolean = { ruleId -> allowedRuleIdAsStrings.contains(ruleId) } + + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + node + .takeIf { isKtlintRuleSuppressionInAnnotation(it) } + ?.let { visitKtlintSuppressionInAnnotation(node, autoCorrect, emit) } + + node + .ktlintDirectiveOrNull(ruleIdValidator) + ?.visitKtlintDirective(autoCorrect, emit) + } + + private fun isKtlintRuleSuppressionInAnnotation(node: ASTNode) = + node + .takeIf { it.elementType == STRING_TEMPLATE } + ?.takeIf { it.text.isKtlintSuppressionId() } + ?.let { literalStringTemplate -> + literalStringTemplate + .parent(VALUE_ARGUMENT) + ?.isPartOfAnnotation() + } + ?: false + + private fun String.isKtlintSuppressionId() = removePrefix(DOUBLE_QUOTE).startsWith(KTLINT_SUPPRESSION_ID_PREFIX) + + private fun ASTNode.isPartOfAnnotation() = parent { it.elementType == ANNOTATION || it.elementType == ANNOTATION_ENTRY } != null + + private fun visitKtlintSuppressionInAnnotation( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + node + .psi + .findDescendantOfType() + ?.node + ?.let { literalStringTemplateEntry -> + val prefixedSuppression = + literalStringTemplateEntry + .text + .prefixKtlintSuppressionWithRuleSetIdOrNull() + val offset = literalStringTemplateEntry.startOffset + KTLINT_SUPPRESSION_ID_PREFIX.length + if (prefixedSuppression.isUnknownKtlintSuppression()) { + emit(offset, "Ktlint rule with id '$prefixedSuppression' is unknown or not loaded", false) + } else if (prefixedSuppression != literalStringTemplateEntry.text) { + emit(offset, "Identifier to suppress ktlint rule must be fully qualified with the rule set id", true) + if (autoCorrect) { + node + .createLiteralStringTemplateEntry(prefixedSuppression) + ?.let { literalStringTemplateEntry.replaceWith(it) } + } + } + } + } + + private fun ASTNode.createLiteralStringTemplateEntry(prefixedSuppression: String) = + PsiFileFactory + .getInstance(psi.project) + .createFileFromText(KotlinLanguage.INSTANCE, "listOf(\"$prefixedSuppression\")") + .getChildOfType() + ?.getChildOfType() + ?.getChildOfType() + ?.getChildOfType() + ?.getChildOfType() + ?.getChildOfType() + ?.getChildOfType() + ?.getChildOfType() + ?.node + + private fun String.prefixKtlintSuppressionWithRuleSetIdOrNull(): String { + val isPrefixedWithDoubleQuote = startsWith(DOUBLE_QUOTE) + return removePrefix(DOUBLE_QUOTE) + .takeIf { startsWith(KTLINT_SUPPRESSION_ID_PREFIX) } + ?.substringAfter(KTLINT_SUPPRESSION_ID_PREFIX) + ?.let { prefixWithRuleSetIdWhenMissing(it) } + ?.prefixIfNot(KTLINT_SUPPRESSION_ID_PREFIX) + ?.applyIf(isPrefixedWithDoubleQuote) { prefixIfNot(DOUBLE_QUOTE) } + ?: this + } + + private fun String.isUnknownKtlintSuppression(): Boolean = + removePrefix(DOUBLE_QUOTE) + .takeIf { startsWith(KTLINT_SUPPRESSION_ID_PREFIX) } + ?.substringAfter(KTLINT_SUPPRESSION_ID_PREFIX) + ?.let { prefixWithRuleSetIdWhenMissing(it) } + ?.let { ruleId -> + allowedRuleIds.none { it.value == ruleId } + } + ?: false + + private fun KtLintDirective.visitKtlintDirective( + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + when (ktlintDirectiveType) { + KTLINT_DISABLE -> { + if (node.elementType == EOL_COMMENT && node.prevLeaf().isWhiteSpaceWithNewline()) { + removeDanglingEolCommentWithKtlintDisableDirective(autoCorrect, emit) + } else { + visitKtlintDisableDirective(autoCorrect, emit) + } + } + + KTLINT_ENABLE -> { + removeKtlintEnableDirective(autoCorrect, emit) + } + } + } + + private fun KtLintDirective.removeDanglingEolCommentWithKtlintDisableDirective( + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + emit(offset, "Directive 'ktlint-disable' in EOL comment is ignored as it is not preceded by a code element", true) + if (autoCorrect) { + node.removePrecedingWhitespace() + node.remove() + } + } + + private fun KtLintDirective.visitKtlintDisableDirective( + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + if (node.elementType == BLOCK_COMMENT && hasNoMatchingKtlintEnableDirective(ruleIdValidator)) { + emit( + offset, + "Directive 'ktlint-disable' is deprecated. The matching 'ktlint-enable' directive is not found in same scope. Replace " + + "with @Suppress annotation", + false, + ) + return + } + emit(offset, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation", true) + suppressionIdChanges + .filterIsInstance() + .forEach { ktlintDirectiveChange -> + emit( + offset + + ktlintDirectiveType.id.length + + ktlintDirectiveChange.offsetOriginalRuleId, + "Ktlint rule with id '${ktlintDirectiveChange.originalRuleId}' is unknown or not loaded", + false, + ) + } + if (autoCorrect) { + findParentDeclarationOrExpression() + .addKtlintRuleSuppression(suppressionIdChanges) + if (node.elementType == EOL_COMMENT) { + node.removePrecedingWhitespace() + } else { + if (node.nextLeaf().isWhiteSpaceWithNewline()) { + node + .nextLeaf() + ?.remove() + } else { + node.removePrecedingWhitespace() + } + } + node.remove() + } + } + + private fun KtLintDirective.findParentDeclarationOrExpression(): ASTNode { + val shouldBeConvertedToFileAnnotation = shouldBeConvertedToFileAnnotation() + var targetNode = + if (node.elementType == BLOCK_COMMENT && + shouldBePromotedToParentDeclaration(ruleIdValidator) + ) { + node.treeParent.treeParent.psi + } else { + node.psi + } + while ( + shouldBeConvertedToFileAnnotation || + targetNode is KtClassInitializer || + targetNode is KtBlockExpression || + targetNode is KtPrimaryConstructor || + targetNode is KtFunctionLiteral || + targetNode is KtLambdaExpression || + (targetNode is KtExpression && targetNode.parent is KtExpression && targetNode.parent !is KtDeclaration) || + (targetNode !is KtDeclaration && targetNode !is KtExpression) + ) { + if (targetNode.parent == null) { + return targetNode.node + } + targetNode = targetNode.parent + } + return targetNode.node + } + + private fun ASTNode.remove() { + treeParent.removeChild(this) + } + + private fun ASTNode.removePrecedingWhitespace() { + prevLeaf() + .takeIf { it.isWhiteSpace() } + ?.remove() + } + + private fun ASTNode.addKtlintRuleSuppression(suppressionIdChanges: Set) { + val ktlintRuleSuppressions = + suppressionIdChanges + .filterIsInstance() + .map { it.suppressionId } + .toSet() + if (ktlintRuleSuppressions.isEmpty()) { + // Do not add or alter the @Suppress / @SuppressWarnings + return + } + val suppressionAnnotations = findSuppressionAnnotations() + // Add ktlint rule suppressions: + // - To the @Suppress annotation if found + // - otherwise to the @SuppressWarnings annotation if found + // - otherwise create a new @Suppress annotation + when { + suppressionAnnotations.containsKey(SUPPRESS) -> + ktlintRuleSuppressions.mergeInto(suppressionAnnotations.getValue(SUPPRESS), SUPPRESS) + + suppressionAnnotations.containsKey(SUPPRESS_WARNINGS) -> + ktlintRuleSuppressions.mergeInto(suppressionAnnotations.getValue(SUPPRESS_WARNINGS), SUPPRESS_WARNINGS) + + else -> createSuppressAnnotation(SUPPRESS, ktlintRuleSuppressions) + } + } + + private fun Set.mergeInto( + annotationNode: ASTNode, + suppressType: SuppressAnnotationType, + ) { + annotationNode + .existingSuppressions() + .plus(this) + .let { suppressions -> + if (suppressions.contains(KTLINT_SUPPRESSION_ID_ALL_RULES)) { + // When all ktlint rules are to be suppressed, then ignore all suppressions for specific ktlint rules + suppressions + .filterNot { it.isKtlintSuppressionId() } + .toSet() + } else { + suppressions + } + }.map { it.prefixKtlintSuppressionWithRuleSetIdOrNull() } + .toSet() + .let { suppressions -> annotationNode.createSuppressAnnotation(suppressType, suppressions) } + } + + private fun ASTNode.existingSuppressions() = + existingSuppressionsFromNamedArgumentOrNull() + ?: getValueArguments() + + private fun ASTNode.existingSuppressionsFromNamedArgumentOrNull() = + psi + .findDescendantOfType() + ?.children + ?.map { it.text } + ?.toSet() + + private fun ASTNode.findSuppressionAnnotations(): Map = + if (this.isRoot()) { + findChildByType(FILE_ANNOTATION_LIST) + ?.findSuppressionAnnotationsInModifierList() + .orEmpty() + } else { + findChildByType(MODIFIER_LIST) + ?.findSuppressionAnnotationsInModifierList() + .orEmpty() + } + + private fun ASTNode.findSuppressionAnnotationsInModifierList(): Map = + children() + .mapNotNull { modifier -> + when (modifier.suppressionAnnotationTypeOrNull()) { + SUPPRESS -> Pair(SUPPRESS, modifier) + SUPPRESS_WARNINGS -> Pair(SUPPRESS_WARNINGS, modifier) + else -> null + } + }.toMap() + + private fun ASTNode.suppressionAnnotationTypeOrNull() = + takeIf { elementType == ANNOTATION || elementType == ANNOTATION_ENTRY } + ?.findChildByType(ElementType.CONSTRUCTOR_CALLEE) + ?.findChildByType(ElementType.TYPE_REFERENCE) + ?.findChildByType(ElementType.USER_TYPE) + ?.findChildByType(ElementType.REFERENCE_EXPRESSION) + ?.findChildByType(ElementType.IDENTIFIER) + ?.text + ?.let { SuppressAnnotationType.findByIdOrNull(it) } + + private fun ASTNode.getValueArguments() = + findChildByType(VALUE_ARGUMENT_LIST) + ?.children() + ?.filter { it.elementType == VALUE_ARGUMENT } + ?.map { it.text } + ?.toSet() + .orEmpty() + + private fun ASTNode.createSuppressAnnotation( + suppressType: SuppressAnnotationType, + suppressions: Set, + ) { + val targetNode = + if (elementType == ANNOTATION_ENTRY) { + treeParent + } else { + this + } + + if (targetNode.psi is KtFile || targetNode.psi is KtFileAnnotationList) { + val fileAnnotation = + targetNode + .psi + .createFileAnnotation( + suppressType, + suppressions + .sorted() + .joinToString(), + ).node + if (targetNode.elementType == FILE_ANNOTATION_LIST) { + this.replaceWith(fileAnnotation.firstChildNode) + } else { + this.createFileAnnotationList(fileAnnotation) + } + } else { + val modifierListWithAnnotation = + targetNode + .psi + .createModifierListWithAnnotationEntry( + suppressType, + suppressions + .sorted() + .joinToString(), + ) + when (elementType) { + ANNOTATION_ENTRY -> + this.replaceWith( + modifierListWithAnnotation + .getChildOfType()!! + .node, + ) + CLASS, FUN, PROPERTY, PROPERTY_ACCESSOR -> { + this.addChild(PsiWhiteSpaceImpl(indent()), this.firstChildNode) + this.addChild(modifierListWithAnnotation.node, this.firstChildNode) + } + else -> { + treeParent.addChild( + modifierListWithAnnotation + .getChildOfType()!! + .node, + this, + ) + treeParent.addChild(PsiWhiteSpaceImpl(indent()), this) + } + } + } + } + + private fun PsiElement.createFileAnnotation( + suppressType: SuppressAnnotationType, + sortedSuppressionsString: String, + ): PsiElement = + "@file:${suppressType.annotationName}($sortedSuppressionsString)" + .let { annotation -> + PsiFileFactory + .getInstance(project) + .createFileFromText(KotlinLanguage.INSTANCE, annotation) + ?.firstChild + ?: throw IllegalStateException("Can not create annotation '$annotation'") + } + + private fun ASTNode.createFileAnnotationList(annotation: ASTNode) { + require(isRoot()) { "File annotation list can only be created for root node" } + // Should always be inserted into the first (root) code child regardless in which root node the ktlint directive + // was actually found + findChildByType(PACKAGE_DIRECTIVE) + ?.let { packageDirective -> + packageDirective + .treeParent + .addChild(annotation, packageDirective) + packageDirective + .treeParent + .addChild(PsiWhiteSpaceImpl("\n" + indent()), packageDirective) + } + } + + private fun PsiElement.createModifierListWithAnnotationEntry( + suppressType: SuppressAnnotationType, + sortedSuppressionsString: String, + ): PsiElement = + "@${suppressType.annotationName}($sortedSuppressionsString)" + .let { annotation -> + PsiFileFactory + .getInstance(project) + .createFileFromText( + KotlinLanguage.INSTANCE, + // Create the annotation for a dummy declaration as the entire code block should be valid Kotlin code + """ + $annotation + fun foo() {} + """.trimIndent(), + ).getChildOfType() + ?.getChildOfType() + ?.getChildOfType() + ?.getChildOfType() + ?: throw IllegalStateException("Can not create annotation '$annotation'") + } + + private fun KtLintDirective.removeKtlintEnableDirective( + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + emit(offset, "Directive 'ktlint-enable' is obsolete after migrating to suppress annotations", true) + if (autoCorrect) { + node.removePrecedingWhitespace() + node.remove() + } + } + + private fun ASTNode.replaceWith(node: ASTNode) { + treeParent.addChild(node, this) + this.remove() + } + + private enum class SuppressAnnotationType(val annotationName: String) { + SUPPRESS("Suppress"), + SUPPRESS_WARNINGS("SuppressWarnings"), + ; + + companion object { + fun findByIdOrNull(id: String): SuppressAnnotationType? = + SuppressAnnotationType + .values() + .firstOrNull { it.annotationName == id } + } + } +} + +private data class KtLintDirective( + val node: ASTNode, + val ktlintDirectiveType: KtlintDirectiveType, + val ktlintDirectives: String, + val suppressionIdChanges: Set, +) { + val offset = node.startOffset + node.text.indexOf(ktlintDirectiveType.id) + + fun hasNoMatchingKtlintEnableDirective(ruleIdValidator: (String) -> Boolean): Boolean { + require(ktlintDirectiveType == KTLINT_DISABLE && node.elementType == BLOCK_COMMENT) + + return if (shouldBeConvertedToFileAnnotation()) { + false + } else { + findMatchingKtlintEnableDirective(ruleIdValidator) == null + } + } + + private fun findMatchingKtlintEnableDirective(ruleIdValidator: (String) -> Boolean) = + node + .applyIf(node.isSuppressibleDeclaration()) { node.treeParent } + .siblings() + .firstOrNull { + it + .ktlintDirectiveOrNull(ruleIdValidator) + ?.takeIf { it.ktlintDirectiveType == KTLINT_ENABLE } + ?.ktlintDirectives == ktlintDirectives + } + + fun shouldBeConvertedToFileAnnotation() = + node.isTopLevel() || + (node.elementType == BLOCK_COMMENT && node.isSuppressibleDeclaration() && node.treeParent.isTopLevel()) + + private fun ASTNode.isSuppressibleDeclaration() = + when (treeParent.elementType) { + CLASS, FUN, PROPERTY -> true + else -> false + } + + private fun ASTNode.isTopLevel() = + FILE == + this + .treeParent + .elementType + + fun shouldBePromotedToParentDeclaration(ruleIdValidator: (String) -> Boolean): Boolean { + require(ktlintDirectiveType == KTLINT_DISABLE && node.elementType == BLOCK_COMMENT) + + return if (shouldBeConvertedToFileAnnotation()) { + false + } else { + node + .takeIf { it.isSuppressibleDeclaration() } + ?.let { findMatchingKtlintEnableDirective(ruleIdValidator) } + ?.let { matchingKtlintEnabledDirective -> + // In case the node is part of a suppressible declaration and the next sibling matches the enable directive then the + // block directive should be match with this declaration only and not be moved to the parent. + matchingKtlintEnabledDirective != + node + .treeParent + .nextSibling { !it.isWhiteSpace() } + } + ?: false + } + } + + enum class KtlintDirectiveType(val id: String) { + KTLINT_DISABLE("ktlint-disable"), + KTLINT_ENABLE("ktlint-enable"), + } + + sealed class SuppressionIdChange { + class ValidSuppressionId(val suppressionId: String) : SuppressionIdChange() + + class InvalidSuppressionId( + val originalRuleId: String, + val offsetOriginalRuleId: Int, + ) : SuppressionIdChange() + } +} + +private fun ASTNode.ktlintDirectiveOrNull(ruleIdValidator: (String) -> Boolean): KtLintDirective? { + val ktlintDirectiveString = + when (elementType) { + EOL_COMMENT -> + text + .removePrefix("//") + .trim() + + BLOCK_COMMENT -> + text + .removePrefix("/*") + .removeSuffix("*/") + .trim() + + else -> + return null + } + val ktlintDirectiveType = + ktlintDirectiveString.toKtlintDirectiveTypeOrNull() + ?: return null + val ruleIds = ktlintDirectiveString.removePrefix(ktlintDirectiveType.id) + val suppressionIdChanges = ruleIds.toSuppressionIdChanges(ruleIdValidator) + + return KtLintDirective(this, ktlintDirectiveType, ruleIds, suppressionIdChanges) +} + +private fun String.toKtlintDirectiveTypeOrNull() = + when { + startsWith(KTLINT_DISABLE.id) -> KTLINT_DISABLE + startsWith(KTLINT_ENABLE.id) -> KTLINT_ENABLE + else -> null + } + +// Transform the string: "ktlint-disable foo standard:bar" +// to a (sorted) list containing elements: +// ktlint:standard:bar +// ktlint:standard:foo +private fun String.toSuppressionIdChanges(ruleIdValidator: (String) -> Boolean) = + trim() + .split(" ") + .map { it.trim() } + .filter { it.isNotBlank() } + .map { originalRuleId -> + val prefixedRuleId = prefixWithRuleSetIdWhenMissing(originalRuleId) + if (ruleIdValidator(prefixedRuleId)) { + ValidSuppressionId( + prefixedRuleId + .prefixIfNot(KTLINT_SUPPRESSION_ID_PREFIX) + .surroundWith(DOUBLE_QUOTE), + ) + } else { + InvalidSuppressionId( + originalRuleId, + this.indexOf(originalRuleId), + ) + } + }.toSet() + .ifEmpty { setOf(ValidSuppressionId("\"ktlint\"")) } + +private fun prefixWithRuleSetIdWhenMissing(ruleIdString: String) = + RuleId + .prefixWithStandardRuleSetIdWhenMissing( + // The experimental ruleset was removed in Ktlint 0.49. References to that ruleset however may still exist. Also, not all + // user seem to understand that it is no longer a separate ruleset. + ruleIdString.removePrefix("experimental:"), + ) + +private fun String.surroundWith(string: String) = + removeSurrounding(string) + .prefixIfNot(string) + .plus(string) + +public val KTLINT_SUPPRESSION_RULE_ID: RuleId = KtlintSuppressionRule(emptyList()).ruleId diff --git a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/SuppressionLocatorBuilderTest.kt b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/SuppressionLocatorBuilderTest.kt index c0acb7df7c..b7c2d4b579 100644 --- a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/SuppressionLocatorBuilderTest.kt +++ b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/SuppressionLocatorBuilderTest.kt @@ -2,6 +2,7 @@ package com.pinterest.ktlint.rule.engine.internal import com.pinterest.ktlint.rule.engine.api.Code import com.pinterest.ktlint.rule.engine.api.EditorConfigOverride +import com.pinterest.ktlint.rule.engine.api.EditorConfigOverride.Companion.EMPTY_EDITOR_CONFIG_OVERRIDE import com.pinterest.ktlint.rule.engine.api.EditorConfigOverride.Companion.plus import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine import com.pinterest.ktlint.rule.engine.api.LintError @@ -15,6 +16,8 @@ import com.pinterest.ktlint.rule.engine.core.api.editorconfig.createRuleExecutio import com.pinterest.ktlint.rule.engine.internal.FormatterTags.Companion.FORMATTER_TAGS_ENABLED_PROPERTY import com.pinterest.ktlint.rule.engine.internal.FormatterTags.Companion.FORMATTER_TAG_OFF_ENABLED_PROPERTY import com.pinterest.ktlint.rule.engine.internal.FormatterTags.Companion.FORMATTER_TAG_ON_ENABLED_PROPERTY +import com.pinterest.ktlint.rule.engine.internal.rules.KTLINT_SUPPRESSION_RULE_ID +import com.pinterest.ktlint.ruleset.standard.rules.IndentationRule import org.assertj.core.api.Assertions.assertThat import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.junit.jupiter.api.Nested @@ -37,51 +40,16 @@ class SuppressionLocatorBuilderTest { } @Test - fun `Given that a NoFooIdentifierRule violation is suppressed with an EOL-comment to disable all rules then do not find a violation`() { + fun `Given a line having a NoFooIdentifierRule violation and an EOL-comment with a ktlint-directive to disable all rules then do not suppress the violation anymore`() { 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 $NON_STANDARD_RULE_SET_ID: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, "standard:no-foo-identifier-standard"), - lintError(4, 5, "$NON_STANDARD_RULE_SET_ID:no-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 $NON_STANDARD_RULE_SET_ID:no-foo-identifier */ - val fooNotReported = "foo" - /* ktlint-enable no-foo-identifier-standard $NON_STANDARD_RULE_SET_ID:no-foo-identifier */ - val fooReported = "foo" - """.trimIndent() - assertThat(lint(code)).containsExactly( - lintError(4, 5, "standard:no-foo-identifier-standard"), - lintError(4, 5, "$NON_STANDARD_RULE_SET_ID:no-foo-identifier"), - ) + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") + assertThat(lint(code)) + .contains( + LintError(1, 5, STANDARD_NO_FOO_IDENTIFIER_RULE_ID, "Line should not contain a foo identifier", false), + ) } @Test @@ -214,24 +182,6 @@ class SuppressionLocatorBuilderTest { assertThat(lint(code)).isEmpty() } - @Test - fun `Given an invalid rule id then ignore it without throwing an exception`() { - val code = - """ - @file:Suppress("ktlint:standard:SOME-INVALID-RULE-ID-1") - - @Suppress("ktlint:standard:SOME-INVALID-RULE-ID-2") - class Foo { - /* ktlint-disable standard:SOME-INVALID-RULE-ID-3 */ - fun foo() { - val fooNotReported = "foo" // ktlint-disable standard:SOME-INVALID-RULE-ID-4 - } - /* ktlint-enable standard:SOME-INVALID-RULE-ID-3 */ - } - """.trimIndent() - assertThat(lint(code)).isEmpty() - } - @Nested inner class `Given that formatter tags are enabled` { @Test @@ -329,6 +279,96 @@ class SuppressionLocatorBuilderTest { } } + @Nested + inner class `Given code that tries to disable to ktlint-suppression rule itself` { + @Test + fun `Given a @file annotation`() { + val code = + """ + @file:Suppress("ktlint:internal:ktlint-suppression") + """.trimIndent() + val actual = lint(code = code, ignoreKtlintSuppressionRule = false) + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") + assertThat(actual).containsExactly( + LintError(1, 24, KTLINT_SUPPRESSION_RULE_ID, "Ktlint rule with id 'ktlint:internal:ktlint-suppression' is unknown or not loaded", false), + ) + } + + @Test + fun `Given a block comment with a ktlint-disable directive`() { + val code = + """ + /* ktlint-disable internal:ktlint-suppression */ + """.trimIndent() + val actual = lint(code = code, ignoreKtlintSuppressionRule = false) + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") + assertThat(actual).containsExactly( + LintError(1, 4, KTLINT_SUPPRESSION_RULE_ID, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation", true), + LintError(1, 19, KTLINT_SUPPRESSION_RULE_ID, "Ktlint rule with id 'internal:ktlint-suppression' is unknown or not loaded", false), + ) + } + + @Test + fun `Given an EOL comment with a ktlint-disable directive which is ignored then emit the violation`() { + val code = + """ + val foo = "foo" // ktlint-disable internal:ktlint-suppression + """.trimIndent() + val actual = lint(code = code, ignoreKtlintSuppressionRule = false) + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") + assertThat(actual).containsExactly( + lintError(1, 5, "standard:no-foo-identifier-standard"), + lintError(1, 5, "$NON_STANDARD_RULE_SET_ID:no-foo-identifier"), + LintError(1, 20, KTLINT_SUPPRESSION_RULE_ID, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation", true), + LintError(1, 35, KTLINT_SUPPRESSION_RULE_ID, "Ktlint rule with id 'internal:ktlint-suppression' is unknown or not loaded", false), + ) + } + } + + @Test + fun `Given a suppression of a rule which alphabetically comes before rule id ktlint-suppression`() { + val code = + """ + fun bar() { + /* ktlint-disable standard:indent */ + return mapOf( + 1 to " 1 ms", + 10 to " 10 ms", + 999 to " 999 ms", + 1000 to " 1 sec", + ) + /* ktlint-enable standard:indent */ + } + """.trimIndent() + val formattedCode = + """ + @Suppress("ktlint:standard:indent") + fun bar() { + return mapOf( + 1 to " 1 ms", + 10 to " 10 ms", + 999 to " 999 ms", + 1000 to " 1 sec", + ) + } + """.trimIndent() + + val actual = + KtLintRuleEngine( + ruleProviders = + setOf( + RuleProvider { IndentationRule() }, + ), + editorConfigOverride = + EMPTY_EDITOR_CONFIG_OVERRIDE + .plus( + STANDARD_NO_FOO_IDENTIFIER_RULE_ID.createRuleExecutionEditorConfigProperty() to RuleExecution.enabled, + ), + ).format(Code.fromSnippet(code)) { _, _ -> } + + assertThat(actual).isEqualTo(formattedCode) + } + private class NoFooIdentifierRule(id: RuleId) : Rule( ruleId = id, about = About(), @@ -347,6 +387,8 @@ class SuppressionLocatorBuilderTest { private fun lint( code: String, editorConfigOverride: EditorConfigOverride = EditorConfigOverride.EMPTY_EDITOR_CONFIG_OVERRIDE, + ruleProviders: Set = emptySet(), + ignoreKtlintSuppressionRule: Boolean = true, ) = ArrayList().apply { KtLintRuleEngine( ruleProviders = @@ -355,21 +397,27 @@ class SuppressionLocatorBuilderTest { // ruleIds are different. RuleProvider { NoFooIdentifierRule(STANDARD_NO_FOO_IDENTIFIER_RULE_ID) }, RuleProvider { NoFooIdentifierRule(NON_STANDARD_NO_FOO_IDENTIFIER_RULE_ID) }, - ), + ).plus(ruleProviders), editorConfigOverride = editorConfigOverride .plus( STANDARD_NO_FOO_IDENTIFIER_RULE_ID.createRuleExecutionEditorConfigProperty() to RuleExecution.enabled, NON_STANDARD_NO_FOO_IDENTIFIER_RULE_ID.createRuleExecutionEditorConfigProperty() to RuleExecution.enabled, ), - ).lint(Code.fromSnippet(code)) { e -> add(e) } + ).lint(Code.fromSnippet(code)) { e -> + if (ignoreKtlintSuppressionRule && e.ruleId == KTLINT_SUPPRESSION_RULE_ID) { + // This class should be able to test code snippets containing the deprecated ktlint-directives + } else { + add(e) + } + } } private fun lintError( line: Int, - col: Int, + column: Int, ruleId: String, - ) = LintError(line, col, RuleId(ruleId), "Line should not contain a foo identifier", false) + ) = LintError(line, column, RuleId(ruleId), "Line should not contain a foo identifier", false) private companion object { val NON_STANDARD_RULE_SET_ID = "custom".also { require(it != RuleSetId.STANDARD.value) } diff --git a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/InternalRuleProvidersFilterTest.kt b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/InternalRuleProvidersFilterTest.kt new file mode 100644 index 0000000000..433fe7dce2 --- /dev/null +++ b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/InternalRuleProvidersFilterTest.kt @@ -0,0 +1,62 @@ +package com.pinterest.ktlint.rule.engine.internal.rulefilter + +import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine +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.rule.engine.core.api.RuleSetId +import com.pinterest.ktlint.rule.engine.internal.rules.KTLINT_SUPPRESSION_RULE_ID +import org.assertj.core.api.Assertions.assertThat +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.junit.jupiter.api.Test + +class InternalRuleProvidersFilterTest { + @Test + fun `Given a ktlint rule engine then add the ktlint suppression rule provider`() { + val ktLintRuleEngine = + KtLintRuleEngine( + ruleProviders = + setOf( + RuleProvider { + object : R(ruleId = STANDARD_RULE_A) {} + }, + ), + ) + val actual = + InternalRuleProvidersFilter(ktLintRuleEngine) + .filter(ktLintRuleEngine.ruleProviders) + .toRuleId() + + assertThat(actual).containsExactly( + STANDARD_RULE_A, + KTLINT_SUPPRESSION_RULE_ID, + ) + } + + private companion object { + const val RULE_A = "rule-a" + val STANDARD = RuleSetId.STANDARD.value + val STANDARD_RULE_A = RuleId("$STANDARD:$RULE_A") + } + + private open class R( + ruleId: RuleId, + visitorModifiers: Set = emptySet(), + ) : Rule( + ruleId = ruleId, + about = About(), + visitorModifiers, + ) { + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + throw UnsupportedOperationException( + "Rule should never be really invoked because that is not the aim of this unit test.", + ) + } + } + + private fun Set.toRuleId() = map { it.ruleId } +} diff --git a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RuleFilterKtTest.kt b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RuleFilterKtTest.kt index 7e8167f065..aaa5b32248 100644 --- a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RuleFilterKtTest.kt +++ b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RuleFilterKtTest.kt @@ -12,7 +12,7 @@ class RuleFilterKtTest { fun `Given an empty list of rule filters then the list of rule providers contains provider for all rule ids initially provided by the ktlint engine`() { val actual = createKtLintRuleEngine(arrayOf(RULE_SET_A_RULE_A, RULE_SET_A_RULE_B, RULE_SET_B_RULE_A, RULE_SET_B_RULE_B)) - .ruleProviders() + .applyRuleFilters() .map { it.ruleId } assertThat(actual).containsExactlyInAnyOrder(RULE_SET_A_RULE_A, RULE_SET_A_RULE_B, RULE_SET_B_RULE_A, RULE_SET_B_RULE_B) @@ -22,7 +22,7 @@ class RuleFilterKtTest { fun `Given a single rule filter then the list of rule providers contains only rule ids that match that filter`() { val actual = createKtLintRuleEngine(arrayOf(RULE_SET_A_RULE_A, RULE_SET_A_RULE_B, RULE_SET_B_RULE_A, RULE_SET_B_RULE_B)) - .ruleProviders(RuleIdRuleFilter(RULE_SET_A)) + .applyRuleFilters(RuleIdRuleFilter(RULE_SET_A)) .map { it.ruleId } assertThat(actual).containsExactlyInAnyOrder(RULE_SET_A_RULE_A, RULE_SET_A_RULE_B) @@ -32,7 +32,7 @@ class RuleFilterKtTest { fun `Given multiple rule filters then the list of rule providers contains only rule ids that match all filters`() { val actual = createKtLintRuleEngine(arrayOf(RULE_SET_A_RULE_A, RULE_SET_A_RULE_B, RULE_SET_B_RULE_B)) - .ruleProviders( + .applyRuleFilters( RuleIdRuleFilter(RULE_SET_A), RuleIdRuleFilter(RULE_B), ).map { it.ruleId } @@ -44,7 +44,7 @@ class RuleFilterKtTest { fun `Given multiple rule filters that exclude each other then the list of rule providers is empty`() { val actual = createKtLintRuleEngine(arrayOf(RULE_SET_A_RULE_A)) - .ruleProviders( + .applyRuleFilters( RuleIdRuleFilter(RULE_A), RuleIdRuleFilter(RULE_SET_B), ).map { it.ruleId } diff --git a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/rules/KtlintSuppressionRuleTest.kt b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/rules/KtlintSuppressionRuleTest.kt new file mode 100644 index 0000000000..4ee2aa4163 --- /dev/null +++ b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/rules/KtlintSuppressionRuleTest.kt @@ -0,0 +1,1329 @@ +package com.pinterest.ktlint.rule.engine.internal.rules + +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.ArgumentListWrappingRule +import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule +import com.pinterest.ktlint.test.KtlintDocumentationTest +import com.pinterest.ktlint.test.LintViolation +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.junit.jupiter.params.provider.ValueSource + +class KtlintSuppressionRuleTest { + private val ktlintSuppressionRuleAssertThat = + assertThatRule( + provider = { KtlintSuppressionRule(emptyList()) }, + additionalRuleProviders = + setOf( + // Create a dummy rule for each rule id that is used in a ktlint directive or suppression in the tests in this + // class. If no rule provider is added for the rule id, a lint violation is thrown which will bloat the tests too + // much. + // + // Ids of real rules used but for which the real implementation is unwanted as it would modify the formatted code + RuleProvider { DummyRule("standard:no-wildcard-imports") }, + RuleProvider { DummyRule("standard:no-multi-spaces") }, + RuleProvider { DummyRule("standard:max-line-length") }, + RuleProvider { DummyRule("standard:package-name") }, + // Ids of fake rules in a custom and the standard rule set + RuleProvider { DummyRule("custom:foo") }, + RuleProvider { DummyRule("standard:bar") }, + RuleProvider { DummyRule("standard:foo") }, + ), + ) + + @Nested + inner class `Given a suppression annotation missing the rule set id prefix` { + @Test + fun `Given a @file Suppress annotation`() { + val code = + """ + @file:Suppress("ktlint:bar", "ktlint:standard:foo", "ktlint:custom:foo") + """.trimIndent() + val formattedCode = + """ + @file:Suppress("ktlint:standard:bar", "ktlint:standard:foo", "ktlint:custom:foo") + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(1, 24, "Identifier to suppress ktlint rule must be fully qualified with the rule set id") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a @file SuppressWarnings annotation`() { + val code = + """ + @file:SuppressWarnings("ktlint:bar", "ktlint:standard:foo", "ktlint:custom:foo") + """.trimIndent() + val formattedCode = + """ + @file:SuppressWarnings("ktlint:standard:bar", "ktlint:standard:foo", "ktlint:custom:foo") + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(1, 32, "Identifier to suppress ktlint rule must be fully qualified with the rule set id") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a @file array annotation with Suppress and SuppressWarnings annotations`() { + val code = + """ + @file:[Suppress("ktlint:bar", "ktlint:custom:foo") SuppressWarnings("ktlint:foo")] + """.trimIndent() + val formattedCode = + """ + @file:[Suppress("ktlint:standard:bar", "ktlint:custom:foo") SuppressWarnings("ktlint:standard:foo")] + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(1, 25, "Identifier to suppress ktlint rule must be fully qualified with the rule set id"), + LintViolation(1, 77, "Identifier to suppress ktlint rule must be fully qualified with the rule set id"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given an array annotation with Suppress and SuppressWarnings annotations`() { + val code = + """ + @[Suppress("ktlint:bar", "ktlint:custom:foo") SuppressWarnings("ktlint:foo")] + val foo = "foo" + """.trimIndent() + val formattedCode = + """ + @[Suppress("ktlint:standard:bar", "ktlint:custom:foo") SuppressWarnings("ktlint:standard:foo")] + val foo = "foo" + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(1, 20, "Identifier to suppress ktlint rule must be fully qualified with the rule set id"), + LintViolation(1, 72, "Identifier to suppress ktlint rule must be fully qualified with the rule set id"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given a Suppress annotation with a named argument with an arrayOf initialization`() { + val code = + """ + @Suppress(names = arrayOf("ktlint:bar", "ktlint:standard:foo", "ktlint:custom:foo")) + val foo = "foo" + """.trimIndent() + val formattedCode = + """ + @Suppress(names = arrayOf("ktlint:standard:bar", "ktlint:standard:foo", "ktlint:custom:foo")) + val foo = "foo" + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(1, 35, "Identifier to suppress ktlint rule must be fully qualified with the rule set id") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a Suppress annotation with a named argument with an array (squared brackets) initialization`() { + val code = + """ + @Suppress(names = ["ktlint:bar", "ktlint:standard:foo", "ktlint:custom:foo"]) + val foo = "foo" + """.trimIndent() + val formattedCode = + """ + @Suppress(names = ["ktlint:standard:bar", "ktlint:standard:foo", "ktlint:custom:foo"]) + val foo = "foo" + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(1, 28, "Identifier to suppress ktlint rule must be fully qualified with the rule set id") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a Suppress annotation on a declaration`() { + val code = + """ + @Suppress("ktlint:bar", "ktlint:standard:foo", "ktlint:custom:foo") + val foo = "foo" + """.trimIndent() + val formattedCode = + """ + @Suppress("ktlint:standard:bar", "ktlint:standard:foo", "ktlint:custom:foo") + val foo = "foo" + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(1, 19, "Identifier to suppress ktlint rule must be fully qualified with the rule set id") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a SuppressWarnings annotation on a declaration`() { + val code = + """ + @SuppressWarnings("ktlint:bar", "ktlint:standard:foo", "ktlint:custom:foo") + val foo = "foo" + """.trimIndent() + val formattedCode = + """ + @SuppressWarnings("ktlint:standard:bar", "ktlint:standard:foo", "ktlint:custom:foo") + val foo = "foo" + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(1, 27, "Identifier to suppress ktlint rule must be fully qualified with the rule set id") + .isFormattedAs(formattedCode) + } + } + + @Test + fun `Given an EOL comment with a ktlint-disable directive not preceded by code leaf on same line`() { + val code = + """ + val foo = "foo" + // ktlint-disable + val bar = "bar" + """.trimIndent() + val formattedCode = + """ + val foo = "foo" + val bar = "bar" + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(2, 4, "Directive 'ktlint-disable' in EOL comment is ignored as it is not preceded by a code element") + .isFormattedAs(formattedCode) + } + + @Nested + inner class `Given an import statement` { + @Test + fun `Given an EOL comment with a ktlint-disable directive on an import`() { + val code = + """ + import foo.bar + import foobar.* // ktlint-disable no-wildcard-imports + """.trimIndent() + val formattedCode = + """ + @file:Suppress("ktlint:standard:no-wildcard-imports") + + import foo.bar + import foobar.* + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(2, 20, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given an EOL comment with a ktlint-disable directive on an import and an existing @file Suppress annotation`() { + val code = + """ + @file:Suppress("aaa", "zzz") + + import foo.bar + import foobar.* // ktlint-disable no-wildcard-imports + """.trimIndent() + val formattedCode = + """ + @file:Suppress("aaa", "ktlint:standard:no-wildcard-imports", "zzz") + + import foo.bar + import foobar.* + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(4, 20, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given an EOL comment with a ktlint-disable directive on an import and an existing @file Suppress annotation without parameters`() { + val code = + """ + @file:Suppress + + import foobar.* // ktlint-disable no-wildcard-imports + """.trimIndent() + val formattedCode = + """ + @file:Suppress("ktlint:standard:no-wildcard-imports") + + import foobar.* + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(3, 20, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given an EOL comment with a ktlint-disable directive on an import on a file starting with a (copyright) comment before the package statement`() { + val code = + """ + /* Some copyright notice before package statement */ + package foo + + import foo.bar + import foobar.* // ktlint-disable no-wildcard-imports + """.trimIndent() + val formattedCode = + """ + /* Some copyright notice before package statement */ + @file:Suppress("ktlint:standard:no-wildcard-imports") + + package foo + + import foo.bar + import foobar.* + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(5, 20, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given an EOL comment with a ktlint-disable directive on an import and an existing @file Suppress annotation on a file starting with a (copyright) comment`() { + val code = + """ + /* Some copyright notice before package statement */ + @file:Suppress("aaa", "zzz") + package foo + + import foo.bar + import foobar.* // ktlint-disable no-wildcard-imports + """.trimIndent() + val formattedCode = + """ + /* Some copyright notice before package statement */ + @file:Suppress("aaa", "ktlint:standard:no-wildcard-imports", "zzz") + package foo + + import foo.bar + import foobar.* + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(6, 20, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + } + + @Nested + inner class `Given an EOL comment with a ktlint-disable directive` { + @Test + fun `Given a ktlint-disable directive without rule-id`() { + val code = + """ + val foo = "foo" // ktlint-disable + """.trimIndent() + val formattedCode = + """ + @Suppress("ktlint") + val foo = "foo" + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(1, 20, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a ktlint-disable directive with rule-id not prefixed with a rule set id`() { + val code = + """ + val foo = "foo" // ktlint-disable foo + """.trimIndent() + val formattedCode = + """ + @Suppress("ktlint:standard:foo") + val foo = "foo" + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(1, 20, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a ktlint-disable directive with rule-id prefixed with a rule set id`() { + val code = + """ + val foo = "foo" // ktlint-disable standard:foo + """.trimIndent() + val formattedCode = + """ + @Suppress("ktlint:standard:foo") + val foo = "foo" + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(1, 20, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + + @ParameterizedTest(name = "Rules: {0}") + @ValueSource( + strings = [ + // Rule ids are already sorted + "custom:foo standard:bar standard:foo", + // Redundant spaces between rule ids should not lead to suppressing all rules by adding "ktlint" as suppression id + "custom:foo standard:bar standard:foo", + // Duplicate rule ids are ignored + "standard:bar standard:foo standard:bar custom:foo", + // Unsorted rule ids are sorted + "standard:foo custom:foo standard:bar", + ], + ) + fun `Given a ktlint-disable directive with multiple rule-ids`(ruleIds: String) { + val code = + """ + val foo = "foo" // ktlint-disable $ruleIds + """.trimIndent() + val formattedCode = + """ + @Suppress("ktlint:custom:foo", "ktlint:standard:bar", "ktlint:standard:foo") + val foo = "foo" + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(1, 20, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a ktlint-disable directive for which the target element is already annotated with @Suppress then add the ktlint suppression and sort all suppressions alphabetically`() { + val code = + """ + @Suppress("zzz", "aaa") + val foo = "foo" // ktlint-disable standard:foo + """.trimIndent() + val formattedCode = + """ + @Suppress("aaa", "ktlint:standard:foo", "zzz") + val foo = "foo" + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(2, 20, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a ktlint-disable directive for which the target element is already annotated with @SuppressWarnings then add the ktlint suppression and sort all suppressions alphabetically`() { + val code = + """ + @SuppressWarnings("aaa", "zzz") + val foo = "foo" // ktlint-disable standard:foo + """.trimIndent() + val formattedCode = + """ + @SuppressWarnings("aaa", "ktlint:standard:foo", "zzz") + val foo = "foo" + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(2, 20, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a ktlint-disable directive for which the target element is already annotated with both @Suppress and @SuppressWarnings then add the ktlint suppression to the @Suppress`() { + val code = + """ + @Suppress("aaa", "zzz") + @SuppressWarnings("bbb", "yyy") + val foo = "foo" // ktlint-disable standard:foo + """.trimIndent() + val formattedCode = + """ + @Suppress("aaa", "ktlint:standard:foo", "zzz") + @SuppressWarnings("bbb", "yyy") + val foo = "foo" + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(3, 20, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + } + + // Note that the embedded kotlin compiler treats the last comment preceding a declaration or expression as integral part of the ASTNode + // representing that declaration or expression. To match the working SuppressionBuilderLocator of Ktlint it should be treated as a top + // level comment. + // To keep test cases below concise, they only contain a ktlint-disable directive in a block comment and no declaration or expression. + @Nested + inner class `Given a top level block comment with a ktlint-disable directive` { + @Test + fun `Given a ktlint-disable directive without rule-id`() { + val code = + """ + /* ktlint-disable */ + """.trimIndent() + val formattedCode = + """ + @file:Suppress("ktlint") + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(1, 4, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + + @ParameterizedTest(name = "Ktlint-disable: {0}") + @CsvSource( + quoteCharacter = '"', + delimiter = '|', + value = [ + "foo | ktlint:standard:foo", + "standard:foo | ktlint:standard:foo", + "custom:foo | ktlint:custom:foo", + "custom:foo standard:bar | ktlint:custom:foo,ktlint:standard:bar", + // Redundant spaces between rule ids should not lead to suppressing all rules by adding "ktlint" as suppression id + "custom:foo standard:bar | ktlint:custom:foo,ktlint:standard:bar", + // Duplicate rule ids are ignored + "custom:foo standard:bar custom:foo | ktlint:custom:foo,ktlint:standard:bar", + // Unsorted rule ids are sorted + "standard:bar custom:foo | ktlint:custom:foo,ktlint:standard:bar", + ], + ) + fun `Given a top-level ktlint-disable directive`( + ruleIds: String, + expectedSuppressionIdString: String, + ) { + val code = + """ + /* ktlint-disable $ruleIds */ + """.trimIndent() + val formattedCode = + """ + @file:Suppress(${ + expectedSuppressionIdString + .split(',') + .joinToString { "\"$it\"" } + }) + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(1, 4, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a ktlint-disable directive for which the target element is already annotated with @Suppress then add the ktlint suppression and sort all suppressions alphabetically`() { + val code = + """ + @file:Suppress("zzz", "aaa") + /* ktlint-disable standard:foo */ + """.trimIndent() + val formattedCode = + """ + @file:Suppress("aaa", "ktlint:standard:foo", "zzz") + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(2, 4, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a ktlint-disable directive for which the target element is already annotated with @SuppressWarnings then add the ktlint suppression and sort all suppressions alphabetically`() { + val code = + """ + @file:SuppressWarnings("aaa", "zzz") + /* ktlint-disable standard:foo */ + """.trimIndent() + val formattedCode = + """ + @file:SuppressWarnings("aaa", "ktlint:standard:foo", "zzz") + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(2, 4, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a ktlint-disable directive for which the target element is already annotated with both @Suppress and @SuppressWarnings then add the ktlint suppression to the @Suppress`() { + val code = + """ + @file:Suppress("aaa", "zzz") + @file:SuppressWarnings("bbb", "yyy") + + /* ktlint-disable standard:foo */ + """.trimIndent() + val formattedCode = + """ + @file:Suppress("aaa", "ktlint:standard:foo", "zzz") + @file:SuppressWarnings("bbb", "yyy") + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(4, 4, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a ktlint-disable directive for which the target element is already annotated with both @SuppressWarnings and @Suppress then add the ktlint suppression to the @Suppress`() { + val code = + """ + @file:SuppressWarnings("bbb", "yyy") + @file:Suppress("aaa", "zzz") + + /* ktlint-disable standard:foo */ + """.trimIndent() + val formattedCode = + """ + @file:SuppressWarnings("bbb", "yyy") + @file:Suppress("aaa", "ktlint:standard:foo", "zzz") + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(4, 4, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + } + + @Nested + inner class `Given a pair of matching ktlint directives in block comments within the same parent node` { + @Test + fun `Given a ktlint-disable directive without rule-id`() { + val code = + """ + fun foo() { + /* ktlint-disable */ + doSomething() + /* ktlint-enable */ + } + """.trimIndent() + val formattedCode = + """ + @Suppress("ktlint") + fun foo() { + doSomething() + } + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(2, 8, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(4, 8, "Directive 'ktlint-enable' is obsolete after migrating to suppress annotations"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given a ktlint-disable directive with rule-id not prefixed with a rule set id`() { + val code = + """ + fun foo() { + /* ktlint-disable foo */ + doSomething() + /* ktlint-enable foo */ + } + """.trimIndent() + val formattedCode = + """ + @Suppress("ktlint:standard:foo") + fun foo() { + doSomething() + } + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(2, 8, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(4, 8, "Directive 'ktlint-enable' is obsolete after migrating to suppress annotations"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given a ktlint-disable directive with rule-id prefixed with a rule set id`() { + val code = + """ + fun foo() { + /* ktlint-disable standard:foo */ + doSomething() + /* ktlint-enable standard:foo */ + } + """.trimIndent() + val formattedCode = + """ + @Suppress("ktlint:standard:foo") + fun foo() { + doSomething() + } + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(2, 8, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(4, 8, "Directive 'ktlint-enable' is obsolete after migrating to suppress annotations"), + ).isFormattedAs(formattedCode) + } + + @ParameterizedTest(name = "Rules: {0}") + @ValueSource( + strings = [ + "standard:bar standard:foo", + // Redundant spaces between rule ids should not lead to suppressing all rules by adding "ktlint" as suppression id + "standard:bar standard:foo", + // Duplicate rule ids are ignored + "standard:bar standard:foo standard:bar", + // Unsorted rule ids are sorted + "standard:foo standard:bar", + ], + ) + fun `Given a ktlint-disable directive with multiple rule-ids`(ruleIds: String) { + val code = + """ + fun foo() { + /* ktlint-disable $ruleIds */ + doSomething() + /* ktlint-enable $ruleIds */ + } + """.trimIndent() + val formattedCode = + """ + @Suppress("ktlint:standard:bar", "ktlint:standard:foo") + fun foo() { + doSomething() + } + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(2, 8, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(4, 8, "Directive 'ktlint-enable' is obsolete after migrating to suppress annotations"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given a ktlint-disable directive for which the target element is already annotated with @Suppress then add the ktlint suppression and sort all suppressions alphabetically`() { + val code = + """ + @Suppress("zzz", "aaa") + fun foo() { + /* ktlint-disable standard:foo */ + doSomething() + /* ktlint-enable standard:foo */ + } + """.trimIndent() + val formattedCode = + """ + @Suppress("aaa", "ktlint:standard:foo", "zzz") + fun foo() { + doSomething() + } + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(3, 8, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(5, 8, "Directive 'ktlint-enable' is obsolete after migrating to suppress annotations"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given a ktlint-disable directive for which the target element is already annotated with @SuppressWarnings then add the ktlint suppression and sort all suppressions alphabetically`() { + val code = + """ + @SuppressWarnings("aaa", "zzz") + fun foo() { + /* ktlint-disable standard:foo */ + doSomething() + /* ktlint-enable standard:foo */ + } + """.trimIndent() + val formattedCode = + """ + @SuppressWarnings("aaa", "ktlint:standard:foo", "zzz") + fun foo() { + doSomething() + } + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(3, 8, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(5, 8, "Directive 'ktlint-enable' is obsolete after migrating to suppress annotations"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given a ktlint-disable directive for which the target element is already annotated with both @Suppress and @SuppressWarnings then add the ktlint suppression to the @Suppress`() { + val code = + """ + @Suppress("aaa", "zzz") + @SuppressWarnings("bbb", "yyy") + fun foo() { + /* ktlint-disable standard:foo */ + doSomething() + /* ktlint-enable standard:foo */ + } + """.trimIndent() + val formattedCode = + """ + @Suppress("aaa", "ktlint:standard:foo", "zzz") + @SuppressWarnings("bbb", "yyy") + fun foo() { + doSomething() + } + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(4, 8, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(6, 8, "Directive 'ktlint-enable' is obsolete after migrating to suppress annotations"), + ).isFormattedAs(formattedCode) + } + } + + @Nested + inner class `Given a ktlint-enable directive` { + @Test + fun `Given a ktlint-enable directive matching with a ktlint-disable directive`() { + val code = + """ + fun foo() { + /* ktlint-disable standard:foo */ + doSomething() + /* ktlint-enable standard:foo */ + } + """.trimIndent() + val formattedCode = + """ + @Suppress("ktlint:standard:foo") + fun foo() { + doSomething() + } + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(2, 8, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(4, 8, "Directive 'ktlint-enable' is obsolete after migrating to suppress annotations"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given a ktlint-enable directive not matching with a ktlint-disable directive`() { + val code = + """ + fun foo() { + doSomething() + /* ktlint-enable standard:foo */ + } + """.trimIndent() + val formattedCode = + """ + fun foo() { + doSomething() + } + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(3, 8, "Directive 'ktlint-enable' is obsolete after migrating to suppress annotations") + .isFormattedAs(formattedCode) + } + } + + @Test + fun `Given a ktlint-disable directive for a specific rule on a declaration which already has suppression annotation for all ktlint rules`() { + val code = + """ + @Suppress("ktlint") + fun foo() { + bar() // ktlint-disable standard:bar + + /* ktlint-disable standard:foo */ + /* ktlint-disable custom:foo */ + bar() + /* ktlint-enable custom:foo */ + /* ktlint-enable standard:foo */ + } + """.trimIndent() + val formattedCode = + """ + @Suppress("ktlint") + fun foo() { + bar() + + bar() + } + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(3, 14, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(5, 8, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(6, 8, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(8, 8, "Directive 'ktlint-enable' is obsolete after migrating to suppress annotations"), + LintViolation(9, 8, "Directive 'ktlint-enable' is obsolete after migrating to suppress annotations"), + ).isFormattedAs(formattedCode) + } + + @KtlintDocumentationTest + fun `Documentation example`() { + val code = + """ + /* ktlint-disable standard:no-wildcard-imports */ + + class FooBar { + val foo = "some longggggggggggggggggggg text" // ktlint-disable standard:max-line-length + + fun bar() = + listOf( + /* ktlint-disable standard:no-multi-spaces */ + "1 One", + "10 Ten", + "100 Hundred", + /* ktlint-enable standard:no-multi-spaces */ + ) + } + """.trimIndent() + val formattedCode = + """ + @file:Suppress("ktlint:standard:no-wildcard-imports") + + class FooBar { + @Suppress("ktlint:standard:max-line-length") + val foo = "some longggggggggggggggggggg text" + + fun bar() = + @Suppress("ktlint:standard:no-multi-spaces") + listOf( + "1 One", + "10 Ten", + "100 Hundred", + ) + } + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .addAdditionalRuleProvider { ArgumentListWrappingRule() } + .hasLintViolations( + LintViolation(1, 4, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(4, 54, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(8, 16, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(12, 16, "Directive 'ktlint-enable' is obsolete after migrating to suppress annotations"), + ).isFormattedAs(formattedCode) + } + + @ParameterizedTest(name = "Suppression type: {0}") + @ValueSource( + strings = [ + "Suppress", + "SuppressWarnings", + ], + ) + fun `Given multiple ktlint-disable directives which have to merged into an existing @file Suppress annotation`(annotationName: String) { + val code = + """ + @file:$annotationName("ktlint:standard:bar") + + import bar // ktlint-disable standard:no-wildcard-imports + + /* ktlint-disable standard:foo */ + + val someFoo = foo.TEST + val someBar = bar.TEST + + /* ktlint-disable custom:foo */ + """.trimIndent() + val formattedCode = + """ + @file:$annotationName("ktlint:custom:foo", "ktlint:standard:bar", "ktlint:standard:foo", "ktlint:standard:no-wildcard-imports") + + import bar + + val someFoo = foo.TEST + val someBar = bar.TEST + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(3, 15, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(5, 4, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(10, 4, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given a block comment containing a ktlint-disable directive inside an init block`() { + val code = + """ + class Foo() { + var foo: String + var bar: String + + init { + /* ktlint-disable standard:foo */ + foo = "foo" + /* ktlint-enable standard:foo */ + } + + init { // ktlint-disable standard:bar + bar = "bar" + } + } + """.trimIndent() + val formattedCode = + """ + @Suppress("ktlint:standard:bar", "ktlint:standard:foo") + class Foo() { + var foo: String + var bar: String + + init { + foo = "foo" + } + + init { + bar = "bar" + } + } + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(6, 12, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(8, 12, "Directive 'ktlint-enable' is obsolete after migrating to suppress annotations"), + LintViolation(11, 15, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given a pair of matching ktlint directives in block comments as siblings in same parent node`() { + val code = + """ + fun foobar( + /* ktlint-disable standard:foo */ + foo: Int, + bar: Int, + /* ktlint-enable standard:foo */ + ) {} + """.trimIndent() + val formattedCode = + """ + @Suppress("ktlint:standard:foo") + fun foobar( + foo: Int, + bar: Int, + ) {} + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(2, 8, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(5, 8, "Directive 'ktlint-enable' is obsolete after migrating to suppress annotations"), + ).isFormattedAs(formattedCode) + } + + @Nested + inner class `Given ktlint-disable directive in block comment not having a ktlint-enable directive in a sibling in the same parent node` { + @Test + fun `G1iven a ktlint-disable directive root level not related to an declaration or expression then move to @file annotation`() { + val code = + """ + /* ktlint-disable standard:foo */ + """.trimIndent() + val formattedCode = + """ + @file:Suppress("ktlint:standard:foo") + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(1, 4, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given ktlint-disable directive in last block comment before class but not having a ktlint-enable directive`() { + val code = + """ + /* ktlint-disable standard:foo */ + class Foo + """.trimIndent() + val formattedCode = + """ + @file:Suppress("ktlint:standard:foo") + + class Foo + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(1, 4, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given ktlint-disable directive in last block comment before property but not having a ktlint-enable directive`() { + val code = + """ + /* ktlint-disable standard:foo */ + val foo = "foo" + """.trimIndent() + val formattedCode = + """ + @file:Suppress("ktlint:standard:foo") + + val foo = "foo" + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(1, 4, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a pair of matching ktlint directives in block comments but not as siblings in same parent node`() { + val code = + """ + fun foobar( + /* ktlint-disable standard:foo */ + foo: Int, + bar: Int, + ) { + /* ktlint-enable standard:foo */ + doSomething() + } + """.trimIndent() + val formattedCode = + """ + fun foobar( + /* ktlint-disable standard:foo */ + foo: Int, + bar: Int, + ) { + doSomething() + } + """.trimIndent() + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(2, 8, "Directive 'ktlint-disable' is deprecated. The matching 'ktlint-enable' directive is not found in same scope. Replace with @Suppress annotation", false), + LintViolation(6, 8, "Directive 'ktlint-enable' is obsolete after migrating to suppress annotations"), + ).isFormattedAs(formattedCode) + } + } + + @Test + fun `Given ktlint-disable directive on a package statement`() { + val code = + """ + package foo.foo_bar // ktlint-disable standard:package-name + """.trimIndent() + val formattedCode = + """ + @file:Suppress("ktlint:standard:package-name") + + package foo.foo_bar + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(1, 24, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given an invalid rule id then ignore it without throwing an exception`() { + val code = + """ + @file:Suppress("ktlint:standard:SOME-INVALID-RULE-ID-1") + + @Suppress("ktlint:standard:SOME-INVALID-RULE-ID-2") + class Foo { + /* ktlint-disable standard:SOME-INVALID-RULE-ID-3 */ + fun bar() { + val bar = "bar" // ktlint-disable standard:SOME-INVALID-RULE-ID-4 + } + /* ktlint-enable standard:SOME-INVALID-RULE-ID-3 */ + } + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(1, 24, "Ktlint rule with id 'ktlint:standard:SOME-INVALID-RULE-ID-1' is unknown or not loaded", false), + LintViolation(3, 19, "Ktlint rule with id 'ktlint:standard:SOME-INVALID-RULE-ID-2' is unknown or not loaded", false), + LintViolation(5, 8, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(5, 23, "Ktlint rule with id 'standard:SOME-INVALID-RULE-ID-3' is unknown or not loaded", false), + LintViolation(7, 28, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(7, 43, "Ktlint rule with id 'standard:SOME-INVALID-RULE-ID-4' is unknown or not loaded", false), + LintViolation(9, 8, "Directive 'ktlint-enable' is obsolete after migrating to suppress annotations"), + ) + } + + @Test + fun `Given an unknown ktlint rule id then do not create an empty @Suppress annotation`() { + val code = + """ + val foo = "foo" // ktlint-disable standard:unknown-rule-id + """.trimIndent() + val formattedCode = + """ + val foo = "foo" + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(1, 20, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(1, 35, "Ktlint rule with id 'standard:unknown-rule-id' is unknown or not loaded", false), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given a setter with multiple ktlint directives`() { + val code = + """ + class Foo { + var foo: Int = 1 + set(value) { // ktlint-disable standard:foo + field = value // ktlint-disable standard:bar + field = value + } + } + """.trimIndent() + val formattedCode = + """ + class Foo { + var foo: Int = 1 + @Suppress("ktlint:standard:bar", "ktlint:standard:foo") + set(value) { + field = value + field = value + } + } + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(3, 25, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(4, 30, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given a primary constructor with multiple ktlint directives`() { + val code = + """ + class Foo constructor(bar: Bar) { + /* ktlint-disable standard:bar standard:foo */ + + /* ktlint-enable standard:bar standard:foo */ + } + """.trimIndent() + val formattedCode = + """ + @Suppress("ktlint:standard:bar", "ktlint:standard:foo") + class Foo constructor(bar: Bar) { + } + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(2, 8, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(4, 8, "Directive 'ktlint-enable' is obsolete after migrating to suppress annotations"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given a class parameter with multiple ktlint directives`() { + val code = + """ + class Foo( + /* ktlint-disable standard:bar standard:foo */ + val bar: Bar + /* ktlint-enable standard:bar standard:foo */ + ) + """.trimIndent() + val formattedCode = + """ + @Suppress("ktlint:standard:bar", "ktlint:standard:foo") + class Foo( + val bar: Bar + ) + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(2, 8, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(4, 8, "Directive 'ktlint-enable' is obsolete after migrating to suppress annotations"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given a ktlint-disable block directive around a single declaration then place the @Suppress on the declaration`() { + val code = + """ + class Foobar { + val bar = "bar" + + /* ktlint-disable standard:bar standard:foo */ + fun foo() {} + /* ktlint-enable standard:bar standard:foo */ + } + """.trimIndent() + val formattedCode = + """ + class Foobar { + val bar = "bar" + + @Suppress("ktlint:standard:bar", "ktlint:standard:foo") + fun foo() {} + } + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(4, 8, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(6, 8, "Directive 'ktlint-enable' is obsolete after migrating to suppress annotations"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given a ktlint-disable block directive around multiple declarations then place the @Suppress on the declaration`() { + val code = + """ + class Foobar { + /* ktlint-disable standard:bar standard:foo */ + val bar = "bar" + + fun foo() {} + /* ktlint-enable standard:bar standard:foo */ + } + """.trimIndent() + val formattedCode = + """ + @Suppress("ktlint:standard:bar", "ktlint:standard:foo") + class Foobar { + val bar = "bar" + + fun foo() {} + } + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolations( + LintViolation(2, 8, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation"), + LintViolation(6, 8, "Directive 'ktlint-enable' is obsolete after migrating to suppress annotations"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given a declaration with a @Suppress annotation using a named argument and a ktlint-disable directive`() { + val code = + """ + @Suppress(names = ["unused"]) + val foo = "foo" // ktlint-disable standard:foo + """.trimIndent() + val formattedCode = + """ + @Suppress("ktlint:standard:foo", "unused") + val foo = "foo" + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(2, 20, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a property delegate with a ktlint-disable directive`() { + val code = + """ + val foo by lazy(LazyThreadSafetyMode.PUBLICATION) { // ktlint-disable standard:foo + // do something + } + """.trimIndent() + val formattedCode = + """ + val foo by @Suppress("ktlint:standard:foo") + lazy(LazyThreadSafetyMode.PUBLICATION) { + // do something + } + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(1, 56, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a nested expression with a ktlint-disable directive`() { + val code = + """ + val foo = + setOf("a") + .map { + bar(it) // ktlint-disable standard:foo + } + """.trimIndent() + val formattedCode = + """ + val foo = + @Suppress("ktlint:standard:foo") + setOf("a") + .map { + bar(it) + } + """.trimIndent() + ktlintSuppressionRuleAssertThat(code) + .hasLintViolation(4, 24, "Directive 'ktlint-disable' is deprecated. Replace with @Suppress annotation") + .isFormattedAs(formattedCode) + } +} + +private class DummyRule(id: String) : Rule(RuleId(id), About()) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MaxLineLengthRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MaxLineLengthRule.kt index 25fd1092e9..6474c7226a 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MaxLineLengthRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MaxLineLengthRule.kt @@ -79,23 +79,18 @@ public class MaxLineLengthRule : // fixme: // normally we would emit here but due to API limitations we need to hold off until // node spanning the same offset is 'visit'ed - // (for ktlint-disable directive to have effect (when applied)) // this will be rectified in the upcoming release(s) errorOffset.add(parsedLine.offset) } } else { - // Allow ktlint-disable comments to exceed max line length - if (!el.text.startsWith("// ktlint-disable")) { - // if comment is the only thing on the line - fine, otherwise emit an error - val prevLeaf = el.prevCodeSibling() - if (prevLeaf != null && prevLeaf.startOffset >= parsedLine.offset) { - // fixme: - // normally we would emit here but due to API limitations we need to hold off until - // node spanning the same offset is 'visit'ed - // (for ktlint-disable directive to have effect (when applied)) - // this will be rectified in the upcoming release(s) - errorOffset.add(parsedLine.offset) - } + // if comment is the only thing on the line - fine, otherwise emit an error + val prevLeaf = el.prevCodeSibling() + if (prevLeaf != null && prevLeaf.startOffset >= parsedLine.offset) { + // fixme: + // normally we would emit here but due to API limitations we need to hold off until + // node spanning the same offset is 'visit'ed + // this will be rectified in the upcoming release(s) + errorOffset.add(parsedLine.offset) } } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRule.kt index 08b99363bc..e51babdebf 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRule.kt @@ -47,7 +47,6 @@ public class NoSingleLineBlockCommentRule : ?: node.lastChildLeafOrSelf() if (!node.textContains('\n') && - !node.isKtlintSuppressionDirective() && afterBlockComment.nextLeaf().isWhitespaceWithNewlineOrNull() ) { emit(node.startOffset, "Replace the block comment with an EOL comment", true) @@ -66,16 +65,6 @@ public class NoSingleLineBlockCommentRule : } private fun ASTNode?.isWhitespaceWithNewlineOrNull() = this == null || this.isWhiteSpaceWithNewline() - - // TODO: Remove when ktlint suppression directive in comments are no longer supported - private fun ASTNode?.isKtlintSuppressionDirective() = - this - ?.text - ?.removePrefix("/*") - ?.removeSuffix("*/") - ?.trim() - ?.let { it.startsWith("ktlint-enable") || it.startsWith("ktlint-disable") } - ?: false } public val NO_SINGLE_LINE_BLOCK_COMMENT_RULE_ID: RuleId = NoSingleLineBlockCommentRule().ruleId diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/AnnotationRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/AnnotationRuleTest.kt index f11f15aa17..d1dd929b5b 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/AnnotationRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/AnnotationRuleTest.kt @@ -253,7 +253,7 @@ class AnnotationRuleTest { val bar: Any } """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") annotationRuleAssertThat(code) .hasLintViolation(3, 9, "Expected newline after last annotation") .isFormattedAs(formattedCode) @@ -406,7 +406,7 @@ class AnnotationRuleTest { package foo.bar """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") annotationRuleAssertThat(code) .hasLintViolations( LintViolation(1, 26, "Expected newline after last annotation"), @@ -740,7 +740,7 @@ class AnnotationRuleTest { String > = FooBar() """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") annotationRuleAssertThat(code) .addAdditionalRuleProvider { IndentationRule() } .addAdditionalRuleProvider { WrappingRule() } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ArgumentListWrappingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ArgumentListWrappingRuleTest.kt index 3309c6fdd4..0f92d26ee0 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ArgumentListWrappingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ArgumentListWrappingRuleTest.kt @@ -29,7 +29,7 @@ class ArgumentListWrappingRuleTest { c ) """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") argumentListWrappingRuleAssertThat(code) .hasLintViolation(3, 8, "Argument should be on a separate line (unless all arguments can fit a single line)") .isFormattedAs(formattedCode) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ClassNamingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ClassNamingRuleTest.kt index ceef40e56f..16c79dce52 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ClassNamingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ClassNamingRuleTest.kt @@ -33,7 +33,7 @@ class ClassNamingRuleTest { """ class $className """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") classNamingRuleAssertThat(code) .hasLintViolationWithoutAutoCorrect(1, 7, "Class or object name should start with an uppercase letter and use camel case") } @@ -46,7 +46,7 @@ class ClassNamingRuleTest { """ class `foo` """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") classNamingRuleAssertThat(code) .hasLintViolationWithoutAutoCorrect(1, 7, "Class or object name should start with an uppercase letter and use camel case") } @@ -73,7 +73,7 @@ class ClassNamingRuleTest { @ParameterizedTest(name = "Suppression annotation: {0}") @ValueSource( strings = [ - "ktlint:class-naming", + "ktlint:standard:class-naming", "ClassName", // IntelliJ IDEA suppression ], ) @@ -117,7 +117,7 @@ class ClassNamingRuleTest { @ParameterizedTest(name = "Suppression annotation: {0}") @ValueSource( strings = [ - "ktlint:class-naming", + "ktlint:standard:class-naming", "ClassName", // IntelliJ IDEA suppression ], ) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/CommentWrappingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/CommentWrappingRuleTest.kt index 4ca97cf275..b9716f2ed3 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/CommentWrappingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/CommentWrappingRuleTest.kt @@ -93,7 +93,7 @@ class CommentWrappingRuleTest { * with a newline */ """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") commentWrappingRuleAssertThat(code) .hasLintViolationWithoutAutoCorrect(1, 17, "A block comment after any other element on the same line must be separated by a new line") } @@ -104,7 +104,7 @@ class CommentWrappingRuleTest { """ val foo /* some comment */ = "foo" """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") commentWrappingRuleAssertThat(code) .hasLintViolationWithoutAutoCorrect(1, 9, "A block comment in between other elements on the same line is disallowed") } @@ -117,7 +117,7 @@ class CommentWrappingRuleTest { some comment */ = "foo" """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") commentWrappingRuleAssertThat(code) .hasLintViolationWithoutAutoCorrect(1, 9, "A block comment starting on same line as another element and ending on another line before another element is disallowed") } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/EnumEntryNameCaseRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/EnumEntryNameCaseRuleTest.kt index cbc1ebc757..cecbc0e4f5 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/EnumEntryNameCaseRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/EnumEntryNameCaseRuleTest.kt @@ -29,7 +29,7 @@ class EnumEntryNameCaseRuleTest { _FOO } """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") enumEntryNameCaseRuleAssertThat(code) .hasLintViolationWithoutAutoCorrect(2, 5, "Enum entry name should be uppercase underscore-separated names like \"ENUM_ENTRY\" or upper camel-case like \"EnumEntry\"") } @@ -55,7 +55,7 @@ class EnumEntryNameCaseRuleTest { Foo_Bar, } """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") enumEntryNameCaseRuleAssertThat(code) .hasLintViolationsWithoutAutoCorrect( LintViolation(2, 5, "Enum entry name should be uppercase underscore-separated names like \"ENUM_ENTRY\" or upper camel-case like \"EnumEntry\""), diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FilenameRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FilenameRuleTest.kt index 8ff0769cc8..c659bc307f 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FilenameRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FilenameRuleTest.kt @@ -72,7 +72,7 @@ class FilenameRuleTest { ], ) fun `Given a file containing a single declaration of a class type then the filename should match the class name`(code: String) { - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") fileNameRuleAssertThat(code) .asFileWithPath("/some/path/$UNEXPECTED_FILE_NAME") .hasLintViolationWithoutAutoCorrect(1, 1, "File '$UNEXPECTED_FILE_NAME' contains a single class and possibly also extension functions for that class and should be named same after that class 'Foo.kt'") @@ -86,7 +86,7 @@ class FilenameRuleTest { ], ) fun `Given a file containing one top level declaration then the file should be named after the identifier`(code: String) { - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") fileNameRuleAssertThat(code) .asFileWithPath(UNEXPECTED_FILE_NAME) .hasLintViolationWithoutAutoCorrect(1, 1, "File '$UNEXPECTED_FILE_NAME' contains a single top level declaration and should be named 'Foo.kt'") @@ -167,7 +167,7 @@ class FilenameRuleTest { class Foo $otherTopLevelDeclaration """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") fileNameRuleAssertThat(code) .asFileWithPath(UNEXPECTED_FILE_NAME) .hasLintViolationWithoutAutoCorrect(1, 1, "File '$UNEXPECTED_FILE_NAME' contains a single class and possibly also extension functions for that class and should be named same after that class 'Foo.kt'") diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionNamingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionNamingRuleTest.kt index ea113c9848..6f8116f8b9 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionNamingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionNamingRuleTest.kt @@ -39,7 +39,7 @@ class FunctionNamingRuleTest { """ fun `Some name`() {} """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") functionNamingRuleAssertThat(code) .hasLintViolationWithoutAutoCorrect(1, 5, "Function name should start with a lowercase letter (except factory methods) and use camel case") } @@ -75,7 +75,7 @@ class FunctionNamingRuleTest { """ fun do_something() {} """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") functionNamingRuleAssertThat(code) .hasLintViolationWithoutAutoCorrect(1, 5, "Function name should start with a lowercase letter (except factory methods) and use camel case") } @@ -117,7 +117,7 @@ class FunctionNamingRuleTest { """ fun $functionName() = "foo" """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") functionNamingRuleAssertThat(code) .hasLintViolationWithoutAutoCorrect(1, 5, "Function name should start with a lowercase letter (except factory methods) and use camel case") } @@ -125,7 +125,7 @@ class FunctionNamingRuleTest { @ParameterizedTest(name = "Suppression annotation: {0}") @ValueSource( strings = [ - "ktlint:function-naming", + "ktlint:standard:function-naming", "FunctionName", // IntelliJ IDEA suppression ], ) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionSignatureRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionSignatureRuleTest.kt index b3eebad07e..138b8cd989 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionSignatureRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionSignatureRuleTest.kt @@ -516,6 +516,7 @@ class FunctionSignatureRuleTest { inner class CleanUpByRelatedRules { @Test fun `Given a nullable type with a space before the quest then remove this space`() { + @Suppress("ktlint:standard:string-template") val code = """ fun String$UNEXPECTED_SPACES?.f1() = "some-result" @@ -525,7 +526,7 @@ class FunctionSignatureRuleTest { fun f5(): String$UNEXPECTED_SPACES? = "some-result" fun f6(): List = listOf("some-result", null) fun f7(): List$UNEXPECTED_SPACES? = null - """.trimIndent() // ktlint-disable string-template + """.trimIndent() val formattedCode = """ fun String?.f1() = "some-result" @@ -608,7 +609,7 @@ class FunctionSignatureRuleTest { fun f28(block: (T) -> String) = "some-result" fun f29(block: (T) -> String) = "some-result" """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") functionSignatureWrappingRuleAssertThat(code) .addAdditionalRuleProviders( { NoMultipleSpacesRule() }, diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IfElseBracingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IfElseBracingRuleTest.kt index 20606c4fb2..b3fe8f0069 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IfElseBracingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IfElseBracingRuleTest.kt @@ -62,7 +62,7 @@ class IfElseBracingRuleTest { } } """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") multiLineIfElseRuleAssertThat(code) .withEditorConfigOverride(CODE_STYLE_PROPERTY to codeStyle) .withEditorConfigOverride(IF_ELSE_BRACING_RULE_ID.createRuleExecutionEditorConfigProperty() to RuleExecution.enabled) @@ -138,7 +138,7 @@ class IfElseBracingRuleTest { } } """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") multiLineIfElseRuleAssertThat(code) .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) .hasLintViolation(4, 12, "All branches of the if statement should be wrapped between braces if at least one branch is wrapped between braces") @@ -237,7 +237,7 @@ class IfElseBracingRuleTest { doSomethingElse2() } """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") multiLineIfElseRuleAssertThat(code) .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) .hasLintViolations( @@ -259,7 +259,7 @@ class IfElseBracingRuleTest { doSomethingElse2() } """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") multiLineIfElseRuleAssertThat(code) .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) .hasLintViolations( @@ -282,7 +282,7 @@ class IfElseBracingRuleTest { } } """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") multiLineIfElseRuleAssertThat(code) .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) .hasLintViolations( @@ -304,7 +304,7 @@ class IfElseBracingRuleTest { doSomethingElse2() } """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") multiLineIfElseRuleAssertThat(code) .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) .hasLintViolation(7, 9, "All branches of the if statement should be wrapped between braces if at least one branch is wrapped between braces") @@ -325,7 +325,7 @@ class IfElseBracingRuleTest { } } """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") multiLineIfElseRuleAssertThat(code) .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) .hasLintViolation(5, 9, "All branches of the if statement should be wrapped between braces if at least one branch is wrapped between braces") @@ -346,7 +346,7 @@ class IfElseBracingRuleTest { } } """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") multiLineIfElseRuleAssertThat(code) .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) .hasLintViolation(3, 9, "All branches of the if statement should be wrapped between braces if at least one branch is wrapped between braces") diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IfElseWrappingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IfElseWrappingRuleTest.kt index b1b46d65f9..ba4dde057b 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IfElseWrappingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IfElseWrappingRuleTest.kt @@ -190,7 +190,7 @@ class IfElseWrappingRuleTest { if (true) { if (false) foo() else bar() } } """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") ifElseWrappingRuleAssertThat(code) .hasLintViolationWithoutAutoCorrect(2, 15, "A single line if-statement should be kept simple. The 'THEN' may not be wrapped in a block.") } @@ -203,7 +203,7 @@ class IfElseWrappingRuleTest { if (true) bar() else { if (false) foo() else bar() } } """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") ifElseWrappingRuleAssertThat(code) .hasLintViolationWithoutAutoCorrect(2, 26, "A single line if-statement should be kept simple. The 'ELSE' may not be wrapped in a block.") } @@ -217,7 +217,7 @@ class IfElseWrappingRuleTest { } else { } """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") ifElseWrappingRuleAssertThat(code).hasNoLintViolations() } @@ -235,7 +235,7 @@ class IfElseWrappingRuleTest { ) } """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") ifElseWrappingRuleAssertThat(code).hasNoLintViolations() } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt index 5e816c0b2f..e7bed23575 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt @@ -3204,7 +3204,7 @@ internal class IndentationRuleTest { ${TAB}line2 $MULTILINE_STRING_QUOTE.trimIndent() """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") indentationRuleAssertThat(code) .hasLintViolationWithoutAutoCorrect(1, 11, "Indentation of multiline string should not contain both tab(s) and space(s)") } @@ -4238,17 +4238,16 @@ internal class IndentationRuleTest { @Nested inner class `Issue 631 - Given some suppression directive in the middle of a file` { @Test - fun `Issue 631 - Given some code for which indentation is disabled with ktlint-disable-enable-block then do not fix indentation of that block only`() { + fun `Issue 631 - Given some code for which indentation is disabled with ktlint-suppression then do not fix indentation of that block only`() { val code = """ val fooWithIndentationFixing1: String = "foo" + "bar" - // ktlint-disable indent + @Suppress("ktlint:standard:indent") val fooWithIndentationFixingSuppressed: String = "foo" + "bar" - // ktlint-enable indent val fooWithIndentationFixing2: String = "foo" + "bar" @@ -4263,7 +4262,7 @@ internal class IndentationRuleTest { val fooWithIndentationFixing1: String = "foo" + "bar" - @Suppress("ktlint:indent") + @Suppress("ktlint:standard:indent") val fooWithIndentationFixingSuppressed: String = "foo" + "bar" @@ -4672,13 +4671,13 @@ internal class IndentationRuleTest { } @Test - fun `Issue 1644 - Given multiple nested brackets and some of them have ktlint-disable`() { + fun `Issue 1644 - Given multiple nested brackets and some of them have EOL comments`() { val code = """ fun fooBar() { fun foo() { // some code - } // ktlint-disable indent + } // some comment fun bar() { // some code diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/KdocWrappingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/KdocWrappingRuleTest.kt index f6a6080b4c..9f79e596b9 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/KdocWrappingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/KdocWrappingRuleTest.kt @@ -64,7 +64,7 @@ class KdocWrappingRuleTest { * with a newline */ """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") kdocWrappingRuleAssertThat(code) .hasLintViolationWithoutAutoCorrect(1, 17, "A KDoc comment after any other element on the same line must be separated by a new line") } @@ -87,7 +87,7 @@ class KdocWrappingRuleTest { some KDoc comment */ = "foo" """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") kdocWrappingRuleAssertThat(code) .hasLintViolationWithoutAutoCorrect(1, 9, "A KDoc comment starting on same line as another element and ending on another line before another element is disallowed") } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MaxLineLengthRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MaxLineLengthRuleTest.kt index 470cb74703..5d51ebb8fd 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MaxLineLengthRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MaxLineLengthRuleTest.kt @@ -105,37 +105,23 @@ class MaxLineLengthRuleTest { @Nested inner class `Given some error suppression` { - @Test - fun `Given some code followed by a ktlint-disable directive which causes the line length to be exceeded then do not return a lint error for that line`() { - val code = - """ - // $MAX_LINE_LENGTH_MARKER $EOL_CHAR - val bar = "bar" // ktlint-disable some-rule-id - """.trimIndent() - maxLineLengthRuleAssertThat(code) - .setMaxLineLength() - .hasNoLintViolations() - } - @Test fun `Given code that is wrapped into a ktlint-disable block then do no return lint errors for lines in this block`() { val code = """ - // $MAX_LINE_LENGTH_MARKER $EOL_CHAR + // $MAX_LINE_LENGTH_MARKER $EOL_CHAR fun foo() { - println("teeeeeeeeeeeeeeeeeeeext") - /* ktlint-disable max-line-length */ - println("teeeeeeeeeeeeeeeeeeeext") - println("teeeeeeeeeeeeeeeeeeeext") - /* ktlint-enable max-line-length */ - println("teeeeeeeeeeeeeeeeeeeext") + println("teeeeeeeeeeeeeeeeeeeeeeeeeeeeeeext") + @Suppress("ktlint:standard:max-line-length") + println("teeeeeeeeeeeeeeeeeeeeeeeeeeeeeeext") + println("teeeeeeeeeeeeeeeeeeeeeeeeeeeeeeext") } """.trimIndent() maxLineLengthRuleAssertThat(code) .setMaxLineLength() .hasLintViolationsWithoutAutoCorrect( - LintViolation(3, 1, "Exceeded max line length (37)"), - LintViolation(8, 1, "Exceeded max line length (37)"), + LintViolation(3, 1, "Exceeded max line length (48)"), + LintViolation(6, 1, "Exceeded max line length (48)"), ) } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveCommentsRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveCommentsRuleTest.kt index 6574988f3a..aeeb62142f 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveCommentsRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveCommentsRuleTest.kt @@ -52,7 +52,7 @@ class NoConsecutiveCommentsRuleTest { /** KDoc 1 */ /* Block comment 2 */ """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") noConsecutiveBlankLinesRuleAssertThat(code) .withEditorConfigOverride(CODE_STYLE_PROPERTY to codeStyle) .withEditorConfigOverride(NO_CONSECUTIVE_COMMENTS_RULE_ID.createRuleExecutionEditorConfigProperty() to RuleExecution.enabled) @@ -118,7 +118,7 @@ class NoConsecutiveCommentsRuleTest { /** KDoc */ /* Block comment */ """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") noConsecutiveBlankLinesRuleAssertThat(code) .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) .hasLintViolationWithoutAutoCorrect(2, 1, "a block comment may not be preceded by a KDoc. Reversed order is allowed though when separated by a newline.") @@ -159,7 +159,7 @@ class NoConsecutiveCommentsRuleTest { /** KDoc */ // EOL comment """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") noConsecutiveBlankLinesRuleAssertThat(code) .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) .hasLintViolationWithoutAutoCorrect(2, 1, "an EOL comment may not be preceded by a KDoc. Reversed order is allowed though when separated by a newline.") @@ -200,7 +200,7 @@ class NoConsecutiveCommentsRuleTest { // EOL comment /* Block comment */ """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") noConsecutiveBlankLinesRuleAssertThat(code) .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) .hasLintViolationWithoutAutoCorrect(2, 1, "a block comment may not be preceded by an EOL comment unless separated by a blank line") @@ -226,7 +226,7 @@ class NoConsecutiveCommentsRuleTest { /* Block comment */ // EOL comment """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") noConsecutiveBlankLinesRuleAssertThat(code) .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) .hasLintViolationWithoutAutoCorrect(2, 1, "an EOL comment may not be preceded by a block comment unless separated by a blank line") diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRuleTest.kt index 68eefbd98b..ec88d2c7c6 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRuleTest.kt @@ -91,7 +91,7 @@ class NoSingleLineBlockCommentRuleTest { """ val foo = "foo" // Some comment """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") noSingleLineBlockCommentRuleAssertThat(code) .hasLintViolation(1, 16, "Replace the block comment with an EOL comment") .isFormattedAs(formattedCode) @@ -107,7 +107,7 @@ class NoSingleLineBlockCommentRuleTest { """ fun foo() = "foo" // Some comment """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") noSingleLineBlockCommentRuleAssertThat(code) .hasLintViolation(1, 19, "Replace the block comment with an EOL comment") .isFormattedAs(formattedCode) @@ -163,17 +163,4 @@ class NoSingleLineBlockCommentRuleTest { .withEditorConfigOverride(CODE_STYLE_PROPERTY to CodeStyleValue.ktlint_official) .hasNoLintViolations() } - - @Test - fun `Given single line block comments to disable or enable ktlint then do not reformat`() { - val code = - """ - /* ktlint-disable foo-rule-id bar-rule-id */ - val foo = "foo" - /* ktlint-enable foo-rule-id bar-rule-id */ - """.trimIndent() - noSingleLineBlockCommentRuleAssertThat(code) - .withEditorConfigOverride(CODE_STYLE_PROPERTY to CodeStyleValue.ktlint_official) - .hasNoLintViolations() - } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PackageNameRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PackageNameRuleTest.kt index 508e17eb7a..8f59022693 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PackageNameRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PackageNameRuleTest.kt @@ -67,7 +67,7 @@ class PackageNameRuleTest { @ParameterizedTest(name = "Suppression annotation: {0}") @ValueSource( strings = [ - "ktlint:package-name", + "ktlint:standard:package-name", "PackageName", // IntelliJ IDEA suppression ], ) @@ -80,15 +80,6 @@ class PackageNameRuleTest { packageNameRuleAssertThat(code).hasNoLintViolations() } - @Test - fun `Given a package name containing an forbidden character and the rule is suppressed via ktlint directive in comment then do not emit`() { - val code = - """ - package foo.foo_bar // ktlint-disable package-name - """.trimIndent() - packageNameRuleAssertThat(code).hasNoLintViolations() - } - @Test fun `Issue 1757 - Given a package name containing diacritics then do no report a violation`() { val code = diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListSpacingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListSpacingRuleTest.kt index ff59f97677..ec6b2788ae 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListSpacingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListSpacingRuleTest.kt @@ -48,12 +48,12 @@ class ParameterListSpacingRuleTest { } @Test - fun `Given a function signature without parameters but containing an comment (for example to disable a ktlint rule) in the parameter list then do not reformat`() { + fun `Given a function signature without parameters but containing an EOL comment in the parameter list then do not reformat`() { val code = """ - data class Foo @JvmOverloads constructor( // ktlint-disable annotation + data class Foo @JvmOverloads constructor( // some comment ) - @JvmOverloads fun foo1( // ktlint-disable annotation + @JvmOverloads fun foo1( // some comment ) fun foo2( // some comment @@ -69,7 +69,7 @@ class ParameterListSpacingRuleTest { fun `Given a function signature without parameters but containing an EOL comment not preceded by a whitespace then avoid conflict with comment spacing rule`() { val code = """ - data class Foo @JvmOverloads constructor(// ktlint-disable annotation + data class Foo @JvmOverloads constructor(// some comment ) """.trimIndent() parameterListSpacingRuleAssertThat(code) @@ -331,10 +331,11 @@ class ParameterListSpacingRuleTest { @Test fun `Given a function signature with multiple parameters and at least one space before the comma separating the parameters then reformat`() { + @Suppress("ktlint:standard:string-template") val code = """ fun foo(a: Any$TOO_MANY_SPACES, b: Any) = "some-result" - """.trimIndent() // ktlint-disable string-template + """.trimIndent() val formattedCode = """ fun foo(a: Any, b: Any) = "some-result" @@ -346,11 +347,12 @@ class ParameterListSpacingRuleTest { @Test fun `Given a function signature with multiple parameters and at least one newline before the comma separating the parameters then reformat`() { + @Suppress("ktlint:standard:string-template") val code = """ fun foo(a: Any , b: Any) = "some-result" - """.trimIndent() // ktlint-disable string-template + """.trimIndent() val formattedCode = """ fun foo(a: Any, b: Any) = "some-result" @@ -377,10 +379,11 @@ class ParameterListSpacingRuleTest { @Test fun `Given a function signature with multiple parameters and too many spaces after the comma separating the parameters then reformat`() { + @Suppress("ktlint:standard:string-template") val code = """ fun foo(a: Any,${TOO_MANY_SPACES}b: Any) = "some-result" - """.trimIndent() // ktlint-disable string-template + """.trimIndent() val formattedCode = """ fun foo(a: Any, b: Any) = "some-result" @@ -392,13 +395,14 @@ class ParameterListSpacingRuleTest { @Test fun `Given a function signature with multiple parameters and a newline after the comma separating the parameters then do not reformat`() { + @Suppress("ktlint:standard:string-template") val code = """ fun foo( a: Any, b: Any ) = "some-result" - """.trimIndent() // ktlint-disable string-template + """.trimIndent() parameterListSpacingRuleAssertThat(code).hasNoLintViolations() } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRuleTest.kt index e8926d274e..94aac3ea93 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRuleTest.kt @@ -150,7 +150,7 @@ class ParameterListWrappingRuleTest { c: Any ) """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") parameterListWrappingRuleAssertThat(code) .hasLintViolation(3, 13, "Parameter should start on a newline") .isFormattedAs(formattedCode) @@ -311,7 +311,7 @@ class ParameterListWrappingRuleTest { } } """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") parameterListWrappingRuleAssertThat(code) .hasLintViolation(2, 11, "Parameter should start on a newline") .isFormattedAs(formattedCode) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRuleTest.kt index bc0271b805..1fbf6136f9 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRuleTest.kt @@ -43,7 +43,7 @@ class PropertyNamingRuleTest { const val FOO_BAR_2 = "foo-bar-2" const val ŸÈŠ_THÎS_IS_ALLOWED_123 = "Yes this is allowed" """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") propertyNamingRuleAssertThat(code) .hasLintViolationWithoutAutoCorrect(1, 11, "Property name should use the screaming snake case notation when the value can not be changed") } @@ -55,7 +55,7 @@ class PropertyNamingRuleTest { val foo = Foo() val FOO_BAR = FooBar() """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") propertyNamingRuleAssertThat(code) .hasLintViolationWithoutAutoCorrect(1, 5, "Property name should use the screaming snake case notation when the value can not be changed") } @@ -71,7 +71,7 @@ class PropertyNamingRuleTest { } } """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") propertyNamingRuleAssertThat(code) .hasLintViolationWithoutAutoCorrect(3, 13, "Property name should use the screaming snake case notation when the value can not be changed") } @@ -161,7 +161,7 @@ class PropertyNamingRuleTest { @ParameterizedTest(name = "Suppression annotation: {0}") @ValueSource( strings = [ - "ktlint:property-naming", + "ktlint:standard:property-naming", "PropertyName", // IntelliJ IDEA suppression ], ) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/importordering/ImportOrderingRuleAsciiTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/importordering/ImportOrderingRuleAsciiTest.kt index b5015142fc..469cd7b536 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/importordering/ImportOrderingRuleAsciiTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/importordering/ImportOrderingRuleAsciiTest.kt @@ -121,7 +121,7 @@ class ImportOrderingRuleAsciiTest { import android.app.Activity import android.view.ViewGroup """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") importOrderingRuleAssertThat(code) .withEditorConfigOverride(ASCII_IMPORT_ORDERING) .hasLintViolationWithoutAutoCorrect(1, 1, "Imports must be ordered in lexicographic order without any empty lines in-between -- no autocorrection due to comments in the import list") @@ -136,7 +136,7 @@ class ImportOrderingRuleAsciiTest { import android.app.Activity import android.view.ViewGroup """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") importOrderingRuleAssertThat(code) .withEditorConfigOverride(ASCII_IMPORT_ORDERING) .hasLintViolationWithoutAutoCorrect(1, 1, "Imports must be ordered in lexicographic order without any empty lines in-between -- no autocorrection due to comments in the import list") diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/importordering/ImportOrderingRuleIdeaTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/importordering/ImportOrderingRuleIdeaTest.kt index 2447bee4bf..1270f32bc7 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/importordering/ImportOrderingRuleIdeaTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/importordering/ImportOrderingRuleIdeaTest.kt @@ -38,7 +38,7 @@ class ImportOrderingRuleIdeaTest { import android.content.Context as Ctx import androidx.fragment.app.Fragment as F """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") importOrderingRuleAssertThat(code) .withEditorConfigOverride(IDEA_DEFAULT_IMPORT_ORDERING) .hasLintViolation(1, 1, "Imports must be ordered in lexicographic order without any empty lines in-between with \"java\", \"javax\", \"kotlin\" and aliases in the end") @@ -109,7 +109,7 @@ class ImportOrderingRuleIdeaTest { import android.view.ViewGroup import java.util.List """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") importOrderingRuleAssertThat(code) .withEditorConfigOverride(IDEA_DEFAULT_IMPORT_ORDERING) .hasLintViolation(1, 1, "Imports must be ordered in lexicographic order without any empty lines in-between with \"java\", \"javax\", \"kotlin\" and aliases in the end") @@ -146,7 +146,7 @@ class ImportOrderingRuleIdeaTest { import android.content.Context as Ctx import androidx.fragment.app.Fragment as F // comment 3 """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") importOrderingRuleAssertThat(code) .withEditorConfigOverride(IDEA_DEFAULT_IMPORT_ORDERING) .hasLintViolation(1, 1, "Imports must be ordered in lexicographic order without any empty lines in-between with \"java\", \"javax\", \"kotlin\" and aliases in the end") diff --git a/ktlint-test/src/main/kotlin/com/pinterest/ktlint/test/KtlintDocumentationTest.kt b/ktlint-test/src/main/kotlin/com/pinterest/ktlint/test/KtlintDocumentationTest.kt new file mode 100644 index 0000000000..b59050a7ce --- /dev/null +++ b/ktlint-test/src/main/kotlin/com/pinterest/ktlint/test/KtlintDocumentationTest.kt @@ -0,0 +1,11 @@ +package com.pinterest.ktlint.test + +import org.junit.jupiter.api.Test + +/** + * Mark a unit test as a unit test for an example in the Ktlint documentation. For now the purpose is only to indicate that the code of the + * test is also used in the documentation. A future possible use it that the code samples in the documentation are extracted from the unit + * test directly which then will ensure that the code samples in the documentation actually provide the documented results. + */ +@Test +public annotation class KtlintDocumentationTest