diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationRule.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationRule.kt index ce1a828f65..d53f145e09 100644 --- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationRule.kt +++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationRule.kt @@ -2,6 +2,8 @@ * Main logic of indentation including Rule and utility classes and methods. */ +@file:Suppress("FILE_UNORDERED_IMPORTS")// False positives, see #1494. + package org.cqfn.diktat.ruleset.rules.chapter3.files import org.cqfn.diktat.common.config.rules.RulesConfig @@ -79,11 +81,13 @@ import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf import org.jetbrains.kotlin.psi.psiUtil.startOffset -import java.util.ArrayDeque as Stack - +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract import kotlin.math.abs import kotlin.reflect.KCallable +import java.util.ArrayDeque as Stack + /** * Rule that checks indentation. The following general rules are checked: * 1. Only spaces should be used each indentation is equal to 4 spaces @@ -198,21 +202,6 @@ class IndentationRule(configRules: List) : DiktatRule( } } - private fun isCloseAndOpenQuoterOffset(nodeWhiteSpace: ASTNode, expectedIndent: Int): Boolean { - val nextNode = nodeWhiteSpace.treeNext - if (nextNode.elementType == VALUE_ARGUMENT) { - val nextNodeDot = getNextDotExpression(nextNode) - nextNodeDot?.getFirstChildWithType(STRING_TEMPLATE)?.let { - if (it.getAllChildrenWithType(LITERAL_STRING_TEMPLATE_ENTRY).size > 1) { - val closingQuote = it.getFirstChildWithType(CLOSING_QUOTE)?.treePrev?.text - ?.length ?: -1 - return expectedIndent == closingQuote - } - } - } - return true - } - @Suppress("ForbiddenComment") private fun IndentContext.visitWhiteSpace(astNode: ASTNode) { require(astNode.isMultilineWhitespace()) { @@ -242,10 +231,10 @@ class IndentationRule(configRules: List) : DiktatRule( addException(astNode.treeParent, abs(indentError.expected - indentError.actual), false) } - val difOffsetCloseAndOpenQuote = isCloseAndOpenQuoterOffset(astNode, indentError.actual) + val alignedOpeningAndClosingQuotes = hasAlignedOpeningAndClosingQuotes(astNode, indentError.actual) - if ((checkResult?.isCorrect != true && expectedIndent != indentError.actual) || !difOffsetCloseAndOpenQuote) { - val warnText = if (!difOffsetCloseAndOpenQuote) { + if ((checkResult?.isCorrect != true && expectedIndent != indentError.actual) || !alignedOpeningAndClosingQuotes) { + val warnText = if (!alignedOpeningAndClosingQuotes) { "the same number of indents to the opening and closing quotes was expected" } else { "expected $expectedIndent but was ${indentError.actual}" @@ -268,7 +257,7 @@ class IndentationRule(configRules: List) : DiktatRule( expectedIndent: Int, actualIndent: Int ) { - val nextNodeDot = getNextDotExpression(whiteSpace.node.treeNext) + val nextNodeDot = whiteSpace.node.treeNext.getNextDotExpression() if (nextNodeDot != null && nextNodeDot.elementType == DOT_QUALIFIED_EXPRESSION && nextNodeDot.firstChildNode.elementType == STRING_TEMPLATE && @@ -361,12 +350,6 @@ class IndentationRule(configRules: List) : DiktatRule( } } - private fun getNextDotExpression(node: ASTNode) = if (node.elementType == DOT_QUALIFIED_EXPRESSION) { - node - } else { - node.getFirstChildWithType(DOT_QUALIFIED_EXPRESSION) - } - /** * Modifies [templateEntry] by correcting its indentation level. * @@ -734,11 +717,30 @@ class IndentationRule(configRules: List) : DiktatRule( private fun ASTNode.isMultilineWhitespace(): Boolean = elementType == WHITE_SPACE && textContains(NEWLINE) + @OptIn(ExperimentalContracts::class) + private fun ASTNode?.isMultilineStringTemplate(): Boolean { + contract { + returns(true) implies (this@isMultilineStringTemplate != null) + } + + this ?: return false + + return elementType == STRING_TEMPLATE && + getAllChildrenWithType(LITERAL_STRING_TEMPLATE_ENTRY).any { entry -> + entry.textContains(NEWLINE) + } + } + /** * @return `true` if this is a [String.trimIndent] or [String.trimMargin] * call, `false` otherwise. */ + @OptIn(ExperimentalContracts::class) private fun ASTNode?.isTrimIndentOrMarginCall(): Boolean { + contract { + returns(true) implies (this@isTrimIndentOrMarginCall != null) + } + this ?: return false require(elementType == CALL_EXPRESSION) { @@ -758,6 +760,12 @@ class IndentationRule(configRules: List) : DiktatRule( return identifier.text in knownTrimFunctionPatterns } + private fun ASTNode.getNextDotExpression(): ASTNode? = + when (elementType) { + DOT_QUALIFIED_EXPRESSION -> this + else -> getFirstChildWithType(DOT_QUALIFIED_EXPRESSION) + } + /** * @return the matching closing brace type for this opening brace type, * or vice versa. @@ -791,5 +799,64 @@ class IndentationRule(configRules: List) : DiktatRule( this > 0 -> this else -> 0 } + + /** + * Processes fragments like: + * + * ```kotlin + * f( + * """ + * |foobar + * """.trimMargin() + * ) + * ``` + * + * @param whitespace the whitespace node between an [LPAR] and the + * `trimIndent()`- or `trimMargin()`- terminated string template, which is + * an effective argument of a function call. The string template is + * expected to begin on a separate line (otherwise, there'll be no + * whitespace in-between). + * @return `true` if the opening and the closing quotes of the string + * template are aligned, `false` otherwise. + */ + private fun hasAlignedOpeningAndClosingQuotes(whitespace: ASTNode, expectedIndent: Int): Boolean { + require(whitespace.isMultilineWhitespace()) { + "The node is $whitespace while a multi-line $WHITE_SPACE expected" + } + + /* + * Here, we expect that `nextNode` is a VALUE_ARGUMENT which contains + * the dot-qualified expression (`STRING_TEMPLATE.trimIndent()` or + * `STRING_TEMPLATE.trimMargin()`). + */ + val nextFunctionArgument = whitespace.treeNext + if (nextFunctionArgument.elementType == VALUE_ARGUMENT) { + val memberOrExtensionCall = nextFunctionArgument.getNextDotExpression() + + /* + * Limit allowed member or extension calls to `trimIndent()` and + * `trimMargin()`. + */ + if (memberOrExtensionCall != null && + memberOrExtensionCall.getFirstChildWithType(CALL_EXPRESSION).isTrimIndentOrMarginCall()) { + val stringTemplate = memberOrExtensionCall.getFirstChildWithType(STRING_TEMPLATE) + + /* + * Limit the logic to multi-line string templates only (the + * opening and closing quotes of a single-line template are, + * obviously, always mis-aligned). + */ + if (stringTemplate != null && stringTemplate.isMultilineStringTemplate()) { + val closingQuoteIndent = stringTemplate.getFirstChildWithType(CLOSING_QUOTE) + ?.treePrev + ?.text + ?.length ?: -1 + return expectedIndent == closingQuoteIndent + } + } + } + + return true + } } } 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 b6a24f9204..75cfdd41be 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 @@ -1,3 +1,5 @@ +@file:Suppress("FILE_UNORDERED_IMPORTS")// False positives, see #1494. + package org.cqfn.diktat.ruleset.chapter3.spaces import org.cqfn.diktat.common.config.rules.RulesConfig @@ -10,12 +12,20 @@ import org.cqfn.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXT import org.cqfn.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_FOR_EXPRESSION_BODIES import org.cqfn.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_OF_PARAMETERS import org.cqfn.diktat.ruleset.utils.indentation.IndentationConfig.Companion.NEWLINE_AT_END +import org.cqfn.diktat.test.framework.processing.FileComparisonResult import org.cqfn.diktat.util.FixTestBase import generated.WarningNames +import org.assertj.core.api.Assertions.assertThat +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestMethodOrder +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path + +import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationConfigFactory as IndentationConfig /** * Legacy indentation tests. @@ -64,9 +74,167 @@ class IndentationRuleFixTest : FixTestBase("test/paragraph3/indentation", fixAndCompare("ConstructorExpected.kt", "ConstructorTest.kt") } - @Test - @Tag(WarningNames.WRONG_INDENTATION) - fun `multiline string`() { - fixAndCompare("MultilionStringExpected.kt", "MultilionStringTest.kt") + @Nested + @TestMethodOrder(NaturalDisplayName::class) + inner class `Multi-line string literals` { + /** + * Correctly-indented opening quotation mark, incorrectly-indented + * closing quotation mark. + */ + @Test + @Tag(WarningNames.WRONG_INDENTATION) + @Suppress("LOCAL_VARIABLE_EARLY_DECLARATION") // False positives + fun `case 1 - mis-aligned opening and closing quotes`(@TempDir tempDir: Path) { + val actualCode = """ + |fun f() { + | g( + | ""${'"'} + | |val q = 1 + | | + | ""${'"'}.trimMargin(), + | arg1 = "arg1" + | ) + |} + """.trimMargin() + + val expectedCode = """ + |fun f() { + | g( + | ""${'"'} + | |val q = 1 + | | + | ""${'"'}.trimMargin(), + | arg1 = "arg1" + | ) + |} + """.trimMargin() + + val lintResult = fixAndCompareContent(actualCode, expectedCode, tempDir) + assertThat(lintResult.actualContent) + .describedAs("lint result for ${actualCode.describe()}") + .isEqualTo(lintResult.expectedContent) + } + + /** + * Both the opening and the closing quotation marks are incorrectly + * indented (indentation level is less than needed). + */ + @Test + @Tag(WarningNames.WRONG_INDENTATION) + @Suppress("LOCAL_VARIABLE_EARLY_DECLARATION") // False positives + fun `case 2`(@TempDir tempDir: Path) { + val actualCode = """ + |fun f() { + | g( + | ""${'"'} + | |val q = 1 + | | + | ""${'"'}.trimMargin(), + | arg1 = "arg1" + | ) + |} + """.trimMargin() + + val expectedCode = """ + |fun f() { + | g( + | ""${'"'} + | |val q = 1 + | | + | ""${'"'}.trimMargin(), + | arg1 = "arg1" + | ) + |} + """.trimMargin() + + val lintResult = fixAndCompareContent(actualCode, expectedCode, tempDir) + assertThat(lintResult.actualContent) + .describedAs("lint result for ${actualCode.describe()}") + .isEqualTo(lintResult.expectedContent) + } + + /** + * Both the opening and the closing quotation marks are incorrectly + * indented (indentation level is greater than needed). + */ + @Test + @Tag(WarningNames.WRONG_INDENTATION) + @Suppress("LOCAL_VARIABLE_EARLY_DECLARATION") // False positives + fun `case 3`(@TempDir tempDir: Path) { + val actualCode = """ + |fun f() { + | g( + | ""${'"'} + | |val q = 1 + | | + | ""${'"'}.trimMargin(), + | arg1 = "arg1" + | ) + |} + """.trimMargin() + + val expectedCode = """ + |fun f() { + | g( + | ""${'"'} + | |val q = 1 + | | + | ""${'"'}.trimMargin(), + | arg1 = "arg1" + | ) + |} + """.trimMargin() + + val lintResult = fixAndCompareContent(actualCode, expectedCode, tempDir) + assertThat(lintResult.actualContent) + .describedAs("lint result for ${actualCode.describe()}") + .isEqualTo(lintResult.expectedContent) + } + + /** + * Both the opening and the closing quotation marks are incorrectly + * indented and misaligned. + */ + @Test + @Tag(WarningNames.WRONG_INDENTATION) + @Suppress("LOCAL_VARIABLE_EARLY_DECLARATION") // False positives + fun `case 4 - mis-aligned opening and closing quotes`(@TempDir tempDir: Path) { + val actualCode = """ + |fun f() { + | g( + | ""${'"'} + | |val q = 1 + | | + | ""${'"'}.trimMargin(), + | arg1 = "arg1" + | ) + |} + """.trimMargin() + + val expectedCode = """ + |fun f() { + | g( + | ""${'"'} + | |val q = 1 + | | + | ""${'"'}.trimMargin(), + | arg1 = "arg1" + | ) + |} + """.trimMargin() + + val lintResult = fixAndCompareContent(actualCode, expectedCode, tempDir) + assertThat(lintResult.actualContent) + .describedAs("lint result for ${actualCode.describe()}") + .isEqualTo(lintResult.expectedContent) + } + + private fun fixAndCompareContent(@Language("kotlin") actualCode: String, + @Language("kotlin") expectedCode: String, + tempDir: Path + ): FileComparisonResult { + val config = IndentationConfig(NEWLINE_AT_END to false).withCustomParameters().asRulesConfigList() + return fixAndCompareContent(actualCode, expectedCode, tempDir, config) + } } } diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleTest.kt index 5c8fb9ec64..b7931600f0 100644 --- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleTest.kt +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleTest.kt @@ -259,16 +259,64 @@ class IndentationRuleTest { @TestMethodOrder(NaturalDisplayName::class) inner class `String templates` { /** + * No message like + * + * > only spaces are allowed for indentation and each indentation should + * > equal to 4 spaces (tabs are not allowed): the same number of + * > indents to the opening and closing quotes was expected + * + * should be reported. + * + * See [#1490](https://github.com/saveourtool/diktat/issues/1490). + */ + @IndentationTest(IndentedSourceCode( + """ + val value = f( + "text ${'$'}variable text".isEmpty() + ) + """), + singleConfiguration = true) + fun `mis-aligned opening and closing quotes of a string template, false positive, case 1 (#1490)`() = Unit + + /** + * No message like + * + * > only spaces are allowed for indentation and each indentation should + * > equal to 4 spaces (tabs are not allowed): the same number of + * > indents to the opening and closing quotes was expected + * + * should be reported. + * + * See [#1490](https://github.com/saveourtool/diktat/issues/1490). + */ + @IndentationTest(IndentedSourceCode( + """ + val value = f( + "text ${'$'}variable text".trimIndent() + ) + """), + singleConfiguration = true) + fun `mis-aligned opening and closing quotes of a string template, false positive, case 2 (#1490)`() = Unit + + /** + * No message like + * + * > only spaces are allowed for indentation and each indentation should + * > equal to 4 spaces (tabs are not allowed): the same number of + * > indents to the opening and closing quotes was expected + * + * should be reported. + * * See [#1490](https://github.com/saveourtool/diktat/issues/1490). */ @IndentationTest(IndentedSourceCode( """ val value = f( - "text ${'$'}variable text".isEmpty() // diktat:WRONG_INDENTATION[message = only spaces are allowed for indentation and each indentation should equal to 4 spaces (tabs are not allowed): the same number of indents to the opening and closing quotes was expected] + "text ${'$'}variable text".trimMargin() ) """), singleConfiguration = true) - fun `issue #1490`() = Unit + fun `mis-aligned opening and closing quotes of a string template, false positive, case 3 (#1490)`() = Unit } /** diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/junit/IndentationTestInvocationContextProvider.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/junit/IndentationTestInvocationContextProvider.kt index 30bf592b51..2bee3d4f0f 100644 --- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/junit/IndentationTestInvocationContextProvider.kt +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/junit/IndentationTestInvocationContextProvider.kt @@ -2,6 +2,7 @@ package org.cqfn.diktat.ruleset.chapter3.spaces.junit import org.cqfn.diktat.ruleset.chapter3.spaces.ExpectedIndentationError import org.cqfn.diktat.ruleset.junit.RuleInvocationContextProvider +import org.cqfn.diktat.ruleset.utils.NEWLINE import org.cqfn.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_AFTER_OPERATORS import org.cqfn.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_BEFORE_DOT import org.cqfn.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_FOR_EXPRESSION_BODIES @@ -95,30 +96,39 @@ class IndentationTestInvocationContextProvider : RuleInvocationContextProvider { + val code1 = indentationTest.second.code + assertThat(code1) + .describedAs("The 2nd code fragment should be empty if `singleConfiguration` is `true`: $NEWLINE$code1") + .isEmpty() + } + + else -> { + val testInput1 = indentationTest.second.extractTestInput( + supportedTags, + allowEmptyErrors = !includeWarnTests) + val (code1, expectedErrors1, customConfig1) = testInput1 + + assertThat(code0) + .describedAs("Both code fragments are the same") + .isNotEqualTo(code1) + assertThat(customConfig0) + .describedAs("Both custom configs are the same") + .isNotEqualTo(customConfig1) + assertThat(testInput0.effectiveConfig) + .describedAs("Both effective configs are the same") + .isNotEqualTo(testInput1.effectiveConfig) + + contexts += IndentationTestFixInvocationContext(customConfig1, actualCode = code1) + contexts += IndentationTestFixInvocationContext(customConfig1, actualCode = code0, expectedCode = code1) + contexts += IndentationTestFixInvocationContext(customConfig0, actualCode = code1, expectedCode = code0) + + if (includeWarnTests) { + contexts += IndentationTestWarnInvocationContext(customConfig1, actualCode = code1) + contexts += IndentationTestWarnInvocationContext(customConfig1, actualCode = code0, expectedErrors0) + contexts += IndentationTestWarnInvocationContext(customConfig0, actualCode = code1, expectedErrors1) + } } } diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/junit/IndentedSourceCode.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/junit/IndentedSourceCode.kt index 4c491dd82b..f18b413300 100644 --- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/junit/IndentedSourceCode.kt +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/junit/IndentedSourceCode.kt @@ -24,6 +24,7 @@ import kotlin.annotation.AnnotationRetention.RUNTIME * @property extendedIndentAfterOperators describes the effective formatting of [code]. * @property extendedIndentBeforeDot describes the effective formatting of [code]. */ +@Target @Retention(RUNTIME) @MustBeDocumented annotation class IndentedSourceCode( diff --git a/diktat-rules/src/test/resources/test/paragraph3/indentation/MultilionStringExpected.kt b/diktat-rules/src/test/resources/test/paragraph3/indentation/MultilionStringExpected.kt deleted file mode 100644 index d4007dd98c..0000000000 --- a/diktat-rules/src/test/resources/test/paragraph3/indentation/MultilionStringExpected.kt +++ /dev/null @@ -1,46 +0,0 @@ -package test.paragraph3.indentation - -//test of correct opening quotation mark and incorrect closing quotation mark -fun multilionString() { - lintMethod( - """ - |val q = 1 - | - """.trimMargin(), - fileName = "src/main/kotlin/org/cqfn/diktat/Example.kts" - ) -} - -//test of incorrect opening quotation mark and incorrect closing quotation mark1 -fun multilionString() { - lintMethod( - """ - |val q = 1 - | - """.trimMargin(), - fileName = "src/main/kotlin/org/cqfn/diktat/Example.kts" - ) -} - -//test of incorrect opening quotation mark and incorrect closing quotation mark2 -fun multilionString() { - lintMethod( - """ - |val q = 1 - | - """.trimMargin(), - fileName = "src/main/kotlin/org/cqfn/diktat/Example.kts" - ) -} - -//test of incorrect opening quotation mark and incorrect closing quotation mark with incorrect shift -fun multilionString() { - lintMethod( - """ - |val q = 1 - | - """.trimMargin(), - fileName = "src/main/kotlin/org/cqfn/diktat/Example.kts" - ) -} - diff --git a/diktat-rules/src/test/resources/test/paragraph3/indentation/MultilionStringTest.kt b/diktat-rules/src/test/resources/test/paragraph3/indentation/MultilionStringTest.kt deleted file mode 100644 index 6274177be1..0000000000 --- a/diktat-rules/src/test/resources/test/paragraph3/indentation/MultilionStringTest.kt +++ /dev/null @@ -1,46 +0,0 @@ -package test.paragraph3.indentation - -//test of correct opening quotation mark and incorrect closing quotation mark -fun multilionString() { - lintMethod( - """ - |val q = 1 - | - """.trimMargin(), - fileName = "src/main/kotlin/org/cqfn/diktat/Example.kts" - ) -} - -//test of incorrect opening quotation mark and incorrect closing quotation mark1 -fun multilionString() { - lintMethod( - """ - |val q = 1 - | - """.trimMargin(), - fileName = "src/main/kotlin/org/cqfn/diktat/Example.kts" - ) -} - -//test of incorrect opening quotation mark and incorrect closing quotation mark2 -fun multilionString() { - lintMethod( - """ - |val q = 1 - | - """.trimMargin(), - fileName = "src/main/kotlin/org/cqfn/diktat/Example.kts" - ) -} - -//test of incorrect opening quotation mark and incorrect closing quotation mark with incorrect shift -fun multilionString() { - lintMethod( - """ - |val q = 1 - | - """.trimMargin(), - fileName = "src/main/kotlin/org/cqfn/diktat/Example.kts" - ) -} -