diff --git a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt index 18dd656fd7..36e84c3886 100644 --- a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt +++ b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt @@ -518,7 +518,7 @@ internal class KtlintCommandLine { } catch (e: Exception) { if (code.isStdIn && e is KtLintParseException) { if (code.script) { - // When reading from stdin, code is only parsed as Kotlint script, if it could not be parsed as pure Kotlin. Now parsing + // When reading from stdin, code is only parsed as Kotlin script, if it could not be parsed as pure Kotlin. Now parsing // of the code has failed for both, the file has to be ignored. logger.error { """ diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/StringTemplateIndentRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/StringTemplateIndentRule.kt index 687d0a920a..f07c4923dd 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/StringTemplateIndentRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/StringTemplateIndentRule.kt @@ -2,10 +2,13 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.rule.engine.core.api.ElementType.CALL_EXPRESSION import com.pinterest.ktlint.rule.engine.core.api.ElementType.CLOSING_QUOTE +import com.pinterest.ktlint.rule.engine.core.api.ElementType.COMMA import com.pinterest.ktlint.rule.engine.core.api.ElementType.DOT +import com.pinterest.ktlint.rule.engine.core.api.ElementType.DOT_QUALIFIED_EXPRESSION import com.pinterest.ktlint.rule.engine.core.api.ElementType.LITERAL_STRING_TEMPLATE_ENTRY import com.pinterest.ktlint.rule.engine.core.api.ElementType.OPEN_QUOTE import com.pinterest.ktlint.rule.engine.core.api.ElementType.REGULAR_STRING_PART +import com.pinterest.ktlint.rule.engine.core.api.ElementType.RETURN_KEYWORD import com.pinterest.ktlint.rule.engine.core.api.ElementType.STRING_TEMPLATE import com.pinterest.ktlint.rule.engine.core.api.ElementType.WHITE_SPACE import com.pinterest.ktlint.rule.engine.core.api.IndentConfig @@ -20,11 +23,14 @@ import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_STYLE_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.firstChildLeafOrSelf +import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpaceWithNewline +import com.pinterest.ktlint.rule.engine.core.api.lastChildLeafOrSelf import com.pinterest.ktlint.rule.engine.core.api.nextCodeSibling import com.pinterest.ktlint.rule.engine.core.api.nextLeaf import com.pinterest.ktlint.rule.engine.core.api.nextSibling +import com.pinterest.ktlint.rule.engine.core.api.prevCodeLeaf import com.pinterest.ktlint.rule.engine.core.api.prevLeaf -import com.pinterest.ktlint.rule.engine.core.api.upsertWhitespaceAfterMe +import com.pinterest.ktlint.rule.engine.core.api.upsertWhitespaceBeforeMe import com.pinterest.ktlint.ruleset.standard.StandardRule import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement @@ -38,7 +44,7 @@ public class StringTemplateIndentRule : visitorModifiers = setOf( // Wrap all multiline string templates to a separate line - VisitorModifier.RunAfterRule(MULTILINE_EXPRESSION_WRAPPING_RULE_ID, ONLY_WHEN_RUN_AFTER_RULE_IS_LOADED_AND_ENABLED), +// VisitorModifier.RunAfterRule(MULTILINE_EXPRESSION_WRAPPING_RULE_ID, ONLY_WHEN_RUN_AFTER_RULE_IS_LOADED_AND_ENABLED), // The IndentationRule first needs to fix the indentation of the opening quotes of the string template. The indentation inside // the string template is relative to the opening quotes. Running this rule before the IndentationRule results in a wrong // indentation whenever the indent level of the root of the string template is changed. @@ -51,12 +57,13 @@ public class StringTemplateIndentRule : ), ), Rule.OfficialCodeStyle { + private lateinit var indentConfig: IndentConfig private lateinit var nextIndent: String private lateinit var wrongIndentChar: String private lateinit var wrongIndentDescription: String override fun beforeFirstNode(editorConfig: EditorConfig) { - val indentConfig = + indentConfig = IndentConfig( indentStyle = editorConfig[INDENT_STYLE_PROPERTY], tabWidth = editorConfig[INDENT_SIZE_PROPERTY], @@ -83,24 +90,66 @@ public class StringTemplateIndentRule : autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, ) { - if (node.elementType == STRING_TEMPLATE) { - val psi = node.psi as KtStringTemplateExpression - if (psi.isMultiLine() && psi.isFollowedByTrimIndent()) { - 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 raw string literal should not contain both tab(s) and space(s)", false) - return - } + node + .takeIf { it.elementType == STRING_TEMPLATE } + ?.let { stringTemplate -> + val psi = stringTemplate.psi as KtStringTemplateExpression + if (psi.isMultiLine() && psi.isFollowedByTrimIndent()) { + stringTemplate + .takeUnless { it.isPrecededByWhitespaceWithNewline() } + ?.takeUnless { it.isPrecededByReturnKeyword() } + ?.let { whiteSpace -> + emit(stringTemplate.startOffset, "Expected newline before multiline string template", true) + if (autoCorrect) { + whiteSpace.upsertWhitespaceBeforeMe(indentConfig.childIndentOf(whiteSpace.treeParent)) + } + } + stringTemplate + .getFirstLeafAfterTrimIndent() + ?.takeUnless { it.isWhiteSpaceWithNewline() } + ?.takeUnless { it.elementType == COMMA } + ?.takeUnless { it.treeParent.elementType == DOT_QUALIFIED_EXPRESSION } + ?.let { nextLeaf -> + emit(nextLeaf.startOffset, "Expected newline after multiline string template", true) + if (autoCorrect) { + nextLeaf.upsertWhitespaceBeforeMe(indentConfig.childIndentOf(stringTemplate.treeParent)) + } + } + + if (stringTemplate.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( + stringTemplate.startOffset, + "Indentation of multiline raw string literal should not contain both tab(s) and space(s)", + false, + ) + return + } - val indent = node.getIndent() -// indentWhiteSpaceBeforeStringTemplate(node, indent, emit, autoCorrect) - indentStringTemplate(node, indent, emit, autoCorrect) + val indent = stringTemplate.getIndent() + indentStringTemplate(node, indent, emit, autoCorrect) + } } - } } + private fun ASTNode.getFirstLeafAfterTrimIndent() = + takeIf { it.elementType == STRING_TEMPLATE } + ?.takeIf { (it.psi as KtStringTemplateExpression).isFollowedByTrimIndent() } + ?.treeParent + ?.lastChildLeafOrSelf() + ?.nextLeaf() + + private fun ASTNode.isPrecededByWhitespaceWithNewline() = prevLeaf().isWhiteSpaceWithNewline() + + private fun ASTNode.isPrecededByReturnKeyword() = + // Allow below as otherwise it results in compilation failure: + // return """ + // some string + // """ + prevCodeLeaf()?.elementType == RETURN_KEYWORD + private fun ASTNode.getIndent(): String { // When executing this rule, the indentation rule may not have run yet. The functionality to determine the correct indentation level // is out of scope of this rule as it is owned by the indentation rule. Therefore, the indentation of the line at which the @@ -149,26 +198,6 @@ public class StringTemplateIndentRule : .filterNot { it.isBlank() } } - private fun indentWhiteSpaceBeforeStringTemplate( - node: ASTNode, - indent: String, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, - autoCorrect: Boolean, - ) { - val prevLeaf = node.prevLeaf()!! - if (!prevLeaf.textContains('\n')) { - emit(prevLeaf.startOffset + 1, """Missing newline before raw string literal""", true) - } else if (prevLeaf.getTextAfterLastNewline() != indent) { - emit(prevLeaf.startOffset + 1, "Unexpected indent before opening quotes of raw string literal", true) - } else { - return - } - - if (autoCorrect) { - prevLeaf.upsertWhitespaceAfterMe("\n" + indent) - } - } - private fun indentStringTemplate( node: ASTNode, newIndent: String, diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/StringTemplateIndentRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/StringTemplateIndentRuleTest.kt index 0517d95c15..1635e9f061 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/StringTemplateIndentRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/StringTemplateIndentRuleTest.kt @@ -10,6 +10,7 @@ import com.pinterest.ktlint.test.TAB import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +@Suppress("RemoveCurlyBracesFromTemplate") class StringTemplateIndentRuleTest { private val stringTemplateIndentRuleAssertThat = assertThatRuleBuilder { StringTemplateIndentRule() } @@ -36,6 +37,7 @@ class StringTemplateIndentRuleTest { """.trimIndent() stringTemplateIndentRuleAssertThat(code) .hasLintViolations( + LintViolation(3, 12, "Unexpected indent of raw string literal"), LintViolation(3, 12, "Unexpected indent of raw string literal"), LintViolation(4, 12, "Unexpected indent of raw string literal"), ).isFormattedAs(formattedCode) @@ -59,6 +61,7 @@ class StringTemplateIndentRuleTest { stringTemplateIndentRuleAssertThat(code) .withEditorConfigOverride(INDENT_STYLE_PROPERTY to IndentConfig.IndentStyle.TAB) .hasLintViolations( + LintViolation(1, 22, "Expected newline before multiline string template"), LintViolation(2, 5, "Unexpected indent of raw string literal"), LintViolation(3, 5, "Unexpected indent of raw string literal"), ).isFormattedAs(formattedCode) @@ -66,7 +69,6 @@ class StringTemplateIndentRuleTest { @Nested inner class `A multiline raw string literal containing both tabs and spaces in the indentation margin can not be autocorrected` { - @Suppress("RemoveCurlyBracesFromTemplate") @Test fun `Mixed indentation characters on inner lines`() { val code = @@ -76,14 +78,13 @@ class StringTemplateIndentRuleTest { ${TAB} line2 $MULTILINE_STRING_QUOTE.trimIndent() """.trimIndent() - stringTemplateIndentRuleAssertThat(code).hasLintViolationWithoutAutoCorrect( - 1, - 11, - "Indentation of multiline raw string literal should not contain both tab(s) and space(s)", - ) + stringTemplateIndentRuleAssertThat(code) + .hasLintViolations( + LintViolation(1, 11, "Expected newline before multiline string template"), + LintViolation(1, 11, "Indentation of multiline raw string literal should not contain both tab(s) and space(s)", false), + ) } - @Suppress("RemoveCurlyBracesFromTemplate", "RemoveCurlyBracesFromTemplate") @Test fun `Mixed indentation characters including line with opening quotes`() { val code = @@ -92,14 +93,13 @@ class StringTemplateIndentRuleTest { ${TAB} line2 $MULTILINE_STRING_QUOTE.trimIndent() """.trimIndent() - stringTemplateIndentRuleAssertThat(code).hasLintViolationWithoutAutoCorrect( - 1, - 11, - "Indentation of multiline raw string literal should not contain both tab(s) and space(s)", - ) + stringTemplateIndentRuleAssertThat(code) + .hasLintViolations( + LintViolation(1, 11, "Expected newline before multiline string template"), + LintViolation(1, 11, "Indentation of multiline raw string literal should not contain both tab(s) and space(s)", false), + ) } - @Suppress("RemoveCurlyBracesFromTemplate") @Test fun `Mixed indentation characters including line with closing quotes`() { val code = @@ -108,11 +108,11 @@ class StringTemplateIndentRuleTest { ${TAB}line1 ${TAB} line2$MULTILINE_STRING_QUOTE.trimIndent() """.trimIndent() - stringTemplateIndentRuleAssertThat(code).hasLintViolationWithoutAutoCorrect( - 1, - 11, - "Indentation of multiline raw string literal should not contain both tab(s) and space(s)", - ) + stringTemplateIndentRuleAssertThat(code) + .hasLintViolations( + LintViolation(1, 11, "Expected newline before multiline string template"), + LintViolation(1, 11, "Indentation of multiline raw string literal should not contain both tab(s) and space(s)", false), + ) } } @@ -156,6 +156,7 @@ class StringTemplateIndentRuleTest { """.trimIndent() stringTemplateIndentRuleAssertThat(code) .hasLintViolations( + LintViolation(1, 15, "Expected newline before multiline string template"), LintViolation(2, 15, "Unexpected indent of raw string literal"), LintViolation(3, 15, "Unexpected indent of raw string literal"), LintViolation(3, 20, "Missing newline before the closing quotes of the raw string literal"), @@ -179,6 +180,7 @@ class StringTemplateIndentRuleTest { """.trimIndent() stringTemplateIndentRuleAssertThat(code) .hasLintViolations( + LintViolation(1, 15, "Expected newline before multiline string template"), LintViolation(2, 15, "Unexpected indent of raw string literal"), LintViolation(3, 15, "Unexpected indent of raw string literal"), ).isFormattedAs(formattedCode) @@ -206,8 +208,10 @@ class StringTemplateIndentRuleTest { """.trimIndent() stringTemplateIndentRuleAssertThat(code) .hasLintViolations( + LintViolation(2, 13, "Expected newline before multiline string template"), LintViolation(3, 13, "Unexpected indent of raw string literal"), LintViolation(4, 13, "Unexpected indent of raw string literal"), + LintViolation(4, 29, "Expected newline after multiline string template"), ).isFormattedAs(formattedCode) } @@ -238,6 +242,7 @@ class StringTemplateIndentRuleTest { """.trimIndent() stringTemplateIndentRuleAssertThat(code) .hasLintViolations( + LintViolation(2, 13, "Expected newline before multiline string template"), LintViolation(3, 1, "Unexpected indent of raw string literal"), LintViolation(5, 1, "Unexpected indent of raw string literal"), LintViolation(6, 1, "Unexpected indent of raw string literal"), @@ -263,13 +268,13 @@ class StringTemplateIndentRuleTest { """.trimIndent() stringTemplateIndentRuleAssertThat(code) .hasLintViolations( + LintViolation(1, 11, "Expected newline before multiline string template"), LintViolation(2, 1, "Unexpected 'tab' character(s) in margin of multiline string"), LintViolation(3, 1, "Unexpected 'tab' character(s) in margin of multiline string"), LintViolation(4, 1, "Unexpected indent of raw string literal"), ).isFormattedAs(formattedCode) } - @Suppress("RemoveCurlyBracesFromTemplate") @Test fun `Format multiline string without tabs in the indentation margin`() { val code = @@ -291,6 +296,7 @@ class StringTemplateIndentRuleTest { """.trimIndent() stringTemplateIndentRuleAssertThat(code) .hasLintViolations( + LintViolation(1, 11, "Expected newline before multiline string template"), LintViolation(2, 7, "Unexpected indent of raw string literal"), LintViolation(3, 7, "Unexpected indent of raw string literal"), LintViolation(4, 7, "Unexpected indent of raw string literal"), @@ -319,6 +325,7 @@ class StringTemplateIndentRuleTest { """.trimIndent() stringTemplateIndentRuleAssertThat(code) .hasLintViolations( + LintViolation(2, 15, "Expected newline before multiline string template"), LintViolation(3, 15, "Unexpected indent of raw string literal"), LintViolation(4, 15, "Unexpected indent of raw string literal"), ).isFormattedAs(formattedCode) @@ -328,10 +335,11 @@ class StringTemplateIndentRuleTest { fun `lint property delegate is indented properly 4`() { val code = """ - fun lazyString() = lazy { $MULTILINE_STRING_QUOTE - someText - $MULTILINE_STRING_QUOTE.trimIndent() - } + fun lazyString() = + lazy { $MULTILINE_STRING_QUOTE + someText + $MULTILINE_STRING_QUOTE.trimIndent() + } """.trimIndent() val formattedCode = """ @@ -344,8 +352,9 @@ class StringTemplateIndentRuleTest { """.trimIndent() stringTemplateIndentRuleAssertThat(code) .hasLintViolations( - LintViolation(2, 27, "Unexpected indent of raw string literal"), - LintViolation(3, 27, "Unexpected indent of raw string literal"), + LintViolation(2, 12, "Expected newline before multiline string template"), + LintViolation(3, 12, "Unexpected indent of raw string literal"), + LintViolation(4, 12, "Unexpected indent of raw string literal"), ).isFormattedAs(formattedCode) } @@ -372,6 +381,7 @@ class StringTemplateIndentRuleTest { """.trimIndent() stringTemplateIndentRuleAssertThat(code) .hasLintViolations( + LintViolation(2, 26, "Expected newline before multiline string template"), LintViolation(3, 26, "Unexpected indent of raw string literal"), LintViolation(4, 26, "Unexpected indent of raw string literal"), ).isFormattedAs(formattedCode) @@ -381,14 +391,15 @@ class StringTemplateIndentRuleTest { fun `lint parameter with multiline string raw string literal after arrow`() { val code = """ - val result = when { - someBooleanFunction() -> $MULTILINE_STRING_QUOTE - someText - $MULTILINE_STRING_QUOTE.trimIndent() - else -> $MULTILINE_STRING_QUOTE - someOtherText - $MULTILINE_STRING_QUOTE.trimIndent() - } + val result = + when { + someBooleanFunction() -> $MULTILINE_STRING_QUOTE + someText + $MULTILINE_STRING_QUOTE.trimIndent() + else -> $MULTILINE_STRING_QUOTE + someOtherText + $MULTILINE_STRING_QUOTE.trimIndent() + } """.trimIndent() val formattedCode = """ @@ -406,10 +417,32 @@ class StringTemplateIndentRuleTest { """.trimIndent() stringTemplateIndentRuleAssertThat(code) .hasLintViolations( - LintViolation(3, 30, "Unexpected indent of raw string literal"), - LintViolation(4, 30, "Unexpected indent of raw string literal"), - LintViolation(6, 13, "Unexpected indent of raw string literal"), - LintViolation(7, 13, "Unexpected indent of raw string literal"), + LintViolation(3, 34, "Expected newline before multiline string template"), + LintViolation(4, 34, "Unexpected indent of raw string literal"), + LintViolation(5, 34, "Unexpected indent of raw string literal"), + LintViolation(6, 17, "Expected newline before multiline string template"), + LintViolation(7, 17, "Unexpected indent of raw string literal"), + LintViolation(8, 17, "Unexpected indent of raw string literal"), ).isFormattedAs(formattedCode) } + + @Test + fun `Given a multiline raw string literal and trimIndent is followed by another dot qualified expression`() { + val code = + """ + val foo = $MULTILINE_STRING_QUOTE + someText + $MULTILINE_STRING_QUOTE.trimIndent().lowercase() + """.trimIndent() + val formattedCode = + """ + val foo = + $MULTILINE_STRING_QUOTE + someText + $MULTILINE_STRING_QUOTE.trimIndent().lowercase() + """.trimIndent() + stringTemplateIndentRuleAssertThat(code) + .hasLintViolation(1, 11, "Expected newline before multiline string template") + .isFormattedAs(formattedCode) + } }