Skip to content

Commit

Permalink
Wrapping block and kdoc comments (#1403)
Browse files Browse the repository at this point in the history
Add new experimental rules for wrapping of block comments and KDoc comments

Closes #1329
  • Loading branch information
paul-dingemans committed Mar 13, 2022
1 parent c4ae318 commit 6c74bcb
Show file tree
Hide file tree
Showing 9 changed files with 513 additions and 9 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ This section is applicable when providing rules that depend on one or more value
### Added
- New experimental rule for aligning the initial stars in a block comment when present (`experimental:block-comment-initial-star-alignment` ([#297](https://github.com/pinterest/ktlint/issues/297))
- Respect `.editorconfig` property `ij_kotlin_packages_to_use_import_on_demand` (`no-wildcard-imports`) ([#1272](https://github.com/pinterest/ktlint/pull/1272))
- Add new experimental rules for wrapping of block comment (`comment-wrapping`) ([#1403](https://github.com/pinterest/ktlint/pull/1403))
- Add new experimental rules for wrapping of KDoc comment (`kdoc-wrapping`) ([#1403](https://github.com/pinterest/ktlint/pull/1403))

### Fixed
- Fix lint message to "Unnecessary long whitespace" (`no-multi-spaces`) ([#1394](https://github.com/pinterest/ktlint/issues/1394))
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,12 @@ New rules will be added into the [experimental ruleset](https://github.com/pinte
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:block-comment-initial-star-alignment`: Lines in a block comment which (exclusive the indentation) start with a `*` should have this `*` aligned with the `*` in the opening of the block comment.
- `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
- `experimental:package-name`: No underscores in package names
- `experimental:unary-op-spacing`: No spaces around unary operators
- `experimental:unnecessary-parentheses-before-trailing-lambda`: An empty parentheses block before a lambda is redundant. For example `some-string".count() { it == '-' }`

### Spacing
Expand All @@ -95,6 +93,12 @@ by passing the `--experimental` flag to `ktlint`.
- `experimental:spacing-around-angle-brackets`: No spaces around angle brackets
- `experimental:spacing-between-declarations-with-annotations`: Declarations with annotations should be separated by a blank line
- `experimental:spacing-between-declarations-with-comments`: Declarations with comments should be separated by a blank line
- `experimental:unary-op-spacing`: No spaces around unary operators

### Wrapping
- `experimental:argument-list-wrapping`: Argument list wrapping
- `experimental:comment-wrapping`: A block comment should start and end on a line that does not contain any other element. A block comment should not be used as end of line comment.
- `experimental:kdoc-wrapping`: A KDoc comment should start and end on a line that does not contain any other element.

## EditorConfig

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties
import com.pinterest.ktlint.core.api.FeatureInAlphaState
import com.pinterest.ktlint.core.api.UsesEditorConfigProperties
import com.pinterest.ktlint.core.ast.ElementType.BLOCK_COMMENT
import com.pinterest.ktlint.core.ast.ElementType.EOL_COMMENT
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
import com.pinterest.ktlint.core.ast.lineIndent
import com.pinterest.ktlint.core.ast.lineNumber
import com.pinterest.ktlint.core.ast.nextLeaf
import com.pinterest.ktlint.core.ast.prevLeaf
import com.pinterest.ktlint.core.ast.upsertWhitespaceBeforeMe
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiCommentImpl

/**
* Checks external wrapping of block comments. Wrapping inside the comment is not altered. A block comment following
* another element on the same line is replaced with an EOL comment, if possible.
*/
@OptIn(FeatureInAlphaState::class)
public class CommentWrappingRule :
Rule("comment-wrapping"),
UsesEditorConfigProperties {
override val editorConfigProperties: List<UsesEditorConfigProperties.EditorConfigProperty<*>> =
listOf(
DefaultEditorConfigProperties.indentSizeProperty,
DefaultEditorConfigProperties.indentStyleProperty
)

override fun visit(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
if (node.elementType == BLOCK_COMMENT) {
val nonIndentLeafOnSameLinePrecedingBlockComment =
node
.prevLeaf()
?.takeIf { isNonIndentLeafOnSameLine(it) }
val nonIndentLeafOnSameLineFollowingBlockComment =
node
.nextLeaf()
?.takeIf { isNonIndentLeafOnSameLine(it) }

if (nonIndentLeafOnSameLinePrecedingBlockComment != null &&
nonIndentLeafOnSameLineFollowingBlockComment != null
) {
if (nonIndentLeafOnSameLinePrecedingBlockComment.lineNumber() == nonIndentLeafOnSameLineFollowingBlockComment.lineNumber()) {
// Do not try to fix constructs like below:
// val foo /* some comment */ = "foo"
emit(
node.startOffset,
"A block comment in between other elements on the same line is disallowed",
false
)
} else {
// Do not try to fix constructs like below:
// val foo = "foo" /*
// some comment
// */ val bar = "bar"
emit(
node.startOffset,
"A block comment starting on same line as another element and ending on another line before another element is disallowed",
false
)
}
return
}

nonIndentLeafOnSameLinePrecedingBlockComment
?.precedesBlockCommentOnSameLine(node, emit, autoCorrect)

nonIndentLeafOnSameLineFollowingBlockComment
?.followsBlockCommentOnSameLine(node, emit, autoCorrect)
}
}

private fun ASTNode.precedesBlockCommentOnSameLine(
blockCommentNode: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean
) {
val leafAfterBlockComment = blockCommentNode.nextLeaf()
if (!blockCommentNode.textContains('\n') && leafAfterBlockComment.isLastElementOnLine()) {
emit(
startOffset,
"A single line block comment after a code element on the same line must be replaced with an EOL comment",
true
)
if (autoCorrect) {
blockCommentNode.replaceWithEndOfLineComment()
}
} else {
// It can not be autocorrected as it might depend on the situation and code style what is preferred.
emit(
blockCommentNode.startOffset,
"A block comment after any other element on the same line must be separated by a new line",
false
)
}
}

private fun ASTNode.replaceWithEndOfLineComment() {
val content = text.removeSurrounding("/*", "*/").trim()
val eolComment = PsiCommentImpl(EOL_COMMENT, "// $content")
(this as LeafPsiElement).rawInsertBeforeMe(eolComment)
rawRemove()
}

private fun ASTNode.followsBlockCommentOnSameLine(
blockCommentNode: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean
) {
emit(startOffset, "A block comment may not be followed by any other element on that same line", true)
if (autoCorrect) {
if (elementType == WHITE_SPACE) {
(this as LeafPsiElement).rawReplaceWithText("\n${blockCommentNode.lineIndent()}")
} else {
(this as LeafPsiElement).upsertWhitespaceBeforeMe("\n${blockCommentNode.lineIndent()}")
}
}
}

private fun isNonIndentLeafOnSameLine(it: ASTNode) =
it.elementType != WHITE_SPACE || !it.textContains('\n')

private fun ASTNode?.isLastElementOnLine() =
this == null || (elementType == WHITE_SPACE && textContains('\n'))
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class ExperimentalRuleSetProvider : RuleSetProvider {
DiscouragedCommentLocationRule(),
FunKeywordSpacingRule(),
FunctionTypeReferenceSpacingRule(),
ModifierListSpacingRule()
ModifierListSpacingRule(),
CommentWrappingRule(),
KdocWrappingRule()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties
import com.pinterest.ktlint.core.api.FeatureInAlphaState
import com.pinterest.ktlint.core.api.UsesEditorConfigProperties
import com.pinterest.ktlint.core.ast.ElementType.KDOC
import com.pinterest.ktlint.core.ast.ElementType.KDOC_END
import com.pinterest.ktlint.core.ast.ElementType.KDOC_START
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
import com.pinterest.ktlint.core.ast.lineIndent
import com.pinterest.ktlint.core.ast.lineNumber
import com.pinterest.ktlint.core.ast.nextLeaf
import com.pinterest.ktlint.core.ast.prevLeaf
import com.pinterest.ktlint.core.ast.upsertWhitespaceBeforeMe
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement

/**
* Checks external wrapping of KDoc comment. Wrapping inside the KDoc comment is not altered.
*/
@OptIn(FeatureInAlphaState::class)
public class KdocWrappingRule :
Rule("kdoc-wrapping"),
UsesEditorConfigProperties {
override val editorConfigProperties: List<UsesEditorConfigProperties.EditorConfigProperty<*>> =
listOf(
DefaultEditorConfigProperties.indentSizeProperty,
DefaultEditorConfigProperties.indentStyleProperty
)

override fun visit(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
if (node.elementType == KDOC) {
val nonIndentLeafOnSameLinePrecedingKdocComment =
node
.findChildByType(KDOC_START)
?.prevLeaf()
?.takeIf { isNonIndentLeafOnSameLine(it) }
val nonIndentLeafOnSameLineFollowingKdocComment =
node
.findChildByType(KDOC_END)
?.nextLeaf()
?.takeIf { isNonIndentLeafOnSameLine(it) }

if (nonIndentLeafOnSameLinePrecedingKdocComment != null &&
nonIndentLeafOnSameLineFollowingKdocComment != null
) {
if (nonIndentLeafOnSameLinePrecedingKdocComment.lineNumber() == nonIndentLeafOnSameLineFollowingKdocComment.lineNumber()) {
// Do not try to fix constructs like below:
// val foo /** some comment */ = "foo"
emit(
node.startOffset,
"A KDoc comment in between other elements on the same line is disallowed",
false
)
} else {
// Do not try to fix constructs like below:
// val foo = "foo" /*
// some comment*
// */ val bar = "bar"
emit(
node.startOffset,
"A KDoc comment starting on same line as another element and ending on another line before another element is disallowed",
false
)
}
return
}

if (nonIndentLeafOnSameLinePrecedingKdocComment != null) {
// It can not be autocorrected as it might depend on the situation and code style what is
// preferred.
emit(
node.startOffset,
"A KDoc comment after any other element on the same line must be separated by a new line",
false
)
}

nonIndentLeafOnSameLineFollowingKdocComment
?.followsKdocCommentOnSameLine(node, emit, autoCorrect)
}
}

private fun ASTNode.followsKdocCommentOnSameLine(
kdocCommentNode: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean
) {
emit(startOffset, "A KDoc comment may not be followed by any other element on that same line", true)
if (autoCorrect) {
if (elementType == WHITE_SPACE) {
(this as LeafPsiElement).rawReplaceWithText("\n${kdocCommentNode.lineIndent()}")
} else {
(this as LeafPsiElement).upsertWhitespaceBeforeMe("\n${kdocCommentNode.lineIndent()}")
}
}
}

private fun isNonIndentLeafOnSameLine(it: ASTNode) =
it.elementType != WHITE_SPACE || !it.textContains('\n')
}

0 comments on commit 6c74bcb

Please sign in to comment.