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 92bc9dd498..c5f664b9b9 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 @@ -4,12 +4,23 @@ import org.cqfn.diktat.common.config.rules.RulesConfig import org.cqfn.diktat.ruleset.constants.Warnings.WRONG_INDENTATION import org.cqfn.diktat.ruleset.rules.DIKTAT_RULE_SET_ID import org.cqfn.diktat.ruleset.rules.chapter3.files.IndentationRule +import org.cqfn.diktat.ruleset.utils.indentation.IndentationConfig import org.cqfn.diktat.util.LintTestBase import com.pinterest.ktlint.core.LintError import generated.WarningNames +import org.assertj.core.api.AbstractSoftAssertions +import org.assertj.core.api.AssertionErrorCollector +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.SoftAssertions.assertSoftly +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.Assumptions.assumeTrue import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test +import org.opentest4j.MultipleFailuresError +import java.util.function.Consumer + +import java.lang.Boolean.getBoolean as getBooleanProperty @Suppress("LargeClass") class IndentationRuleWarnTest : LintTestBase(::IndentationRule) { @@ -697,5 +708,340 @@ class IndentationRuleWarnTest : LintTestBase(::IndentationRule) { ) } + /** + * See [#1330](https://github.com/saveourtool/diktat/issues/1330). + * + * @see expressionBodyFunctionsContinuationIndent + */ + @Language("kotlin") + private val expressionBodyFunctionsSingleIndent: Array = arrayOf( + """ + |@Test + |fun `checking that suppression with ignore everything works`() { + | val code = + | ""${'"'} + | @Suppress("diktat") + | fun foo() { + | val a = 1 + | } + | ""${'"'}.trimIndent() + | lintMethod(code) + |} + """.trimMargin(), + + """ + |val currentTime: Time + | get() = + | with(currentDateTime) { + | Time(hour = hour, minute = minute, second = second) + | } + """.trimMargin(), + + """ + |fun formatDateByPattern(date: String, pattern: String = "ddMMyy"): String = + | DateTimeFormatter.ofPattern(pattern).format(LocalDate.parse(date)) + """.trimMargin(), + + """ + |private fun createLayoutParams(): WindowManager.LayoutParams = + | WindowManager.LayoutParams().apply { /* ... */ } + """.trimMargin(), + + """ + |val offsetDelta = + | if (shimmerAnimationType != ShimmerAnimationType.FADE) translateAnim.dp + | else 2000.dp + """.trimMargin(), + + """ + |private fun lerp(start: Float, stop: Float, fraction: Float): Float = + | (1 - fraction) * start + fraction * stop + """.trimMargin(), + ) + + /** + * See [#1330](https://github.com/saveourtool/diktat/issues/1330). + * + * @see expressionBodyFunctionsSingleIndent + */ + @Language("kotlin") + private val expressionBodyFunctionsContinuationIndent: Array = arrayOf( + """ + |@Test + |fun `checking that suppression with ignore everything works`() { + | val code = + | ""${'"'} + | @Suppress("diktat") + | fun foo() { + | val a = 1 + | } + | ""${'"'}.trimIndent() + | lintMethod(code) + |} + """.trimMargin(), + + """ + |val currentTime: Time + | get() = + | with(currentDateTime) { + | Time(hour = hour, minute = minute, second = second) + | } + """.trimMargin(), + + """ + |fun formatDateByPattern(date: String, pattern: String = "ddMMyy"): String = + | DateTimeFormatter.ofPattern(pattern).format(LocalDate.parse(date)) + """.trimMargin(), + + """ + |private fun createLayoutParams(): WindowManager.LayoutParams = + | WindowManager.LayoutParams().apply { /* ... */ } + """.trimMargin(), + + """ + |val offsetDelta = + | if (shimmerAnimationType != ShimmerAnimationType.FADE) translateAnim.dp + | else 2000.dp + """.trimMargin(), + + """ + |private fun lerp(start: Float, stop: Float, fraction: Float): Float = + | (1 - fraction) * start + fraction * stop + """.trimMargin(), + ) + + /** + * See [#1330](https://github.com/saveourtool/diktat/issues/1330). + */ + @Test + fun `expression body functions should be properly indented (extendedIndentAfterOperators = true)`() { + assumeTrue(true || testsCanBeMuted) { + "Skipping a known-to-fail test" + } + + val defaultConfig = IndentationConfig("newlineAtEnd" to false) + val customConfig = defaultConfig.withCustomParameters("extendedIndentAfterOperators" to true) + + lintMultipleMethods( + expressionBodyFunctionsContinuationIndent, + lintErrors = emptyArray(), + customConfig.asRulesConfigList() + ) + } + + /** + * See [#1330](https://github.com/saveourtool/diktat/issues/1330). + */ + @Test + fun `expression body functions should be properly indented (extendedIndentAfterOperators = false)`() { + assumeTrue(true || testsCanBeMuted) { + "Skipping a known-to-fail test" + } + + val defaultConfig = IndentationConfig("newlineAtEnd" to false) + val customConfig = defaultConfig.withCustomParameters("extendedIndentAfterOperators" to false) + + lintMultipleMethods( + expressionBodyFunctionsSingleIndent, + lintErrors = emptyArray(), + customConfig.asRulesConfigList() + ) + } + + /** + * See [#1330](https://github.com/saveourtool/diktat/issues/1330). + */ + @Test + fun `expression body functions should be reported if mis-indented (extendedIndentAfterOperators = true)`() { + assumeTrue(true || testsCanBeMuted) { + "Skipping a known-to-fail test" + } + + val defaultConfig = IndentationConfig("newlineAtEnd" to false) + val customConfig = defaultConfig.withCustomParameters("extendedIndentAfterOperators" to true) + + assertSoftly { softly -> + expressionBodyFunctionsSingleIndent.forEach { code -> + softly.assertThat(lintResult(code, customConfig.asRulesConfigList())) + .describedAs("lint result for \"$code\"") + .isNotEmpty + .hasSizeBetween(1, 3).allSatisfy(Consumer { lintError -> + assertThat(lintError.ruleId).describedAs("ruleId").isEqualTo(ruleId) + assertThat(lintError.canBeAutoCorrected).describedAs("canBeAutoCorrected").isTrue + assertThat(lintError.detail).matches(WARN_TEXT_REGEX) + }) + } + } + } + + /** + * See [#1330](https://github.com/saveourtool/diktat/issues/1330). + */ + @Test + fun `expression body functions should be reported if mis-indented (extendedIndentAfterOperators = false)`() { + assumeTrue(true || testsCanBeMuted) { + "Skipping a known-to-fail test" + } + + val defaultConfig = IndentationConfig("newlineAtEnd" to false) + val customConfig = defaultConfig.withCustomParameters("extendedIndentAfterOperators" to false) + + assertSoftly { softly -> + expressionBodyFunctionsContinuationIndent.forEach { code -> + softly.assertThat(lintResult(code, customConfig.asRulesConfigList())) + .describedAs("lint result for \"$code\"") + .isNotEmpty + .hasSizeBetween(1, 3).allSatisfy(Consumer { lintError -> + assertThat(lintError.ruleId).describedAs("ruleId").isEqualTo(ruleId) + assertThat(lintError.canBeAutoCorrected).describedAs("canBeAutoCorrected").isTrue + assertThat(lintError.detail).matches(WARN_TEXT_REGEX) + }) + } + } + } + + /** + * @see WARN_TEXT_REGEX + */ private fun warnText(expected: Int, actual: Int) = "${WRONG_INDENTATION.warnText()} expected $expected but was $actual" + + /** + * Creates an `IndentationConfig` from zero or more + * [config entries][configEntries]. Invoke without arguments to create a + * default `IndentationConfig`. + * + * @see [IndentationConfig] + */ + @Suppress("TestFunctionName") + private fun IndentationConfig(vararg configEntries: Pair): IndentationConfig = + IndentationConfig(mapOf(*configEntries).mapValues(Any::toString)) + + private fun IndentationConfig.withCustomParameters(vararg configEntries: Pair): Map = + mutableMapOf( + "alignedParameters" to "$alignedParameters", + "indentationSize" to "$indentationSize", + "newlineAtEnd" to "$newlineAtEnd", + "extendedIndentOfParameters" to "$extendedIndentOfParameters", + "extendedIndentAfterOperators" to "$extendedIndentAfterOperators", + "extendedIndentBeforeDot" to "$extendedIndentBeforeDot", + ).apply { + configEntries.forEach { (key, value) -> + this[key] = value.toString() + } + } + + /** + * Converts this map to a list containing a single [RulesConfig]. + */ + private fun Map.asRulesConfigList(): List { + return listOf( + RulesConfig( + name = WRONG_INDENTATION.name, + enabled = true, + configuration = this + ) + ) + } + + /** + * When within a scope of an `AbstractSoftAssertions`, collects failures + * thrown by [block], correctly accumulating multiple failures from nested + * soft assertions (if any). + * + * @see AssertionErrorCollector.collectAssertionError + */ + private fun AbstractSoftAssertions.collectAssertionErrors(block: () -> Unit) = + try { + block() + } catch (mfe: MultipleFailuresError) { + mfe.failures.forEach { failure -> + when (failure) { + is AssertionError -> collectAssertionError(failure) + else -> fail(failure.toString(), failure) + } + } + } catch (ae: AssertionError) { + collectAssertionError(ae) + } catch (t: Throwable) { + fail(t.toString(), t) + } + + /** + * Similar to [lintMethod], but can be invoked from a scope of + * `AbstractSoftAssertions` in order to accumulate test results from linting + * _multiple_ code fragments. + * + * @param rulesConfigList the list of rules which can optionally override + * the [default value][LintTestBase.rulesConfigList]. + * @see lintMethod + */ + private fun AbstractSoftAssertions.lintMethodSoftly( + @Language("kotlin") code: String, + vararg lintErrors: LintError, + rulesConfigList: List? = null, + fileName: String? = null + ) { + require(code.isNotBlank()) { + "code is blank" + } + + collectAssertionErrors { + lintMethod(code, lintErrors = lintErrors, rulesConfigList, fileName) + } + } + + /** + * Tests multiple code [fragments] using the same + * [rule configuration][rulesConfigList]. + * + * All code fragments get concatenated together and the resulting, bigger + * fragment gets tested, too. + * + * @param rulesConfigList the list of rules which can optionally override + * the [default value][LintTestBase.rulesConfigList]. + * @see lintMethod + */ + private fun lintMultipleMethods( + @Language("kotlin") fragments: Array, + vararg lintErrors: LintError, + rulesConfigList: List? = null, + fileName: String? = null + ) { + require(fragments.isNotEmpty()) { + "code fragments is an empty array" + } + + assertSoftly { softly -> + sequence { + yieldAll(fragments.asSequence()) + + /* + * All fragments concatenated. + */ + yield(fragments.joinToString(separator = "\n\n")) + }.forEach { fragment -> + softly.lintMethodSoftly( + fragment, + lintErrors = lintErrors, + rulesConfigList, + fileName + ) + } + } + } + + /** + * @return `true` if known-to-fail unit tests can be muted on the CI server. + */ + private val testsCanBeMuted: Boolean + get() = + getBooleanProperty("tests.can.be.muted") + + companion object { + /** + * @see warnText + */ + @Language("RegExp") + private val WARN_TEXT_REGEX = "^\\Q${WRONG_INDENTATION.warnText()}\\E expected \\d+ but was \\d+$" + } } diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/LintTestBase.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/LintTestBase.kt index 512e8c17cf..f066654ce2 100644 --- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/LintTestBase.kt +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/LintTestBase.kt @@ -22,15 +22,36 @@ open class LintTestBase(private val ruleSupplier: (rulesConfigList: List? = null, fileName: String? = null ) { + lintResult(code, rulesConfigList, fileName) + .assertEquals(*lintErrors) + } + + /** + * Lints the [code] and returns the errors collected, but (unlike + * [lintMethod]) doesn't make any assertions. + * + * @param code the code to check. + * @param rulesConfigList an optional override for `this.rulesConfigList`. + * @param fileName an optional override for the file name. + * @return the list of lint errors. + * @see lintMethod + */ + @OptIn(FeatureInAlphaState::class) + protected fun lintResult( + @Language("kotlin") code: String, + rulesConfigList: List? = null, + fileName: String? = null + ): List { val actualFileName = fileName ?: TEST_FILE_NAME - val res: MutableList = mutableListOf() + val lintErrors = mutableListOf() + KtLint.lint( KtLint.ExperimentalParams( fileName = actualFileName, @@ -38,10 +59,11 @@ open class LintTestBase(private val ruleSupplier: (rulesConfigList: List res.add(lintError) }, + cb = { lintError, _ -> lintErrors += lintError }, userData = mapOf("file_path" to actualFileName) ) ) - res.assertEquals(*lintErrors) + + return lintErrors } }