Skip to content

Commit b8bd8a2

Browse files
WRONG_INDENTATION: fix the regression introduced with 998d0e9 (#1503)
### What's done: * Fixes the false-positive warnings reported for single-line string templates. * Closes #1490. * See #811 (the original feature request). * See #1364 (the pull request which introduced the regression). * See 998d0e9 (the commit which introduced the regression).
1 parent ce65178 commit b8bd8a2

File tree

7 files changed

+351
-149
lines changed

7 files changed

+351
-149
lines changed

diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationRule.kt

+94-27
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* Main logic of indentation including Rule and utility classes and methods.
33
*/
44

5+
@file:Suppress("FILE_UNORDERED_IMPORTS")// False positives, see #1494.
6+
57
package org.cqfn.diktat.ruleset.rules.chapter3.files
68

79
import org.cqfn.diktat.common.config.rules.RulesConfig
@@ -79,11 +81,13 @@ import org.jetbrains.kotlin.psi.psiUtil.parents
7981
import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf
8082
import org.jetbrains.kotlin.psi.psiUtil.startOffset
8183

82-
import java.util.ArrayDeque as Stack
83-
84+
import kotlin.contracts.ExperimentalContracts
85+
import kotlin.contracts.contract
8486
import kotlin.math.abs
8587
import kotlin.reflect.KCallable
8688

89+
import java.util.ArrayDeque as Stack
90+
8791
/**
8892
* Rule that checks indentation. The following general rules are checked:
8993
* 1. Only spaces should be used each indentation is equal to 4 spaces
@@ -198,21 +202,6 @@ class IndentationRule(configRules: List<RulesConfig>) : DiktatRule(
198202
}
199203
}
200204

201-
private fun isCloseAndOpenQuoterOffset(nodeWhiteSpace: ASTNode, expectedIndent: Int): Boolean {
202-
val nextNode = nodeWhiteSpace.treeNext
203-
if (nextNode.elementType == VALUE_ARGUMENT) {
204-
val nextNodeDot = getNextDotExpression(nextNode)
205-
nextNodeDot?.getFirstChildWithType(STRING_TEMPLATE)?.let {
206-
if (it.getAllChildrenWithType(LITERAL_STRING_TEMPLATE_ENTRY).size > 1) {
207-
val closingQuote = it.getFirstChildWithType(CLOSING_QUOTE)?.treePrev?.text
208-
?.length ?: -1
209-
return expectedIndent == closingQuote
210-
}
211-
}
212-
}
213-
return true
214-
}
215-
216205
@Suppress("ForbiddenComment")
217206
private fun IndentContext.visitWhiteSpace(astNode: ASTNode) {
218207
require(astNode.isMultilineWhitespace()) {
@@ -242,10 +231,10 @@ class IndentationRule(configRules: List<RulesConfig>) : DiktatRule(
242231
addException(astNode.treeParent, abs(indentError.expected - indentError.actual), false)
243232
}
244233

245-
val difOffsetCloseAndOpenQuote = isCloseAndOpenQuoterOffset(astNode, indentError.actual)
234+
val alignedOpeningAndClosingQuotes = hasAlignedOpeningAndClosingQuotes(astNode, indentError.actual)
246235

247-
if ((checkResult?.isCorrect != true && expectedIndent != indentError.actual) || !difOffsetCloseAndOpenQuote) {
248-
val warnText = if (!difOffsetCloseAndOpenQuote) {
236+
if ((checkResult?.isCorrect != true && expectedIndent != indentError.actual) || !alignedOpeningAndClosingQuotes) {
237+
val warnText = if (!alignedOpeningAndClosingQuotes) {
249238
"the same number of indents to the opening and closing quotes was expected"
250239
} else {
251240
"expected $expectedIndent but was ${indentError.actual}"
@@ -268,7 +257,7 @@ class IndentationRule(configRules: List<RulesConfig>) : DiktatRule(
268257
expectedIndent: Int,
269258
actualIndent: Int
270259
) {
271-
val nextNodeDot = getNextDotExpression(whiteSpace.node.treeNext)
260+
val nextNodeDot = whiteSpace.node.treeNext.getNextDotExpression()
272261
if (nextNodeDot != null &&
273262
nextNodeDot.elementType == DOT_QUALIFIED_EXPRESSION &&
274263
nextNodeDot.firstChildNode.elementType == STRING_TEMPLATE &&
@@ -361,12 +350,6 @@ class IndentationRule(configRules: List<RulesConfig>) : DiktatRule(
361350
}
362351
}
363352

364-
private fun getNextDotExpression(node: ASTNode) = if (node.elementType == DOT_QUALIFIED_EXPRESSION) {
365-
node
366-
} else {
367-
node.getFirstChildWithType(DOT_QUALIFIED_EXPRESSION)
368-
}
369-
370353
/**
371354
* Modifies [templateEntry] by correcting its indentation level.
372355
*
@@ -734,11 +717,30 @@ class IndentationRule(configRules: List<RulesConfig>) : DiktatRule(
734717
private fun ASTNode.isMultilineWhitespace(): Boolean =
735718
elementType == WHITE_SPACE && textContains(NEWLINE)
736719

720+
@OptIn(ExperimentalContracts::class)
721+
private fun ASTNode?.isMultilineStringTemplate(): Boolean {
722+
contract {
723+
returns(true) implies (this@isMultilineStringTemplate != null)
724+
}
725+
726+
this ?: return false
727+
728+
return elementType == STRING_TEMPLATE &&
729+
getAllChildrenWithType(LITERAL_STRING_TEMPLATE_ENTRY).any { entry ->
730+
entry.textContains(NEWLINE)
731+
}
732+
}
733+
737734
/**
738735
* @return `true` if this is a [String.trimIndent] or [String.trimMargin]
739736
* call, `false` otherwise.
740737
*/
738+
@OptIn(ExperimentalContracts::class)
741739
private fun ASTNode?.isTrimIndentOrMarginCall(): Boolean {
740+
contract {
741+
returns(true) implies (this@isTrimIndentOrMarginCall != null)
742+
}
743+
742744
this ?: return false
743745

744746
require(elementType == CALL_EXPRESSION) {
@@ -758,6 +760,12 @@ class IndentationRule(configRules: List<RulesConfig>) : DiktatRule(
758760
return identifier.text in knownTrimFunctionPatterns
759761
}
760762

763+
private fun ASTNode.getNextDotExpression(): ASTNode? =
764+
when (elementType) {
765+
DOT_QUALIFIED_EXPRESSION -> this
766+
else -> getFirstChildWithType(DOT_QUALIFIED_EXPRESSION)
767+
}
768+
761769
/**
762770
* @return the matching closing brace type for this opening brace type,
763771
* or vice versa.
@@ -791,5 +799,64 @@ class IndentationRule(configRules: List<RulesConfig>) : DiktatRule(
791799
this > 0 -> this
792800
else -> 0
793801
}
802+
803+
/**
804+
* Processes fragments like:
805+
*
806+
* ```kotlin
807+
* f(
808+
* """
809+
* |foobar
810+
* """.trimMargin()
811+
* )
812+
* ```
813+
*
814+
* @param whitespace the whitespace node between an [LPAR] and the
815+
* `trimIndent()`- or `trimMargin()`- terminated string template, which is
816+
* an effective argument of a function call. The string template is
817+
* expected to begin on a separate line (otherwise, there'll be no
818+
* whitespace in-between).
819+
* @return `true` if the opening and the closing quotes of the string
820+
* template are aligned, `false` otherwise.
821+
*/
822+
private fun hasAlignedOpeningAndClosingQuotes(whitespace: ASTNode, expectedIndent: Int): Boolean {
823+
require(whitespace.isMultilineWhitespace()) {
824+
"The node is $whitespace while a multi-line $WHITE_SPACE expected"
825+
}
826+
827+
/*
828+
* Here, we expect that `nextNode` is a VALUE_ARGUMENT which contains
829+
* the dot-qualified expression (`STRING_TEMPLATE.trimIndent()` or
830+
* `STRING_TEMPLATE.trimMargin()`).
831+
*/
832+
val nextFunctionArgument = whitespace.treeNext
833+
if (nextFunctionArgument.elementType == VALUE_ARGUMENT) {
834+
val memberOrExtensionCall = nextFunctionArgument.getNextDotExpression()
835+
836+
/*
837+
* Limit allowed member or extension calls to `trimIndent()` and
838+
* `trimMargin()`.
839+
*/
840+
if (memberOrExtensionCall != null &&
841+
memberOrExtensionCall.getFirstChildWithType(CALL_EXPRESSION).isTrimIndentOrMarginCall()) {
842+
val stringTemplate = memberOrExtensionCall.getFirstChildWithType(STRING_TEMPLATE)
843+
844+
/*
845+
* Limit the logic to multi-line string templates only (the
846+
* opening and closing quotes of a single-line template are,
847+
* obviously, always mis-aligned).
848+
*/
849+
if (stringTemplate != null && stringTemplate.isMultilineStringTemplate()) {
850+
val closingQuoteIndent = stringTemplate.getFirstChildWithType(CLOSING_QUOTE)
851+
?.treePrev
852+
?.text
853+
?.length ?: -1
854+
return expectedIndent == closingQuoteIndent
855+
}
856+
}
857+
}
858+
859+
return true
860+
}
794861
}
795862
}

diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleFixTest.kt

+172-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@file:Suppress("FILE_UNORDERED_IMPORTS")// False positives, see #1494.
2+
13
package org.cqfn.diktat.ruleset.chapter3.spaces
24

35
import org.cqfn.diktat.common.config.rules.RulesConfig
@@ -10,12 +12,20 @@ import org.cqfn.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXT
1012
import org.cqfn.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_FOR_EXPRESSION_BODIES
1113
import org.cqfn.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_OF_PARAMETERS
1214
import org.cqfn.diktat.ruleset.utils.indentation.IndentationConfig.Companion.NEWLINE_AT_END
15+
import org.cqfn.diktat.test.framework.processing.FileComparisonResult
1316
import org.cqfn.diktat.util.FixTestBase
1417

1518
import generated.WarningNames
19+
import org.assertj.core.api.Assertions.assertThat
20+
import org.intellij.lang.annotations.Language
21+
import org.junit.jupiter.api.Nested
1622
import org.junit.jupiter.api.Tag
1723
import org.junit.jupiter.api.Test
1824
import org.junit.jupiter.api.TestMethodOrder
25+
import org.junit.jupiter.api.io.TempDir
26+
import java.nio.file.Path
27+
28+
import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationConfigFactory as IndentationConfig
1929

2030
/**
2131
* Legacy indentation tests.
@@ -64,9 +74,167 @@ class IndentationRuleFixTest : FixTestBase("test/paragraph3/indentation",
6474
fixAndCompare("ConstructorExpected.kt", "ConstructorTest.kt")
6575
}
6676

67-
@Test
68-
@Tag(WarningNames.WRONG_INDENTATION)
69-
fun `multiline string`() {
70-
fixAndCompare("MultilionStringExpected.kt", "MultilionStringTest.kt")
77+
@Nested
78+
@TestMethodOrder(NaturalDisplayName::class)
79+
inner class `Multi-line string literals` {
80+
/**
81+
* Correctly-indented opening quotation mark, incorrectly-indented
82+
* closing quotation mark.
83+
*/
84+
@Test
85+
@Tag(WarningNames.WRONG_INDENTATION)
86+
@Suppress("LOCAL_VARIABLE_EARLY_DECLARATION") // False positives
87+
fun `case 1 - mis-aligned opening and closing quotes`(@TempDir tempDir: Path) {
88+
val actualCode = """
89+
|fun f() {
90+
| g(
91+
| ""${'"'}
92+
| |val q = 1
93+
| |
94+
| ""${'"'}.trimMargin(),
95+
| arg1 = "arg1"
96+
| )
97+
|}
98+
""".trimMargin()
99+
100+
val expectedCode = """
101+
|fun f() {
102+
| g(
103+
| ""${'"'}
104+
| |val q = 1
105+
| |
106+
| ""${'"'}.trimMargin(),
107+
| arg1 = "arg1"
108+
| )
109+
|}
110+
""".trimMargin()
111+
112+
val lintResult = fixAndCompareContent(actualCode, expectedCode, tempDir)
113+
assertThat(lintResult.actualContent)
114+
.describedAs("lint result for ${actualCode.describe()}")
115+
.isEqualTo(lintResult.expectedContent)
116+
}
117+
118+
/**
119+
* Both the opening and the closing quotation marks are incorrectly
120+
* indented (indentation level is less than needed).
121+
*/
122+
@Test
123+
@Tag(WarningNames.WRONG_INDENTATION)
124+
@Suppress("LOCAL_VARIABLE_EARLY_DECLARATION") // False positives
125+
fun `case 2`(@TempDir tempDir: Path) {
126+
val actualCode = """
127+
|fun f() {
128+
| g(
129+
| ""${'"'}
130+
| |val q = 1
131+
| |
132+
| ""${'"'}.trimMargin(),
133+
| arg1 = "arg1"
134+
| )
135+
|}
136+
""".trimMargin()
137+
138+
val expectedCode = """
139+
|fun f() {
140+
| g(
141+
| ""${'"'}
142+
| |val q = 1
143+
| |
144+
| ""${'"'}.trimMargin(),
145+
| arg1 = "arg1"
146+
| )
147+
|}
148+
""".trimMargin()
149+
150+
val lintResult = fixAndCompareContent(actualCode, expectedCode, tempDir)
151+
assertThat(lintResult.actualContent)
152+
.describedAs("lint result for ${actualCode.describe()}")
153+
.isEqualTo(lintResult.expectedContent)
154+
}
155+
156+
/**
157+
* Both the opening and the closing quotation marks are incorrectly
158+
* indented (indentation level is greater than needed).
159+
*/
160+
@Test
161+
@Tag(WarningNames.WRONG_INDENTATION)
162+
@Suppress("LOCAL_VARIABLE_EARLY_DECLARATION") // False positives
163+
fun `case 3`(@TempDir tempDir: Path) {
164+
val actualCode = """
165+
|fun f() {
166+
| g(
167+
| ""${'"'}
168+
| |val q = 1
169+
| |
170+
| ""${'"'}.trimMargin(),
171+
| arg1 = "arg1"
172+
| )
173+
|}
174+
""".trimMargin()
175+
176+
val expectedCode = """
177+
|fun f() {
178+
| g(
179+
| ""${'"'}
180+
| |val q = 1
181+
| |
182+
| ""${'"'}.trimMargin(),
183+
| arg1 = "arg1"
184+
| )
185+
|}
186+
""".trimMargin()
187+
188+
val lintResult = fixAndCompareContent(actualCode, expectedCode, tempDir)
189+
assertThat(lintResult.actualContent)
190+
.describedAs("lint result for ${actualCode.describe()}")
191+
.isEqualTo(lintResult.expectedContent)
192+
}
193+
194+
/**
195+
* Both the opening and the closing quotation marks are incorrectly
196+
* indented and misaligned.
197+
*/
198+
@Test
199+
@Tag(WarningNames.WRONG_INDENTATION)
200+
@Suppress("LOCAL_VARIABLE_EARLY_DECLARATION") // False positives
201+
fun `case 4 - mis-aligned opening and closing quotes`(@TempDir tempDir: Path) {
202+
val actualCode = """
203+
|fun f() {
204+
| g(
205+
| ""${'"'}
206+
| |val q = 1
207+
| |
208+
| ""${'"'}.trimMargin(),
209+
| arg1 = "arg1"
210+
| )
211+
|}
212+
""".trimMargin()
213+
214+
val expectedCode = """
215+
|fun f() {
216+
| g(
217+
| ""${'"'}
218+
| |val q = 1
219+
| |
220+
| ""${'"'}.trimMargin(),
221+
| arg1 = "arg1"
222+
| )
223+
|}
224+
""".trimMargin()
225+
226+
val lintResult = fixAndCompareContent(actualCode, expectedCode, tempDir)
227+
assertThat(lintResult.actualContent)
228+
.describedAs("lint result for ${actualCode.describe()}")
229+
.isEqualTo(lintResult.expectedContent)
230+
}
231+
232+
private fun fixAndCompareContent(@Language("kotlin") actualCode: String,
233+
@Language("kotlin") expectedCode: String,
234+
tempDir: Path
235+
): FileComparisonResult {
236+
val config = IndentationConfig(NEWLINE_AT_END to false).withCustomParameters().asRulesConfigList()
237+
return fixAndCompareContent(actualCode, expectedCode, tempDir, config)
238+
}
71239
}
72240
}

0 commit comments

Comments
 (0)