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

Closes #297
  • Loading branch information
paul-dingemans committed Mar 12, 2022
1 parent 22e4234 commit c4ae318
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ If you are not an API user nor a RuleSet provider, then you can safely skip this
This section is applicable when providing rules that depend on one or more values of ".editorconfig" properties. Property values should no longer be retrieved via *EditConfig* or directly via `userData[EDITOR_CONFIG_USER_DATA_KEY]`. Property values should now only be retrieved using method `ASTNode.getEditorConfigValue(editorConfigProperty)` of interface `UsesEditorConfigProperties` which is provided in this release. Starting from next release after the current release, the *EditConfig* and/or `userData[EDITOR_CONFIG_USER_DATA_KEY]` may be removed without further notice which will break your API or rule. To prevent disruption of your end user, you should migrate a.s.a.p.

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

### Fixed
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: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
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 = "\n")
if (node.text != newText) {
(node as LeafElement).rawReplaceWithText(newText)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class ExperimentalRuleSetProvider : RuleSetProvider {
SpacingAroundUnaryOperatorRule(),
AnnotationSpacingRule(),
UnnecessaryParenthesesBeforeTrailingLambdaRule(),
BlockCommentInitialStarAlignmentRule(),
DiscouragedCommentLocationRule(),
FunKeywordSpacingRule(),
FunctionTypeReferenceSpacingRule(),
Expand Down
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 a function that contains 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 c4ae318

Please sign in to comment.