Skip to content

Commit

Permalink
Do not indent raw string literals that are not followed by either tri…
Browse files Browse the repository at this point in the history
…mIndent() or trimMargin() (#1401)

Closes #1375
  • Loading branch information
paul-dingemans authored Mar 12, 2022
1 parent 8bb3fc4 commit 4079b94
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 117 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
- Fix lint message to "Unnecessary long whitespace" (`no-multi-spaces`) ([#1394](https://github.com/pinterest/ktlint/issues/1394))
- Do not remove trailing comma after a parameter of type array in an annotation (experimental:trailing-comma) ([#1379](https://github.com/pinterest/ktlint/issues/1379))
- Do not delete blank lines in KDoc (no-trailing-spaces) ([#1376](https://github.com/pinterest/ktlint/issues/1376))
- Do not indent raw string literals that are not followed by either trimIndent() or trimMargin() (`indent`) ([#1375](https://github.com/pinterest/ktlint/issues/1375))

### Changed
- Print the rule id always in the PlainReporter ([#1121](https://github.com/pinterest/ktlint/issues/1121))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.pinterest.ktlint.core.ast.ElementType.CLOSING_QUOTE
import com.pinterest.ktlint.core.ast.ElementType.COLON
import com.pinterest.ktlint.core.ast.ElementType.CONDITION
import com.pinterest.ktlint.core.ast.ElementType.DELEGATED_SUPER_TYPE_ENTRY
import com.pinterest.ktlint.core.ast.ElementType.DOT
import com.pinterest.ktlint.core.ast.ElementType.DOT_QUALIFIED_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.ELSE
import com.pinterest.ktlint.core.ast.ElementType.ELVIS
Expand Down Expand Up @@ -587,98 +588,102 @@ public class IndentationRule : Rule(
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
val psi = node.psi as KtStringTemplateExpression
if (psi.isMultiLine()) {
if (node.containsMixedIndentationCharacters()) {
// It can not be determined with certainty how mixed indentation characters should be interpreted.
// The trimIndent function handles tabs and spaces equally (one tabs equals one space) while the user
// might expect that the tab size in the indentation is more than one space.
emit(
node.startOffset,
"Indentation of multiline string should not contain both tab(s) and space(s)",
false
)
return
}
node
.let { it.psi as KtStringTemplateExpression }
.takeIf { it.isFollowedByTrimIndent() || it.isFollowedByTrimMargin() }
?.takeIf { it.isMultiLine() }
?.let {
if (node.containsMixedIndentationCharacters()) {
// It can not be determined with certainty how mixed indentation characters should be interpreted.
// The trimIndent function handles tabs and spaces equally (one tabs equals one space) while the user
// might expect that the tab size in the indentation is more than one space.
emit(
node.startOffset,
"Indentation of multiline string should not contain both tab(s) and space(s)",
false
)
return
}

val prefixLength = node.children()
.filterNot { it.elementType == OPEN_QUOTE }
.filterNot { it.elementType == CLOSING_QUOTE }
.filter { it.prevLeaf()?.text == "\n" }
.filterNot { it.text == "\n" }
.let { indents ->
val indentsExceptBlankIndentBeforeClosingQuote = indents
.filterNot { it.isIndentBeforeClosingQuote() }
if (indentsExceptBlankIndentBeforeClosingQuote.count() > 0) {
indentsExceptBlankIndentBeforeClosingQuote
} else {
indents
val prefixLength = node.children()
.filterNot { it.elementType == OPEN_QUOTE }
.filterNot { it.elementType == CLOSING_QUOTE }
.filter { it.prevLeaf()?.text == "\n" }
.filterNot { it.text == "\n" }
.let { indents ->
val indentsExceptBlankIndentBeforeClosingQuote = indents
.filterNot { it.isIndentBeforeClosingQuote() }
if (indentsExceptBlankIndentBeforeClosingQuote.count() > 0) {
indentsExceptBlankIndentBeforeClosingQuote
} else {
indents
}
}
}
.map { it.text.indentLength() }
.minOrNull() ?: 0
.map { it.text.indentLength() }
.minOrNull() ?: 0

val correctedExpectedIndent = if (node.prevLeaf()?.text == "\n") {
// In case the opening quotes are placed at the start of the line, then expect all lines inside the
// string literal and the closing quotes to have no indent as well.
0
} else {
expectedIndent
}
val expectedIndentation = indentConfig.indent.repeat(correctedExpectedIndent)
val expectedPrefixLength = correctedExpectedIndent * indentConfig.indent.length
node.children()
.forEach {
if (it.prevLeaf()?.text == "\n" &&
(
it.isLiteralStringTemplateEntry() ||
it.isVariableStringTemplateEntry() ||
it.isClosingQuote()
)
) {
val (actualIndent, actualContent) =
if (it.isIndentBeforeClosingQuote()) {
it.text.splitIndentAt(it.text.length)
} else if (it.isVariableStringTemplateEntry() && it.isFirstNonBlankElementOnLine()) {
it.getFirstElementOnSameLine().text.splitIndentAt(expectedPrefixLength)
} else {
it.text.splitIndentAt(prefixLength)
}
if (indentConfig.containsUnexpectedIndentChar(actualIndent)) {
val offsetFirstWrongIndentChar = indentConfig.indexOfFirstUnexpectedIndentChar(actualIndent)
emit(
it.startOffset + offsetFirstWrongIndentChar,
"Unexpected '${indentConfig.unexpectedIndentCharDescription}' character(s) in margin of multiline string",
true
)
if (autoCorrect) {
(it.firstChildNode as LeafPsiElement).rawReplaceWithText(
expectedIndentation + actualContent
val correctedExpectedIndent = if (node.prevLeaf()?.text == "\n") {
// In case the opening quotes are placed at the start of the line, then expect all lines inside the
// string literal and the closing quotes to have no indent as well.
0
} else {
expectedIndent
}
val expectedIndentation = indentConfig.indent.repeat(correctedExpectedIndent)
val expectedPrefixLength = correctedExpectedIndent * indentConfig.indent.length
node.children()
.forEach {
if (it.prevLeaf()?.text == "\n" &&
(
it.isLiteralStringTemplateEntry() ||
it.isVariableStringTemplateEntry() ||
it.isClosingQuote()
)
}
} else if (actualIndent != expectedIndentation && it.isIndentBeforeClosingQuote()) {
// It is a deliberate choice not to fix the indents inside the string literal except the line which only contains
// the closing quotes.
emit(
it.startOffset,
"Unexpected indent of multiline string closing quotes",
true
)
if (autoCorrect) {
if (it.firstChildNode == null) {
(it as LeafPsiElement).rawInsertBeforeMe(
LeafPsiElement(REGULAR_STRING_PART, expectedIndentation)
)
) {
val (actualIndent, actualContent) =
if (it.isIndentBeforeClosingQuote()) {
it.text.splitIndentAt(it.text.length)
} else if (it.isVariableStringTemplateEntry() && it.isFirstNonBlankElementOnLine()) {
it.getFirstElementOnSameLine().text.splitIndentAt(expectedPrefixLength)
} else {
it.text.splitIndentAt(prefixLength)
}
if (indentConfig.containsUnexpectedIndentChar(actualIndent)) {
val offsetFirstWrongIndentChar =
indentConfig.indexOfFirstUnexpectedIndentChar(actualIndent)
emit(
it.startOffset + offsetFirstWrongIndentChar,
"Unexpected '${indentConfig.unexpectedIndentCharDescription}' character(s) in margin of multiline string",
true
)
if (autoCorrect) {
(it.firstChildNode as LeafPsiElement).rawReplaceWithText(
expectedIndentation + actualContent
)
}
} else if (actualIndent != expectedIndentation && it.isIndentBeforeClosingQuote()) {
// It is a deliberate choice not to fix the indents inside the string literal except the line which only contains
// the closing quotes.
emit(
it.startOffset,
"Unexpected indent of multiline string closing quotes",
true
)
if (autoCorrect) {
if (it.firstChildNode == null) {
(it as LeafPsiElement).rawInsertBeforeMe(
LeafPsiElement(REGULAR_STRING_PART, expectedIndentation)
)
} else {
(it.firstChildNode as LeafPsiElement).rawReplaceWithText(
expectedIndentation + actualContent
)
}
}
}
}
}
}
}
}
}

private fun KtStringTemplateExpression.isMultiLine(): Boolean {
Expand Down Expand Up @@ -1008,3 +1013,11 @@ private fun String.splitIndentAt(index: Int): Pair<String, String> {
second = this.substring(safeIndex)
)
}

private fun KtStringTemplateExpression.isFollowedByTrimIndent() = isFollowedBy("trimIndent()")

private fun KtStringTemplateExpression.isFollowedByTrimMargin() = isFollowedBy("trimMargin()")

private fun KtStringTemplateExpression.isFollowedBy(callExpressionName: String) =
this.node.nextSibling { it.elementType != DOT }
.let { it?.elementType == CALL_EXPRESSION && it.text == callExpressionName }
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import com.pinterest.ktlint.core.IndentConfig
import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.ANNOTATION
import com.pinterest.ktlint.core.ast.ElementType.ARROW
import com.pinterest.ktlint.core.ast.ElementType.CALL_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.CLOSING_QUOTE
import com.pinterest.ktlint.core.ast.ElementType.COMMA
import com.pinterest.ktlint.core.ast.ElementType.CONDITION
import com.pinterest.ktlint.core.ast.ElementType.DOT
import com.pinterest.ktlint.core.ast.ElementType.FUN
import com.pinterest.ktlint.core.ast.ElementType.FUNCTION_LITERAL
import com.pinterest.ktlint.core.ast.ElementType.GT
Expand Down Expand Up @@ -269,14 +271,18 @@ public class WrappingRule : Rule(
}

private fun rearrangeClosingQuote(
n: ASTNode,
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
val treeParent = n.treeParent
if (treeParent.elementType == STRING_TEMPLATE) {
val treeParentPsi = treeParent.psi as KtStringTemplateExpression
if (treeParentPsi.isMultiLine() && n.treePrev.text.isNotBlank()) {
node
.treeParent
.takeIf { it.elementType == STRING_TEMPLATE }
?.let { it.psi as KtStringTemplateExpression }
?.takeIf { it.isMultiLine() }
?.takeIf { it.isFollowedByTrimIndent() || it.isFollowedByTrimMargin() }
?.takeIf { node.treePrev.text.isNotBlank() }
?.let {
// rewriting
// """
// text
Expand All @@ -287,29 +293,16 @@ public class WrappingRule : Rule(
// _
// """.trimIndent()
emit(
n.startOffset,
node.startOffset,
"Missing newline before \"\"\"",
true
)
if (autoCorrect) {
val newIndent =
treeParent.lineIndent() +
if (n.elementType == CLOSING_QUOTE) {
""
} else {
indentConfig.indent
}
n as LeafPsiElement
n.rawInsertBeforeMe(
LeafPsiElement(
REGULAR_STRING_PART,
"\n" + newIndent
)
)
node as LeafPsiElement
node.rawInsertBeforeMe(LeafPsiElement(REGULAR_STRING_PART, "\n"))
}
logger.trace { "$line: " + (if (!autoCorrect) "would have " else "") + "inserted newline before (closing) \"\"\"" }
}
}
}

private fun mustBeFollowedByNewline(node: ASTNode): Boolean {
Expand Down Expand Up @@ -436,4 +429,12 @@ public class WrappingRule : Rule(
children().any { c -> c.textContains('\n') && c.elementType !in ignoreElementTypes }
}
}

private fun KtStringTemplateExpression.isFollowedByTrimIndent() = isFollowedBy("trimIndent()")

private fun KtStringTemplateExpression.isFollowedByTrimMargin() = isFollowedBy("trimMargin()")

private fun KtStringTemplateExpression.isFollowedBy(callExpressionName: String) =
this.node.nextSibling { it.elementType != DOT }
.let { it?.elementType == CALL_EXPRESSION && it.text == callExpressionName }
}
Original file line number Diff line number Diff line change
Expand Up @@ -1271,15 +1271,15 @@ internal class IndentationRuleTest {
}

@Test
fun `Issue 1127 - multiline string in parameter list`() {
fun `Issue 1127 - multiline string followed by trimIndent in parameter list`() {
val code =
"""
interface UserRepository : JpaRepository<User, UUID> {
@Query($MULTILINE_STRING_QUOTE
select u from User u
inner join Organization o on u.organization = o
where o = :organization
$MULTILINE_STRING_QUOTE)
$MULTILINE_STRING_QUOTE.trimIndent())
fun findByOrganization(organization: Organization, pageable: Pageable): Page<User>
}
""".trimIndent()
Expand All @@ -1291,7 +1291,7 @@ internal class IndentationRuleTest {
select u from User u
inner join Organization o on u.organization = o
where o = :organization
$MULTILINE_STRING_QUOTE
$MULTILINE_STRING_QUOTE.trimIndent()
)
fun findByOrganization(organization: Organization, pageable: Pageable): Page<User>
}
Expand All @@ -1302,7 +1302,7 @@ internal class IndentationRuleTest {
listOf(
LintError(line = 2, col = 12, ruleId = "wrapping", detail = "Missing newline after \"(\""),
LintError(line = 6, col = 1, ruleId = "indent", detail = "Unexpected indent of multiline string closing quotes"),
LintError(line = 6, col = 7, ruleId = "wrapping", detail = "Missing newline before \")\"")
LintError(line = 6, col = 20, ruleId = "wrapping", detail = "Missing newline before \")\"")
)
)
assertThat(wrappingAndIndentRule.format(code)).isEqualTo(formattedCode)
Expand Down
Loading

0 comments on commit 4079b94

Please # to comment.