Skip to content

Commit

Permalink
Add new experimental rule for aligning the initial stars in a block c…
Browse files Browse the repository at this point in the history
…omment when present

Closes pinterest#297
  • Loading branch information
paul-dingemans committed Mar 5, 2022
1 parent cf96f51 commit 613d59b
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
## Unreleased

### 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))

### Fixed

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,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:annotation-spacing``: Annotations should be separated by the annotated declaration by a single line break
- `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: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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.BLOCK_COMMENT
import com.pinterest.ktlint.core.ast.lineIndent
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafElement

/**
* When present, align the initial star in a block comment.
*/
class BlockCommentInitialStarAlignmentRule :
Rule(
"block-comment-initial-star-alignment",
visitorModifiers = setOf(
// The block comment is a node which can contain multiple lines. The indent of the second and later line
// should be determined based on the indent of the block comment node. This indent is determined by the
// indentation rule.
VisitorModifier.RunAfterRule("standard:indent")
)
) {
override fun visit(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
if (node.elementType == BLOCK_COMMENT) {
val expectedIndentForLineWithInitialStar = node.lineIndent() + " *"
val lines = node.text.split("\n")
var offset = node.startOffset
val modifiedLines = mutableListOf<String>()
lines.forEach { line ->
val modifiedLine =
Regex("^([\t ]+\\*)(.*)$")
.find(line)
?.let { matchResult ->
val (prefix, content) = matchResult.destructured
if (prefix != expectedIndentForLineWithInitialStar) {
emit(offset + prefix.length, "Initial star should be align with start of block comment", true)
expectedIndentForLineWithInitialStar + content
} else {
line
}
}
?: line
modifiedLines.add(modifiedLine)
offset += line.length + 1
}
if (autoCorrect) {
val newText = modifiedLines.joinToString(separator = System.lineSeparator())
if (node.text != newText) {
(node as LeafElement).rawReplaceWithText(newText)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class ExperimentalRuleSetProvider : RuleSetProvider {
SpacingAroundAngleBracketsRule(),
SpacingAroundUnaryOperatorRule(),
AnnotationSpacingRule(),
UnnecessaryParenthesesBeforeTrailingLambdaRule()
UnnecessaryParenthesesBeforeTrailingLambdaRule(),
BlockCommentInitialStarAlignmentRule()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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 BlockCommentInitialStarTest {
@Test
fun `Given a block comment for which the indentation followed by the star already aligns with the star in the first line of the block comment then do not reformat`() {
val code =
"""
/*
* This blocked is formatted well.
*/
""".trimIndent()
assertThat(BlockCommentInitialStarAlignmentRule().lint(code)).isEmpty()
assertThat(BlockCommentInitialStarAlignmentRule().format(code)).isEqualTo(code)
}

@Test
fun `Given a block comment with a line that does not have an initial star then do not reformat that line`() {
val code =
"""
/*
This blocked is formatted well.
*/
""".trimIndent()
assertThat(BlockCommentInitialStarAlignmentRule().lint(code)).isEmpty()
assertThat(BlockCommentInitialStarAlignmentRule().format(code)).isEqualTo(code)
}

@Test
fun `Given a block comment with a line that contains a star which is preceded by a non-space or non-tab character then do not reformat that line`() {
val code =
"""
/*
- This line contains a * but it is not the initial *.
*/
""".trimIndent()
assertThat(BlockCommentInitialStarAlignmentRule().lint(code)).isEmpty()
assertThat(BlockCommentInitialStarAlignmentRule().format(code)).isEqualTo(code)
}

@Test
fun `Given that the initial stars of the block comment are not aligned then reformat`() {
val code =
"""
/*
* This blocked is not formatted well.
*/
""".trimIndent()
val formattedCode =
"""
/*
* This blocked is not formatted well.
*/
""".trimIndent()
assertThat(BlockCommentInitialStarAlignmentRule().lint(code)).containsExactly(
LintError(2, 8, "block-comment-initial-star-alignment", "Initial star should be align with start of block comment"),
LintError(3, 6, "block-comment-initial-star-alignment", "Initial star should be align with start of block comment")
)
assertThat(BlockCommentInitialStarAlignmentRule().format(code)).isEqualTo(formattedCode)
}

@Test
fun `Given function contain a block comment for which the initial stars of the block comment are not aligned then reformat`() {
val code =
"""
fun foo() {
/*
* This blocked is not formatted well.
* This blocked is not formatted well.
*/
}
""".trimIndent()
val formattedCode =
"""
fun foo() {
/*
* This blocked is not formatted well.
* This blocked is not formatted well.
*/
}
""".trimIndent()
assertThat(BlockCommentInitialStarAlignmentRule().lint(code)).containsExactly(
LintError(3, 12, "block-comment-initial-star-alignment", "Initial star should be align with start of block comment"),
LintError(4, 4, "block-comment-initial-star-alignment", "Initial star should be align with start of block comment"),
LintError(5, 10, "block-comment-initial-star-alignment", "Initial star should be align with start of block comment")
)
assertThat(BlockCommentInitialStarAlignmentRule().format(code)).isEqualTo(formattedCode)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,14 +178,14 @@ object IntellijIDEAIntegration {

private fun enableOptimizeImportsOnTheFly(arr: ByteArray): ByteArray {
/*
<application>
<component name="CodeInsightSettings">
<option name="OPTIMIZE_IMPORTS_ON_THE_FLY" value="true" />
...
</component>
...
</application>
*/
* <application>
* <component name="CodeInsightSettings">
* <option name="OPTIMIZE_IMPORTS_ON_THE_FLY" value="true" />
* ...
* </component>
* ...
* </application>
*/
val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(ByteArrayInputStream(arr))
val xpath = XPathFactory.newInstance().newXPath()
var cis = xpath.evaluate(
Expand Down Expand Up @@ -218,14 +218,14 @@ object IntellijIDEAIntegration {

private fun enableOptimizeImportsOnTheFlyInsideWorkspace(arr: ByteArray): ByteArray {
/*
<project>
<component name="CodeInsightWorkspaceSettings">
<option name="optimizeImportsOnTheFly" value="false" />
...
</component>
...
</project>
*/
* <project>
* <component name="CodeInsightWorkspaceSettings">
* <option name="optimizeImportsOnTheFly" value="false" />
* ...
* </component>
* ...
* </project>
*/
val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(ByteArrayInputStream(arr))
val xpath = XPathFactory.newInstance().newXPath()
var cis = xpath.evaluate(
Expand Down

0 comments on commit 613d59b

Please sign in to comment.