Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Break dependency between string-template-indent and multiline-expression-wrapping #2505

Merged
merged 3 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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],
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Loading