diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f05039b3f5..c9ffaf8beb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,6 +85,12 @@ jobs: cp diktat-rules/src/main/resources/diktat-analysis.yml $(dirname $file) done next_snapshot_version=$(printf 'VERSION=${project.version}\n0\n' | mvn help:evaluate | grep '^VERSION' | cut -d= -f2) + # Update the version in `examples/maven/pom.xml` + # (which is not a part of the multi-module project). + for file in examples/maven/pom.xml + do + sed -i "s|\(\)[[:digit:]]\(\.[[:digit:]]\)\+-SNAPSHOT\(\)|\1${next_snapshot_version}\3|g" "${file}" || echo "File ${file} hasn't been updated (2nd sed pass)" + done echo "version=$next_snapshot_version" > info/buildSrc/gradle.properties - name: Create pull request uses: peter-evans/create-pull-request@v4 diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter2/kdoc/CommentsFormatting.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter2/kdoc/CommentsFormatting.kt index 800004bf8d..fb95cec57f 100644 --- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter2/kdoc/CommentsFormatting.kt +++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter2/kdoc/CommentsFormatting.kt @@ -233,11 +233,12 @@ class CommentsFormatting(configRules: List) : DiktatRule( } if (node.elementType == BLOCK_COMMENT && - (node - .text - .trim('/', '*') - .takeWhile { it == ' ' } - .length == configuration.maxSpacesInComment || + (node.isIndentStyleComment() || + node + .text + .trim('/', '*') + .takeWhile { it == ' ' } + .length == configuration.maxSpacesInComment || node .text .trim('/', '*') @@ -343,6 +344,30 @@ class CommentsFormatting(configRules: List) : DiktatRule( private fun ASTNode.isChildOfBlockOrClassBody(): Boolean = treeParent.elementType == BLOCK || treeParent.elementType == CLASS_BODY + /** + * Returns whether this block comment is a `indent`-style comment. + * + * `indent(1)` is a source code formatting utility for C-like languages. + * Historically, source code formatters are permitted to reformat and reflow + * the content of block comments, except for those comments which start with + * "/*-". + * + * See also: + * - [5.1.1 Block Comments](https://www.oracle.com/java/technologies/javase/codeconventions-comments.html) + * - [`indent(1)`](https://man.openbsd.org/indent.1) + * - [`style(9)`](https://www.freebsd.org/cgi/man.cgi?query=style&sektion=9) + * + * @return `true` if this block comment is a `indent`-style comment, `false` + * otherwise. + */ + private fun ASTNode.isIndentStyleComment(): Boolean { + require(elementType == BLOCK_COMMENT) { + "The elementType of this node is $elementType while $BLOCK_COMMENT expected" + } + + return text.matches(indentCommentMarker) + } + /** * [RuleConfiguration] for [CommentsFormatting] rule */ @@ -361,5 +386,10 @@ class CommentsFormatting(configRules: List) : DiktatRule( private const val APPROPRIATE_COMMENT_SPACES = 1 private const val MAX_SPACES = 1 const val NAME_ID = "kdoc-comments-codeblocks-formatting" + + /** + * "/*-" followed by anything but `*` or `-`. + */ + private val indentCommentMarker = Regex("""(?s)^\Q/*-\E[^*-].*?""") } } diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/indentation/Checkers.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/indentation/Checkers.kt index 5ab0f78d31..7d8457cdee 100644 --- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/indentation/Checkers.kt +++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/indentation/Checkers.kt @@ -213,18 +213,33 @@ internal class DotCallChecker(config: IndentationConfig) : CustomIndentationChec } || nextNode.isCommentBeforeDot()) && whiteSpace.parents.none { it.node.elementType == LONG_STRING_TEMPLATE_ENTRY } } ?.let { node -> + val indentIncrement = (if (configuration.extendedIndentBeforeDot) 2 else 1) * configuration.indentationSize if (node.isFromStringTemplate()) { return CheckResult.from(indentError.actual, indentError.expected + - (if (configuration.extendedIndentBeforeDot) 2 else 1) * configuration.indentationSize, true) + indentIncrement, true) } // we need to get indent before the first expression in calls chain - return CheckResult.from(indentError.actual, (whiteSpace.run { + /*- + * If the parent indent (the one before a `DOT_QUALIFIED_EXPRESSION` + * or a `SAFE_ACCESS_EXPRESSION`) is `null`, then use 0 as the + * fallback value. + * + * If `indentError.expected` is used as a fallback (pre-1.2.2 + * behaviour), this breaks chained dot-qualified or safe-access + * expressions (see #1336), e.g.: + * + * ```kotlin + * val a = first() + * .second() + * .third() + * ``` + */ + val parentIndent = whiteSpace.run { parents.takeWhile { it is KtDotQualifiedExpression || it is KtSafeQualifiedExpression }.lastOrNull() ?: this - } - .parentIndent() - ?: indentError.expected) + - (if (configuration.extendedIndentBeforeDot) 2 else 1) * configuration.indentationSize, true) + }.parentIndent() ?: 0 + val expectedIndent = parentIndent + indentIncrement + return CheckResult.from(indentError.actual, expectedIndent, true) } return null } diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter2/CommentsFormattingFixTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter2/CommentsFormattingFixTest.kt index 53d0f3a217..997e1ed293 100644 --- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter2/CommentsFormattingFixTest.kt +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter2/CommentsFormattingFixTest.kt @@ -1,5 +1,7 @@ package org.cqfn.diktat.ruleset.chapter2 +import org.cqfn.diktat.ruleset.chapter2.CommentsFormattingTest.Companion.indentStyleComment +import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestMixin.describe import org.cqfn.diktat.ruleset.rules.chapter2.kdoc.CommentsFormatting import org.cqfn.diktat.util.FixTestBase @@ -7,9 +9,13 @@ import generated.WarningNames.COMMENT_WHITE_SPACE import generated.WarningNames.FIRST_COMMENT_NO_BLANK_LINE import generated.WarningNames.IF_ELSE_COMMENTS import generated.WarningNames.WRONG_NEWLINES_AROUND_KDOC +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +import java.nio.file.Path class CommentsFormattingFixTest : FixTestBase("test/paragraph2/kdoc/", ::CommentsFormatting) { @Test @@ -40,4 +46,16 @@ class CommentsFormattingFixTest : FixTestBase("test/paragraph2/kdoc/", ::Comment fun `regression - should not insert newline before the first comment in a file`() { fixAndCompare("NoPackageNoImportExpected.kt", "NoPackageNoImportTest.kt") } + + /** + * `indent(1)` and `style(9)` style comments. + */ + @Test + @Tag(COMMENT_WHITE_SPACE) + fun `indent-style header in a block comment should be preserved`(@TempDir tempDir: Path) { + val lintResult = fixAndCompareContent(indentStyleComment, tempDir = tempDir) + assertThat(lintResult.actualContent) + .describedAs("lint result for ${indentStyleComment.describe()}") + .isEqualTo(lintResult.expectedContent) + } } diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter2/CommentsFormattingTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter2/CommentsFormattingTest.kt index 758f87243e..8b87bbc735 100644 --- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter2/CommentsFormattingTest.kt +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter2/CommentsFormattingTest.kt @@ -8,6 +8,7 @@ import org.cqfn.diktat.util.LintTestBase import com.pinterest.ktlint.core.LintError import generated.WarningNames +import org.intellij.lang.annotations.Language import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test @@ -491,4 +492,35 @@ class CommentsFormattingTest : LintTestBase(::CommentsFormatting) { lintMethod(code) } + + /** + * `indent(1)` and `style(9)` style comments. + */ + @Test + @Tag(WarningNames.COMMENT_WHITE_SPACE) + fun `indent-style header in a block comment should produce no warnings`() = + lintMethod(indentStyleComment) + + internal companion object { + @Language("kotlin") + internal val indentStyleComment = """ + |/*- + | * This is an indent-style comment, and it's different from regular + | * block comments in C-like languages. + | * + | * Code formatters should not wrap or reflow its content, so you can + | * safely insert code fragments: + | * + | * ``` + | * int i = 42; + | * ``` + | * + | * or ASCII diagrams: + | * + | * +-----+ + | * | Box | + | * +-----+ + | */ + """.trimMargin() + } } diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleFixTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleFixTest.kt index 2117f95773..9e744752b9 100644 --- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleFixTest.kt +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleFixTest.kt @@ -8,6 +8,7 @@ import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestMixin.assertNo import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestMixin.describe import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestMixin.extendedIndent import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestMixin.withCustomParameters +import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.dotQualifiedExpressions import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.expressionBodyFunctions import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.expressionsWrappedAfterOperator import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.parenthesesSurroundedInfixExpressions @@ -248,4 +249,38 @@ class IndentationRuleFixTest : FixTestBase("test/paragraph3/indentation", rulesConfigList = customConfig.asRulesConfigList()) } } + + /** + * See [#1336](https://github.com/saveourtool/diktat/issues/1336). + */ + @Nested + @TestMethodOrder(DisplayName::class) + inner class `Dot- and safe-qualified expressions` { + @ParameterizedTest(name = "extendedIndentBeforeDot = {0}") + @ValueSource(booleans = [false, true]) + @Tag(WarningNames.WRONG_INDENTATION) + fun `should be properly indented`(extendedIndentBeforeDot: Boolean, @TempDir tempDir: Path) { + val defaultConfig = IndentationConfig("newlineAtEnd" to false) + val customConfig = defaultConfig.withCustomParameters("extendedIndentBeforeDot" to extendedIndentBeforeDot) + + lintMultipleMethods( + dotQualifiedExpressions[extendedIndentBeforeDot].assertNotNull(), + tempDir = tempDir, + rulesConfigList = customConfig.asRulesConfigList()) + } + + @ParameterizedTest(name = "extendedIndentBeforeDot = {0}") + @ValueSource(booleans = [false, true]) + @Tag(WarningNames.WRONG_INDENTATION) + fun `should be reformatted if mis-indented`(extendedIndentBeforeDot: Boolean, @TempDir tempDir: Path) { + val defaultConfig = IndentationConfig("newlineAtEnd" to false) + val customConfig = defaultConfig.withCustomParameters("extendedIndentBeforeDot" to extendedIndentBeforeDot) + + lintMultipleMethods( + actualContent = dotQualifiedExpressions[!extendedIndentBeforeDot].assertNotNull(), + expectedContent = dotQualifiedExpressions[extendedIndentBeforeDot].assertNotNull(), + tempDir = tempDir, + rulesConfigList = customConfig.asRulesConfigList()) + } + } } diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleTestResources.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleTestResources.kt index 9cacd40b54..277b96917f 100644 --- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleTestResources.kt +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleTestResources.kt @@ -979,6 +979,161 @@ internal object IndentationRuleTestResources { false to parenthesesSurroundedInfixExpressionsSingleIndent, true to parenthesesSurroundedInfixExpressionsContinuationIndent) + /** + * Dot-qualified and safe-access expressions, single indent + * (`extendedIndentBeforeDot` is **off**). + * + * When adding new code fragments to this list, be sure to also add their + * counterparts (preserving order) to + * [dotQualifiedExpressionsContinuationIndent]. + * + * See [#1336](https://github.com/saveourtool/diktat/issues/1336). + * + * @see dotQualifiedExpressionsContinuationIndent + */ + @Language("kotlin") + val dotQualifiedExpressionsSingleIndent = arrayOf( + """ + |fun LocalDateTime.updateTime( + | hour: Int? = null, + | minute: Int? = null, + | second: Int? = null, + |): LocalDateTime = withHour(hour ?: getHour()) + | .withMinute(minute ?: getMinute()) + | .withSecond(second ?: getSecond()) + """.trimMargin(), + + """ + |fun f() { + | first() + | .second() + | .third() + |} + """.trimMargin(), + + """ + |val a = first() + | .second() + | .third() + """.trimMargin(), + + """ + |val b = first() + | ?.second() + | ?.third() + """.trimMargin(), + + """ + |fun f1() = first() + | .second() + | .third() + """.trimMargin(), + + """ + |fun f2() = + | first() + | .second() + | .third() + """.trimMargin(), + + """ + |fun f3() = g(first() + | .second() + | .third() + | .fourth()) + """.trimMargin(), + + """ + |fun f4() = g( + | first() + | .second() + | .third() + | .fourth()) + """.trimMargin(), + ) + + /** + * Dot-qualified and safe-access expressions, continuation indent + * (`extendedIndentBeforeDot` is **on**). + * + * When adding new code fragments to this list, be sure to also add their + * counterparts (preserving order) to + * [dotQualifiedExpressionsSingleIndent]. + * + * See [#1336](https://github.com/saveourtool/diktat/issues/1336). + * + * @see dotQualifiedExpressionsSingleIndent + */ + @Language("kotlin") + val dotQualifiedExpressionsContinuationIndent = arrayOf( + """ + |fun LocalDateTime.updateTime( + | hour: Int? = null, + | minute: Int? = null, + | second: Int? = null, + |): LocalDateTime = withHour(hour ?: getHour()) + | .withMinute(minute ?: getMinute()) + | .withSecond(second ?: getSecond()) + """.trimMargin(), + + """ + |fun f() { + | first() + | .second() + | .third() + |} + """.trimMargin(), + + """ + |val a = first() + | .second() + | .third() + """.trimMargin(), + + """ + |val b = first() + | ?.second() + | ?.third() + """.trimMargin(), + + """ + |fun f1() = first() + | .second() + | .third() + """.trimMargin(), + + """ + |fun f2() = + | first() + | .second() + | .third() + """.trimMargin(), + + """ + |fun f3() = g(first() + | .second() + | .third() + | .fourth()) + """.trimMargin(), + + """ + |fun f4() = g( + | first() + | .second() + | .third() + | .fourth()) + """.trimMargin(), + ) + + /** + * Dot-qualified and safe-access expressions. + * + * See [#1336](https://github.com/saveourtool/diktat/issues/1336). + */ + val dotQualifiedExpressions = mapOf( + false to dotQualifiedExpressionsSingleIndent, + true to dotQualifiedExpressionsContinuationIndent) + @Language("kotlin") @Suppress("COMMENT_WHITE_SPACE") private val ifExpressionsSingleIndent = arrayOf( diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleWarnTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleWarnTest.kt index 2892cfde57..fdda961e50 100644 --- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleWarnTest.kt +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleWarnTest.kt @@ -8,6 +8,7 @@ import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestMixin.asSequen import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestMixin.assertNotNull import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestMixin.describe import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestMixin.withCustomParameters +import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.dotQualifiedExpressions import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.expressionBodyFunctions import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.expressionsWrappedAfterOperator import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.ifExpressions @@ -15,6 +16,7 @@ import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.pare import org.cqfn.diktat.ruleset.constants.Warnings.WRONG_INDENTATION import org.cqfn.diktat.ruleset.rules.chapter3.files.IndentationRule import org.cqfn.diktat.util.LintTestBase + import com.pinterest.ktlint.core.LintError import generated.WarningNames import org.assertj.core.api.AbstractSoftAssertions @@ -29,6 +31,7 @@ import org.junit.jupiter.api.TestMethodOrder import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource import org.opentest4j.MultipleFailuresError + import java.util.function.Consumer @Suppress("LargeClass") @@ -936,6 +939,48 @@ class IndentationRuleWarnTest : LintTestBase(::IndentationRule) { } } + /** + * See [#1336](https://github.com/saveourtool/diktat/issues/1336). + */ + @Nested + @TestMethodOrder(DisplayName::class) + inner class `Dot- and safe-qualified expressions` { + @ParameterizedTest(name = "extendedIndentBeforeDot = {0}") + @ValueSource(booleans = [false, true]) + @Tag(WarningNames.WRONG_INDENTATION) + fun `should be properly indented`(extendedIndentBeforeDot: Boolean) { + val defaultConfig = IndentationConfig("newlineAtEnd" to false) + val customConfig = defaultConfig.withCustomParameters("extendedIndentBeforeDot" to extendedIndentBeforeDot) + + lintMultipleMethods( + dotQualifiedExpressions[extendedIndentBeforeDot].assertNotNull(), + lintErrors = emptyArray(), + customConfig.asRulesConfigList() + ) + } + + @ParameterizedTest(name = "extendedIndentBeforeDot = {0}") + @ValueSource(booleans = [false, true]) + @Tag(WarningNames.WRONG_INDENTATION) + fun `should be reported if mis-indented`(extendedIndentBeforeDot: Boolean) { + val defaultConfig = IndentationConfig("newlineAtEnd" to false) + val customConfig = defaultConfig.withCustomParameters("extendedIndentBeforeDot" to extendedIndentBeforeDot) + + assertSoftly { softly -> + dotQualifiedExpressions[!extendedIndentBeforeDot].assertNotNull().forEach { code -> + softly.assertThat(lintResult(code, customConfig.asRulesConfigList())) + .describedAs("lint result for ${code.describe()}") + .isNotEmpty + .hasSizeBetween(2, 3).allSatisfy(Consumer { lintError -> + assertThat(lintError.ruleId).describedAs("ruleId").isEqualTo(ruleId) + assertThat(lintError.canBeAutoCorrected).describedAs("canBeAutoCorrected").isTrue + assertThat(lintError.detail).matches(warnTextRegex) + }) + } + } + } + } + @Nested @TestMethodOrder(DisplayName::class) inner class `If expressions` { diff --git a/examples/maven/pom.xml b/examples/maven/pom.xml index de15d4fc23..d47946c445 100644 --- a/examples/maven/pom.xml +++ b/examples/maven/pom.xml @@ -5,7 +5,7 @@ org.cqfn.diktat diktat-examples-maven pom - 1.2.1-SNAPSHOT + 1.2.2-SNAPSHOT 1.2.1 diff --git a/pom.xml b/pom.xml index 00b18b7101..7f3f124069 100644 --- a/pom.xml +++ b/pom.xml @@ -254,7 +254,7 @@ org.apache.maven.plugins maven-assembly-plugin - 3.4.0 + 3.4.1