diff --git a/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/AnnotationRuleTest.kt b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/AnnotationRuleTest.kt index def89d7fff..a68a5ffb7c 100644 --- a/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/AnnotationRuleTest.kt +++ b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/AnnotationRuleTest.kt @@ -938,11 +938,12 @@ class AnnotationRuleTest { @Test fun `lint file annotations should be separated with a blank line in script 1`() { - val code = """ + val code = + """ @file:Suppress("UnstableApiUsage") pluginManagement { } - """.trimIndent() + """.trimIndent() assertThat(AnnotationRule().lint(code, script = true)).isEqualTo( listOf( LintError(1, 34, "annotation", AnnotationRule.fileAnnotationsShouldBeSeparated) @@ -952,12 +953,13 @@ class AnnotationRuleTest { @Test fun `lint file annotations should be separated with a blank line in script 2`() { - val code = """ + val code = + """ @file:Suppress("UnstableApiUsage") pluginManagement { } - """.trimIndent() + """.trimIndent() assertThat(AnnotationRule().lint(code, script = true)).isEmpty() } diff --git a/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/AnnotationSpacingRuleTest.kt b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/AnnotationSpacingRuleTest.kt index 900481c0ed..49c07e7e7f 100644 --- a/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/AnnotationSpacingRuleTest.kt +++ b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/AnnotationSpacingRuleTest.kt @@ -332,12 +332,12 @@ class AnnotationSpacingRuleTest { fun `annotations should not be separated by comments from the annotated construct`() { val code = """ - @Suppress("DEPRECATION") @Hello - /** - * block comment - */ - class Foo { - } + @Suppress("DEPRECATION") @Hello + /** + * block comment + */ + class Foo { + } """.trimIndent() assertThat( AnnotationSpacingRule().lint(code) @@ -352,41 +352,41 @@ class AnnotationSpacingRuleTest { fun `annotations should be moved after comments`() { val code = """ - @Suppress("DEPRECATION") @Hello - /** - * block comment - */ - class Foo { - } + @Suppress("DEPRECATION") @Hello + /** + * block comment + */ + class Foo { + } """.trimIndent() assertThat( AnnotationSpacingRule().format(code) ).isEqualTo( """ - /** - * block comment - */ - @Suppress("DEPRECATION") @Hello - class Foo { - } + /** + * block comment + */ + @Suppress("DEPRECATION") @Hello + class Foo { + } """.trimIndent() ) val codeEOL = """ - @Suppress("DEPRECATION") @Hello - // hello - class Foo { - } + @Suppress("DEPRECATION") @Hello + // hello + class Foo { + } """.trimIndent() assertThat( AnnotationSpacingRule().format(codeEOL) ).isEqualTo( """ - // hello - @Suppress("DEPRECATION") @Hello - class Foo { - } + // hello + @Suppress("DEPRECATION") @Hello + class Foo { + } """.trimIndent() ) } @@ -395,33 +395,33 @@ class AnnotationSpacingRuleTest { fun `preceding whitespaces are preserved`() { val code = """ - package a.b.c + package a.b.c - val hello = 5 + val hello = 5 - @Suppress("DEPRECATION") @Hello - /** - * block comment - */ - class Foo { - } + @Suppress("DEPRECATION") @Hello + /** + * block comment + */ + class Foo { + } """.trimIndent() assertThat( AnnotationSpacingRule().format(code) ).isEqualTo( """ - package a.b.c + package a.b.c - val hello = 5 + val hello = 5 - /** - * block comment - */ - @Suppress("DEPRECATION") @Hello - class Foo { - } + /** + * block comment + */ + @Suppress("DEPRECATION") @Hello + class Foo { + } """.trimIndent() ) } @@ -443,18 +443,18 @@ class AnnotationSpacingRuleTest { fun `format eol comment on the same line as the annotation`() { val code = """ - @SuppressWarnings // foo + @SuppressWarnings // foo - fun bar() { - } + fun bar() { + } """.trimIndent() assertThat( AnnotationSpacingRule().format(code) ).isEqualTo( """ - @SuppressWarnings // foo - fun bar() { - } + @SuppressWarnings // foo + fun bar() { + } """.trimIndent() ) } @@ -463,19 +463,19 @@ class AnnotationSpacingRuleTest { fun `format eol comment on the same line as the annotation 2`() { val code = """ - @SuppressWarnings // foo - // bar - fun bar() { - } + @SuppressWarnings // foo + // bar + fun bar() { + } """.trimIndent() assertThat( AnnotationSpacingRule().format(code) ).isEqualTo( """ - // bar - @SuppressWarnings // foo - fun bar() { - } + // bar + @SuppressWarnings // foo + fun bar() { + } """.trimIndent() ) } diff --git a/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/ArgumentListWrappingRuleTest.kt b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/ArgumentListWrappingRuleTest.kt index feb5488bb1..39cf99fa63 100644 --- a/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/ArgumentListWrappingRuleTest.kt +++ b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/ArgumentListWrappingRuleTest.kt @@ -13,10 +13,10 @@ class ArgumentListWrappingRuleTest { assertThat( ArgumentListWrappingRule().lint( """ - val x = f( - a, - b, c - ) + val x = f( + a, + b, c + ) """.trimIndent() ) ).isEqualTo( @@ -36,19 +36,19 @@ class ArgumentListWrappingRuleTest { assertThat( ArgumentListWrappingRule().format( """ - val x = f( - a, - b, c - ) + val x = f( + a, + b, c + ) """.trimIndent() ) ).isEqualTo( """ - val x = f( - a, - b, - c - ) + val x = f( + a, + b, + c + ) """.trimIndent() ) } @@ -58,25 +58,25 @@ class ArgumentListWrappingRuleTest { assertThat( ArgumentListWrappingRule().format( """ - val x = test( - one("a", "b", - "c"), - "Two", "Three", "Four" - ) + val x = test( + one("a", "b", + "c"), + "Two", "Three", "Four" + ) """.trimIndent() ) ).isEqualTo( """ - val x = test( - one( - "a", - "b", - "c" - ), - "Two", - "Three", - "Four" - ) + val x = test( + one( + "a", + "b", + "c" + ), + "Two", + "Three", + "Four" + ) """.trimIndent() ) } @@ -86,7 +86,7 @@ class ArgumentListWrappingRuleTest { assertThat( ArgumentListWrappingRule().lint( """ - val x = f(a, b, c) + val x = f(a, b, c) """.trimIndent(), userData = mapOf("max_line_length" to "10") ) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRule.kt index 23c898b01f..0a049d9a6d 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRule.kt @@ -79,7 +79,6 @@ import com.pinterest.ktlint.core.ast.prevSibling import com.pinterest.ktlint.core.ast.upsertWhitespaceAfterMe import com.pinterest.ktlint.core.ast.upsertWhitespaceBeforeMe import com.pinterest.ktlint.core.ast.visit -import java.lang.StringBuilder import java.util.Deque import java.util.LinkedList import org.jetbrains.kotlin.com.intellij.lang.ASTNode @@ -763,85 +762,80 @@ class IndentationRule : Rule("indent"), Rule.Modifier.RestrictToRootLast { ) { val psi = node.psi as KtStringTemplateExpression if (psi.isMultiLine() && psi.isFollowedByTrimIndent()) { - val children = node.children() - val prefixLength = - children - .fold(StringBuilder()) { sb, child -> - when (child.elementType) { - LITERAL_STRING_TEMPLATE_ENTRY -> { - val text = child.text - // bail if indentation contains Tab - for (c in text) { - if (c == '\t') { - return // bail - } - if (!c.isWhitespace()) { - break - } + 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() } + .min() ?: 0 + + val expectedIndentation = editorConfig.repeatIndent(expectedIndent) + val expectedPrefixLength = expectedIndent * editorConfig.indentSize + 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) + } + val (wrongIndentChar, wrongIndentDescription) = editorConfig.wrongIndentChar() + if (actualIndent.contains(wrongIndentChar)) { + val offsetFirstWrongIndentChar = actualIndent.indexOfFirst(wrongIndentChar) + emit( + it.startOffset + offsetFirstWrongIndentChar, + "Unexpected '$wrongIndentDescription' 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 + ) } - sb.append(text) } - LONG_STRING_TEMPLATE_ENTRY -> sb.append("${'$'}{}") - SHORT_STRING_TEMPLATE_ENTRY -> sb.append("${'$'}") - else -> sb } } - .split('\n') - .filter(String::isNotBlank) - .map { it.indentLength() } - .min() ?: 0 - val expectedPrefixLength = expectedIndent * editorConfig.indentSize - // TODO: uncomment, once it's clear how to indent stuff within string templates -// if (prefixLength != expectedPrefixLength) { -// for (child in children) { -// if (child.isPrecededByLFStringTemplateEntry()) { -// when (child.elementType) { -// LITERAL_STRING_TEMPLATE_ENTRY -> { -// val v = child.text -// if (v != "\n") { -// val indentLength = v.indentLength() -// val expectedIndentLength = indentLength - prefixLength + expectedPrefixLength -// if (indentLength != expectedIndentLength) { -// reindentStringTemplateEntry( -// child, -// autoCorrect, -// emit, -// indentLength, -// expectedIndentLength -// ) -// } -// } -// } -// LONG_STRING_TEMPLATE_ENTRY, SHORT_STRING_TEMPLATE_ENTRY -> { -// if (expectedPrefixLength != 0) { -// preindentStringTemplateEntry( -// child.firstChildNode, -// autoCorrect, -// emit, -// expectedPrefixLength -// ) -// } -// } -// } -// } -// } -// } - val closingQuote = children.find { it.elementType == CLOSING_QUOTE }!! - if (closingQuote.treePrev.text == "\n") { - // rewriting - // ( - // """ - // """.trimIndent() - // ) - // to - // ( - // """ - // """.trimIndent() - // ) - if (expectedPrefixLength != 0) { - preindentStringTemplateEntry(closingQuote, autoCorrect, emit, expectedPrefixLength) } - } else if (closingQuote.treePrev.text.isNotBlank()) { + + val closingQuote = node.children().find { it.elementType == CLOSING_QUOTE }!! + if (closingQuote.treePrev.text.isNotBlank()) { // rewriting // """ // text @@ -867,64 +861,10 @@ class IndentationRule : Rule("indent"), Rule.Modifier.RestrictToRootLast { (if (!autoCorrect) "would have " else "") + "inserted newline before (closing) \"\"\"" } - } else { // preceded by blank LITERAL_STRING_TEMPLATE_ENTRY - val child = closingQuote.treePrev - val indentLength = child.text.length - if (indentLength != expectedPrefixLength) { - reindentStringTemplateEntry(child, autoCorrect, emit, indentLength, expectedPrefixLength) - } } } } - private fun preindentStringTemplateEntry( - node: ASTNode, - autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, - expectedIndentLength: Int - ) { - emit( - node.startOffset, - "Unexpected indentation (0) (should be $expectedIndentLength)", - true - ) - if (autoCorrect) { - (node as LeafPsiElement).rawInsertBeforeMe( - LeafPsiElement(REGULAR_STRING_PART, " ".repeat(expectedIndentLength)) - ) - } - debug { - (if (!autoCorrect) "would have " else "") + - "changed indentation before ${node.text} to $expectedIndentLength (from 0)" - } - } - - private fun reindentStringTemplateEntry( - node: ASTNode, - autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, - indentLength: Int, - expectedIndentLength: Int - ) { - emit( - node.startOffset, - "Unexpected indentation ($indentLength) (should be $expectedIndentLength)", - true - ) - if (autoCorrect) { - (node.firstChildNode as LeafPsiElement).rawReplaceWithText( - " ".repeat(expectedIndentLength) + node.text.substring(indentLength) - ) - } - debug { - (if (!autoCorrect) "would have " else "") + - "changed indentation to $expectedIndentLength (from $indentLength)" - } - } - - private fun ASTNode.isPrecededByLFStringTemplateEntry() = - treePrev?.let { it.elementType == LITERAL_STRING_TEMPLATE_ENTRY && it.text == "\n" } == true - private fun KtStringTemplateExpression.isMultiLine(): Boolean { for (child in node.children()) { if (child.elementType == LITERAL_STRING_TEMPLATE_ENTRY) { @@ -1146,3 +1086,72 @@ class IndentationRule : Rule("indent"), Rule.Modifier.RestrictToRootLast { } } } + +private fun ASTNode.isIndentBeforeClosingQuote() = + elementType == CLOSING_QUOTE || (text.isBlank() && nextCodeSibling()?.elementType == CLOSING_QUOTE) + +private fun EditorConfig.repeatIndent(indentLevel: Int) = + when (indentStyle) { + IndentStyle.SPACE -> " ".repeat(indentLevel * indentSize) + IndentStyle.TAB -> "\t".repeat(indentLevel) + } + +private fun EditorConfig.wrongIndentChar(): Pair = + when (indentStyle) { + IndentStyle.SPACE -> Pair('\t', "tab") + IndentStyle.TAB -> Pair(' ', "space") + } + +private fun ASTNode.isLiteralStringTemplateEntry() = + elementType == LITERAL_STRING_TEMPLATE_ENTRY && text != "\n" + +private fun ASTNode.isVariableStringTemplateEntry() = + elementType == LONG_STRING_TEMPLATE_ENTRY || elementType == SHORT_STRING_TEMPLATE_ENTRY + +private fun ASTNode.isClosingQuote() = + elementType == CLOSING_QUOTE + +private fun ASTNode.isFirstNonBlankElementOnLine(): Boolean { + var node: ASTNode? = getFirstElementOnSameLine() + while (node != null && node != this && node.text.isWhitespace()) { + node = node.nextLeaf() + } + return node != this +} + +private fun String.isWhitespace() = + none { !it.isWhitespace() } + +private fun ASTNode.getFirstElementOnSameLine(): ASTNode { + val firstLeafOnLine = prevLeaf { it.text == "\n" } + return if (firstLeafOnLine == null) { + this + } else { + firstLeafOnLine.nextLeaf(includeEmpty = true) ?: this + } +} + +/** + * Splits the string at the given index or at the first non white space character before that index. The returned pair + * consists of the indentation and the second part contains the remainder. Note that the second part still can start + * with whitespace characters in case the original strings starts with more white space characters than the requested + * split index. + */ +private fun String.splitIndentAt(index: Int): Pair { + assert(index >= 0) + val firstNonWhitespaceIndex = indexOfFirst { !it.isWhitespace() }.let { + if (it == -1) { + this.length + } else { + it + } + } + val safeIndex = kotlin.math.min(firstNonWhitespaceIndex, index) + return Pair( + first = this.take(safeIndex), + second = this.substring(safeIndex) + ) +} + +private fun String.indexOfFirst(char: Char) = + indexOfFirst { it == char } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/ChainWrappingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/ChainWrappingRuleTest.kt index c177886bf6..676ec804df 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/ChainWrappingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/ChainWrappingRuleTest.kt @@ -66,16 +66,16 @@ class ChainWrappingRuleTest { ) ).isEqualTo( """ - fun test(foo: String?, bar: String?, baz: String?) { - when { - foo != null && - bar != null && - baz != null -> { - } - else -> { - } - } - } + fun test(foo: String?, bar: String?, baz: String?) { + when { + foo != null && + bar != null && + baz != null -> { + } + else -> { + } + } + } """.trimIndent() ) } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRuleTest.kt index 70baba2588..e7bde11372 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRuleTest.kt @@ -16,7 +16,7 @@ internal class IndentationRuleTest { } @Test - fun testFormat() { + fun `format unindented input`() { assertThat( IndentationRule().diffFileFormat( "spec/indent/format.kt.spec", @@ -26,12 +26,12 @@ internal class IndentationRuleTest { } @Test - fun testFormatTabs() { + fun `format unindented input with tabs`() { assertThat( IndentationRule().diffFileFormat( "spec/indent/format.kt.spec", "spec/indent/format-expected-tabs.kt.spec", - mapOf("indent_style" to "tab") + INDENT_STYLE_TABS ) ).isEmpty() } @@ -66,7 +66,7 @@ internal class IndentationRuleTest { } @Test - fun testLintIndentTab() { + fun `lint IndentTab with tabs`() { assertThat( IndentationRule().lint( """ @@ -84,7 +84,7 @@ internal class IndentationRuleTest { | set(v: String) { x = v } |} |""".trimMargin(), - mapOf("indent_style" to "tab") + INDENT_STYLE_TABS ) ).isEqualTo( listOf( @@ -181,10 +181,30 @@ internal class IndentationRuleTest { @Test fun testLintFirstLine() { - assertThat(IndentationRule().lint(" // comment")).hasSize(1) - assertThat(IndentationRule().lint(" // comment", script = true)).hasSize(1) - assertThat(IndentationRule().lint(" \n // comment")).hasSize(1) - assertThat(IndentationRule().lint(" \n // comment", script = true)).hasSize(1) + assertThat(IndentationRule().lint(" // comment")) + .isEqualTo( + listOf( + LintError(line = 1, col = 1, ruleId = "indent", detail = "Unexpected indentation (2) (should be 0)"), + ) + ) + assertThat(IndentationRule().lint(" // comment", script = true)) + .isEqualTo( + listOf( + LintError(line = 1, col = 1, ruleId = "indent", detail = "Unexpected indentation (2) (should be 0)"), + ) + ) + assertThat(IndentationRule().lint(" \n // comment")) + .isEqualTo( + listOf( + LintError(line = 2, col = 1, ruleId = "indent", detail = "Unexpected indentation (2) (should be 0)"), + ) + ) + assertThat(IndentationRule().lint(" \n // comment", script = true)) + .isEqualTo( + listOf( + LintError(line = 2, col = 1, ruleId = "indent", detail = "Unexpected indentation (2) (should be 0)"), + ) + ) } @Test @@ -403,23 +423,21 @@ internal class IndentationRuleTest { } @Test - fun `no indentation after lambda arrow`() { + fun `incorrect indentation after lambda arrow`() { assertThat( IndentationRule().lint( """ fun bar() { - foo.func { - param1, param2 -> - doSomething() - doSomething2() - } + Pair("val1", "val2") + .let { (first, second) -> + first + second + } } """.trimIndent() ) ).isEqualTo( listOf( - LintError(line = 4, col = 1, ruleId = "indent", detail = "Unexpected indentation (12) (should be 8)"), - LintError(line = 5, col = 1, ruleId = "indent", detail = "Unexpected indentation (12) (should be 8)") + LintError(line = 4, col = 1, ruleId = "indent", detail = "Unexpected indentation (16) (should be 12)"), ) ) } @@ -596,6 +614,171 @@ internal class IndentationRuleTest { assertThat(IndentationRule().format(code)).isEqualTo(code) } + @Test + fun `format new line before opening quotes multiline string as parameter`() { + val code = + """ + fun foo() { + println($MULTILINE_STRING_QUOTE + line1 + line2 + $MULTILINE_STRING_QUOTE.trimIndent()) + } + """.trimIndent() + val expectedCode = + """ + fun foo() { + println( + $MULTILINE_STRING_QUOTE + line1 + line2 + $MULTILINE_STRING_QUOTE.trimIndent() + ) + } + """.trimIndent() + @Suppress("RemoveCurlyBracesFromTemplate") val expectedCodeTabs = + """ + fun foo() { + ${TAB}println( + ${TAB}${TAB}$MULTILINE_STRING_QUOTE + ${TAB}${TAB}line1 + ${TAB}${TAB} line2 + ${TAB}${TAB}$MULTILINE_STRING_QUOTE.trimIndent() + ${TAB}) + } + """.trimIndent() + assertThat( + IndentationRule().lint(code) + ).isEqualTo( + listOf( + LintError(2, 13, "indent", "Missing newline after \"(\""), + LintError(5, 24, "indent", "Missing newline before \")\""), + ) + ) + assertThat(IndentationRule().format(code)).isEqualTo(expectedCode) + assertThat(IndentationRule().format(code, INDENT_STYLE_TABS)).isEqualTo(expectedCodeTabs) + } + + @Test + fun `format multiline string assignment to variable with opening quotes on same line as declaration`() { + val code = + """ + fun foo() { + val bar = $MULTILINE_STRING_QUOTE + line1 + line2 + $MULTILINE_STRING_QUOTE.trimIndent() + } + """.trimIndent() + val expectedCode = + """ + fun foo() { + val bar = $MULTILINE_STRING_QUOTE + line1 + line2 + $MULTILINE_STRING_QUOTE.trimIndent() + } + """.trimIndent() + assertThat( + IndentationRule().lint(code) + ).isEqualTo( + listOf( + LintError(5, 1, "indent", "Unexpected indent of multiline string closing quotes"), + ) + ) + assertThat(IndentationRule().format(code)).isEqualTo(expectedCode) + } + + @Test + fun `format multiline string containing quotation marks`() { + val code = + """ + fun foo() { + println($MULTILINE_STRING_QUOTE + text "" + + text + "" + $MULTILINE_STRING_QUOTE.trimIndent()) + } + """.trimIndent() + val expectedCode = + """ + fun foo() { + println( + $MULTILINE_STRING_QUOTE + text "" + + text + "" + $MULTILINE_STRING_QUOTE.trimIndent() + ) + } + """.trimIndent() + assertThat( + IndentationRule().lint(code) + ).isEqualTo( + listOf( + LintError(line = 2, col = 13, ruleId = "indent", detail = "Missing newline after \"(\""), + LintError(line = 7, col = 1, ruleId = "indent", detail = "Unexpected indent of multiline string closing quotes"), + LintError(line = 7, col = 20, ruleId = "indent", detail = "Missing newline before \")\""), + ) + ) + assertThat(IndentationRule().format(code)).isEqualTo(expectedCode) + } + + @Test + fun `format multiline string containing a template string as the first non blank element on the line`() { + // Escape '${true}' as '${"$"}{true}' to prevent evaluation before actually processing the multiline sting + val code = + """ + fun foo() { + println($MULTILINE_STRING_QUOTE + ${"$"}{true} + + ${"$"}{true} + $MULTILINE_STRING_QUOTE.trimIndent()) + } + """.trimIndent() + val expectedCode = + """ + fun foo() { + println( + $MULTILINE_STRING_QUOTE + ${"$"}{true} + + ${"$"}{true} + $MULTILINE_STRING_QUOTE.trimIndent() + ) + } + """.trimIndent() + assertThat( + IndentationRule().lint(code) + ).isEqualTo( + listOf( + LintError(line = 2, col = 13, ruleId = "indent", detail = "Missing newline after \"(\""), + LintError(line = 6, col = 1, ruleId = "indent", detail = "Unexpected indent of multiline string closing quotes"), + LintError(line = 6, col = 20, ruleId = "indent", detail = "Missing newline before \")\""), + ) + ) + assertThat(IndentationRule().format(code)).isEqualTo(expectedCode) + } + + @Test + fun `issue 575 - format multiline string with tabs after the margin is indented properly`() { + val code = + """ + val str = + $MULTILINE_STRING_QUOTE + ${TAB}Tab at the beginning of this line but after the indentation margin + Tab${TAB}in the middle of this string + Tab at the end of this line.$TAB + $MULTILINE_STRING_QUOTE.trimIndent() + """.trimIndent() + assertThat(IndentationRule().lint(code)).isEmpty() + assertThat(IndentationRule().format(code)).isEqualTo(code) + } + @Test fun `lint if-condition with line break and multiline call expression is indented properly`() { assertThat( @@ -1087,4 +1270,11 @@ internal class IndentationRuleTest { ) ).isEmpty() } + + private companion object { + const val MULTILINE_STRING_QUOTE = "${'"'}${'"'}${'"'}" + const val TAB = "${'\t'}" + + val INDENT_STYLE_TABS = mapOf("indent_style" to "tab") + } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/ParameterListWrappingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/ParameterListWrappingRuleTest.kt index 18ca843291..4984e03ab0 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/ParameterListWrappingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/ParameterListWrappingRuleTest.kt @@ -168,10 +168,10 @@ class ParameterListWrappingRuleTest { assertThat( ParameterListWrappingRule().lint( """ - fun f( - a: Any, - b: Any, c: Any - ) + fun f( + a: Any, + b: Any, c: Any + ) """.trimIndent() ) ).isEqualTo( diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundKeywordRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundKeywordRuleTest.kt index c547d5985a..379074a5b4 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundKeywordRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundKeywordRuleTest.kt @@ -110,24 +110,24 @@ class SpacingAroundKeywordRuleTest { assertThat( SpacingAroundKeywordRule().format( """ - var x: String - get () { - return "" - } - private set (value) { - x = value - } - """.trimIndent() + var x: String + get () { + return "" + } + private set (value) { + x = value + } + """.trimIndent() ) ).isEqualTo( """ var x: String - get() { - return "" - } - private set(value) { - x = value - } + get() { + return "" + } + private set(value) { + x = value + } """.trimIndent() ) } diff --git a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-raw-string-trim-indent-expected-tabs.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-raw-string-trim-indent-expected-tabs.kt.spec new file mode 100644 index 0000000000..d7eff10374 --- /dev/null +++ b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-raw-string-trim-indent-expected-tabs.kt.spec @@ -0,0 +1,115 @@ +fun f() { + val y = 5 + val x = + """ + $y + """.trimIndent() + println("""${true}""".trimIndent()) + println( + """ + """.trimIndent() + ) + println( + """ + ${true} + + ${true} + """.trimIndent() + ) + println( + """ + ${true} + + ${true} + """.trimIndent() + ) + println( + """ + text + + text + """.trimIndent().toByteArray() + ) + println( + """ + text + + text + """.trimIndent() + ) + println( + """ + text + + text + _ + """.trimIndent() + ) + println( + """ + text "" + + text + "" + """.trimIndent() + ) + format( + """ + class A { + fun f(@Annotation + a: Any, + @Annotation([ + "v1", + "v2" + ]) + b: Any, + c: Any = + false, + @Annotation d: Any) { + } + } + """.trimIndent() + ) + write( + fs.getPath("/projects/.editorconfig"), + """ + root = true + [*] + end_of_line = lf + """.trimIndent().toByteArray() + ) +} + +class C { + val CONFIG_COMPACT = """ + { + } + """.trimIndent() + val CONFIG_COMPACT = // comment + """ + { + } + """.trimIndent() + + fun getBazelWorkspaceContent(blueprint: BazelWorkspaceBlueprint) = + """${Target( + "android_sdk_repository", + listOf(StringAttribute("name", "androidsdk")) + )} + +${Comment("Google Maven Repository")} +${LoadStatement("@bazel_tools//tools/build_defs/repo:http.bzl", listOf("http_archive"))} +${AssignmentStatement("GMAVEN_TAG", "\"${blueprint.gmavenRulesTag}\"")} +${Target( + "http_archive", + listOf( + StringAttribute("name", "gmaven_rules"), + RawAttribute("strip_prefix", "\"gmaven_rules-%s\" % GMAVEN_TAG"), + RawAttribute("urls", "[\"https://github.com/bazelbuild/gmaven_rules/archive/%s.tar.gz\" % GMAVEN_TAG]") + ) + )} +${LoadStatement("@gmaven_rules//:gmaven.bzl", listOf("gmaven_rules"))} +${Target("gmaven_rules", listOf())} +""" + +} diff --git a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-raw-string-trim-indent-expected.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-raw-string-trim-indent-expected.kt.spec index 16fe69e9a5..79ea8cc075 100644 --- a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-raw-string-trim-indent-expected.kt.spec +++ b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-raw-string-trim-indent-expected.kt.spec @@ -50,8 +50,8 @@ _ text "" text - """.trimIndent(), "" + """.trimIndent() ) format( """ @@ -78,17 +78,6 @@ _ end_of_line = lf """.trimIndent().toByteArray() ) - SpacingAroundKeywordRule().format( // string below is tab-indented - """ - var x: String - get () { - return "" - } - private set (value) { - x = value - } - """.trimIndent() - ) } class C { diff --git a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-raw-string-trim-indent.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-raw-string-trim-indent.kt.spec index 47fcf9be9e..3a7decb496 100644 --- a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-raw-string-trim-indent.kt.spec +++ b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-raw-string-trim-indent.kt.spec @@ -37,8 +37,8 @@ _""".trimIndent()) text "" text - """.trimIndent(), "" + """.trimIndent() ) format( """ @@ -62,17 +62,6 @@ _""".trimIndent()) [*] end_of_line = lf """.trimIndent().toByteArray()) - SpacingAroundKeywordRule().format( // string below is tab-indented - """ - var x: String - get () { - return "" - } - private set (value) { - x = value - } - """.trimIndent() - ) } class C { diff --git a/ktlint-test/src/main/kotlin/com/pinterest/ktlint/test/RuleExtension.kt b/ktlint-test/src/main/kotlin/com/pinterest/ktlint/test/RuleExtension.kt index 347353a6da..87daae0733 100644 --- a/ktlint-test/src/main/kotlin/com/pinterest/ktlint/test/RuleExtension.kt +++ b/ktlint-test/src/main/kotlin/com/pinterest/ktlint/test/RuleExtension.kt @@ -5,6 +5,7 @@ import com.pinterest.ktlint.core.LintError import com.pinterest.ktlint.core.Rule import com.pinterest.ktlint.core.RuleSet import java.util.ArrayList +import org.assertj.core.api.Assertions import org.assertj.core.util.diff.DiffUtils.diff import org.assertj.core.util.diff.DiffUtils.generateUnifiedDiff @@ -121,6 +122,30 @@ public fun Rule.diffFileFormat( return diff.ifEmpty { "" } } +/** + * Alternative to [diffFileFormat]. Depending on your personal favor it might be more insightful whenever a test is + * failing. Currently it is offered as utility so it can be used during development. + * + * To be used as: + * + * @Test + * fun testFormatRawStringTrimIndent() { + * IndentationRule().assertThatFileFormat( + * "spec/indent/format-raw-string-trim-indent.kt.spec", + * "spec/indent/format-raw-string-trim-indent-expected.kt.spec" + * ) + * } + */ +public fun Rule.assertThatFileFormat( + srcPath: String, + expectedPath: String, + userData: Map = emptyMap() +) { + val actual = format(getResourceAsText(srcPath), userData, script = true).split('\n') + val expected = getResourceAsText(expectedPath).split('\n') + Assertions.assertThat(actual).isEqualTo(expected) +} + private fun getResourceAsText(path: String) = (ClassLoader.getSystemClassLoader().getResourceAsStream(path) ?: throw RuntimeException("$path not found")) .bufferedReader()