diff --git a/CHANGELOG.md b/CHANGELOG.md index e2bb99b58c..dc877aa9d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Please welcome [paul-dingemans](https://github.com/paul-dingemans) as an officia - Basic tests for CLI ([#540](https://github.com/pinterest/ktlint/issues/540)) - Add experimental rule for unexpected spaces in a type reference before a function identifier (`function-type-reference-spacing`) ([#1341](https://github.com/pinterest/ktlint/issues/1341)) - Add experimental rule for unnecessary parentheses in function call followed by lambda ([#1068](https://github.com/pinterest/ktlint/issues/1068)) +- Add experimental rule to detect discouraged comment locations (`discouraged-comment-location`) ([#1365](https://github.com/pinterest/ktlint/pull/1365)) - Add rule to check spacing after fun keyword (`fun-keyword-spacing`) ([#1362](https://github.com/pinterest/ktlint/pull/1362)) - Add experimental rules for unnecessary spacing between modifiers in and after the last modifier in a modifier list ([#1361](https://github.com/pinterest/ktlint/pull/1361)) diff --git a/README.md b/README.md index f19f616c5c..e32f933b58 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ by passing the `--experimental` flag to `ktlint`. - `experimental:annotation`: Annotation formatting - multiple annotations should be on a separate line than the annotated declaration; annotations with parameters should each be on separate lines; annotations should be followed by a space - `experimental:argument-list-wrapping`: Argument list wrapping +- `experimental:discouraged-comment-location`: Detect discouraged comment locations (no autocorrect) - `experimental:enum-entry-name-case`: Enum entry names should be uppercase underscore-separated names - `experimental:multiline-if-else`: Braces required for multiline if/else statements - `experimental:no-empty-first-line-in-method-block`: No leading empty lines in method blocks diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/DiscouragedCommentLocationRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/DiscouragedCommentLocationRule.kt new file mode 100644 index 0000000000..6979a11a3d --- /dev/null +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/DiscouragedCommentLocationRule.kt @@ -0,0 +1,51 @@ +package com.pinterest.ktlint.ruleset.experimental + +import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.ast.ElementType.TYPE_PARAMETER_LIST +import com.pinterest.ktlint.core.ast.isPartOfComment +import com.pinterest.ktlint.core.ast.prevCodeSibling +import org.jetbrains.kotlin.com.intellij.lang.ASTNode + +/** + * The AST allows comments to be placed anywhere. This however can lead to code which is unnecessarily hard to read. + * Disallowing comments at certain positions in the AST makes development of a rule easier as they have not to be taken + * into account in that rule. + * + * In examples below, a comment is placed between a type parameter list and the function name. Such comments are badly + * handled by default IntelliJ IDEA code formatter. We should put no effort in making and keeping ktlint in sync with + * such bad code formatting. + * + * ``` + * fun + * // some comment + * foo(t: T) = "some-result" + * + * fun + * /* some comment + * * + * */ + * foo(t: T) = "some-result" + * ``` + */ +public class DiscouragedCommentLocationRule : Rule("discouraged-comment-location") { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + node + .takeIf { it.isPartOfComment() } + ?.prevCodeSibling() + ?.let { codeSiblingBeforeComment -> + // Be restrictive when adding new locations at which comments are discouraged. Always run against major + // open source projects first to verify whether valid cases are found to comment at this location. + if (codeSiblingBeforeComment.elementType == TYPE_PARAMETER_LIST) { + emit( + node.startOffset, + "No comment expected at this location", + false + ) + } + } + } +} diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ExperimentalRuleSetProvider.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ExperimentalRuleSetProvider.kt index 164847fe1c..42a26e5a84 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ExperimentalRuleSetProvider.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ExperimentalRuleSetProvider.kt @@ -22,6 +22,7 @@ public class ExperimentalRuleSetProvider : RuleSetProvider { SpacingAroundUnaryOperatorRule(), AnnotationSpacingRule(), UnnecessaryParenthesesBeforeTrailingLambdaRule(), + DiscouragedCommentLocationRule(), FunKeywordSpacingRule(), FunctionTypeReferenceSpacingRule(), ModifierListSpacingRule() diff --git a/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/DiscouragedCommentLocationTest.kt b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/DiscouragedCommentLocationTest.kt new file mode 100644 index 0000000000..5501b9f0f4 --- /dev/null +++ b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/DiscouragedCommentLocationTest.kt @@ -0,0 +1,92 @@ +package com.pinterest.ktlint.ruleset.experimental + +import com.pinterest.ktlint.core.LintError +import com.pinterest.ktlint.test.format +import com.pinterest.ktlint.test.lint +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class DiscouragedCommentLocationTest { + @Test + fun `Given an EOL comment after a type parameter then report a discouraged comment location`() { + val code = + """ + fun // some comment + foo(t: T) = "some-result" + """.trimIndent() + assertThat(DiscouragedCommentLocationRule().lint(code)).containsExactly( + LintError(1, 9, "discouraged-comment-location", "No comment expected at this location") + ) + assertThat(DiscouragedCommentLocationRule().format(code)).isEqualTo(code) + } + + @Test + fun `Given an EOL comment on a newline after a type parameter then report a discouraged comment location`() { + val code = + """ + fun + // some comment + foo(t: T) = "some-result" + """.trimIndent() + assertThat(DiscouragedCommentLocationRule().lint(code)).containsExactly( + LintError(2, 1, "discouraged-comment-location", "No comment expected at this location") + ) + assertThat(DiscouragedCommentLocationRule().format(code)).isEqualTo(code) + } + + @Test + fun `Given a block comment after a type parameter then report a discouraged comment location`() { + val code = + """ + fun /* some comment */ + foo(t: T) = "some-result" + """.trimIndent() + assertThat(DiscouragedCommentLocationRule().lint(code)).containsExactly( + LintError(1, 9, "discouraged-comment-location", "No comment expected at this location") + ) + assertThat(DiscouragedCommentLocationRule().format(code)).isEqualTo(code) + } + + @Test + fun `Given a block comment on a newline after a type parameter then report a discouraged comment location`() { + val code = + """ + fun + /* some comment */ + foo(t: T) = "some-result" + """.trimIndent() + assertThat(DiscouragedCommentLocationRule().lint(code)).containsExactly( + LintError(2, 1, "discouraged-comment-location", "No comment expected at this location") + ) + assertThat(DiscouragedCommentLocationRule().format(code)).isEqualTo(code) + } + + @Test + fun `Given a KDOC comment after a type parameter then report a discouraged comment location`() { + val code = + """ + fun /** some comment */ + foo(t: T) = "some-result" + """.trimIndent() + assertThat(DiscouragedCommentLocationRule().lint(code)).containsExactly( + LintError(1, 9, "discouraged-comment-location", "No comment expected at this location") + ) + assertThat(DiscouragedCommentLocationRule().format(code)).isEqualTo(code) + } + + @Test + fun `Given a KDOC comment on a newline after a type parameter then report a discouraged comment location`() { + val code = + """ + fun + /** + * some comment + */ + foo(t: T) = "some-result" + """.trimIndent() + assertThat(DiscouragedCommentLocationRule().lint(code)).containsExactly( + LintError(2, 1, "discouraged-comment-location", "No comment expected at this location") + ) + assertThat(DiscouragedCommentLocationRule().format(code)).isEqualTo(code) + } +}