com.pinterest.ktlint.ruleset.standard.rules.ArgumentListWrappingRule.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ktlint-ruleset-standard Show documentation
Show all versions of ktlint-ruleset-standard Show documentation
An anti-bikeshedding Kotlin linter with built-in formatter.
package com.pinterest.ktlint.ruleset.standard.rules
import com.pinterest.ktlint.rule.engine.core.api.ElementType
import com.pinterest.ktlint.rule.engine.core.api.ElementType.BINARY_EXPRESSION
import com.pinterest.ktlint.rule.engine.core.api.ElementType.ELSE
import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_ARGUMENT_LIST
import com.pinterest.ktlint.rule.engine.core.api.IndentConfig
import com.pinterest.ktlint.rule.engine.core.api.Rule.VisitorModifier.RunAfterRule.Mode.REGARDLESS_WHETHER_RUN_AFTER_RULE_IS_LOADED_OR_DISABLED
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import com.pinterest.ktlint.rule.engine.core.api.SinceKtlint
import com.pinterest.ktlint.rule.engine.core.api.SinceKtlint.Status.STABLE
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.editorconfig.MAX_LINE_LENGTH_PROPERTY
import com.pinterest.ktlint.rule.engine.core.api.indent
import com.pinterest.ktlint.rule.engine.core.api.isPartOf
import com.pinterest.ktlint.rule.engine.core.api.isPartOfComment
import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpace
import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpaceWithNewline
import com.pinterest.ktlint.rule.engine.core.api.lineLengthWithoutNewlinePrefix
import com.pinterest.ktlint.rule.engine.core.api.prevLeaf
import com.pinterest.ktlint.ruleset.standard.StandardRule
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl
import org.jetbrains.kotlin.psi.KtContainerNode
import org.jetbrains.kotlin.psi.KtDoWhileExpression
import org.jetbrains.kotlin.psi.KtIfExpression
import org.jetbrains.kotlin.psi.KtWhileExpression
import org.jetbrains.kotlin.psi.psiUtil.children
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
/**
* https://kotlinlang.org/docs/reference/coding-conventions.html#method-call-formatting
*
* The rule is more aggressive in inserting newlines after arguments than mentioned in the styleguide:
* Each argument should be on a separate line if
* - at least one of the arguments is
* - maxLineLength exceeded (and separating arguments with \n would actually help)
* in addition, "(" and ")" must be on separates line if any of the arguments are (otherwise on the same)
*/
@SinceKtlint("0.1", STABLE)
public class ArgumentListWrappingRule :
StandardRule(
id = "argument-list-wrapping",
visitorModifiers =
setOf(
// ArgumentListWrapping should only be used in case the max_line_length is still violated after running rules below:
VisitorModifier.RunAfterRule(
ruleId = WRAPPING_RULE_ID,
mode = REGARDLESS_WHETHER_RUN_AFTER_RULE_IS_LOADED_OR_DISABLED,
),
VisitorModifier.RunAfterRule(
ruleId = CLASS_SIGNATURE_RULE_ID,
mode = REGARDLESS_WHETHER_RUN_AFTER_RULE_IS_LOADED_OR_DISABLED,
),
),
usesEditorConfigProperties =
setOf(
INDENT_SIZE_PROPERTY,
INDENT_STYLE_PROPERTY,
MAX_LINE_LENGTH_PROPERTY,
),
) {
private var editorConfigIndent = IndentConfig.DEFAULT_INDENT_CONFIG
private var maxLineLength = MAX_LINE_LENGTH_PROPERTY.defaultValue
override fun beforeFirstNode(editorConfig: EditorConfig) {
editorConfigIndent =
IndentConfig(
indentStyle = editorConfig[INDENT_STYLE_PROPERTY],
tabWidth = editorConfig[INDENT_SIZE_PROPERTY],
)
maxLineLength = editorConfig[MAX_LINE_LENGTH_PROPERTY]
}
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
) {
if (editorConfigIndent.disabled) {
return
}
if (node.elementType == VALUE_ARGUMENT_LIST) {
if (needToWrapArgumentList(node)) {
node
.children()
.forEach { child -> wrapArgumentInList(child, emit, autoCorrect) }
}
}
}
private fun needToWrapArgumentList(node: ASTNode) =
if ( // skip when there are no arguments
node.firstChildNode?.treeNext?.elementType != ElementType.RPAR &&
// skip lambda arguments
node.treeParent?.elementType != ElementType.FUNCTION_LITERAL &&
// skip if number of arguments is big (we assume it with a magic number of 8)
node.children().count { it.elementType == ElementType.VALUE_ARGUMENT } <= 8 &&
// skip if part of a value argument list. It depends on the situation whether it is better to wrap the arguments in the list
// or the operators in the binary expression
!node.isPartOf(BINARY_EXPRESSION)
) {
// each argument should be on a separate line if
// - at least one of the arguments is
// - maxLineLength exceeded (and separating arguments with \n would actually help)
// in addition, "(" and ")" must be on separates line if any of the arguments are (otherwise on the same)
node.textContainsIgnoringLambda('\n') || node.exceedsMaxLineLength()
} else {
false
}
private fun ASTNode.exceedsMaxLineLength() = lineLengthWithoutNewlinePrefix() > maxLineLength && !textContains('\n')
private fun intendedIndent(child: ASTNode): String =
when {
// IDEA quirk:
// generic<
// T,
// R>(
// 1,
// 2
// )
// instead of
// generic<
// T,
// R>(
// 1,
// 2
// )
child.treeParent.hasTypeArgumentListInFront() -> -1
// IDEA quirk:
// foo
// .bar = Baz(
// 1,
// 2
// )
// instead of
// foo
// .bar = Baz(
// 1,
// 2
// )
child.treeParent.isPartOfDotQualifiedAssignmentExpression() -> -1
else -> 0
}.let {
if (child.treeParent.isOnSameLineAsControlFlowKeyword()) {
it + 1
} else {
it
}
}.let {
if (child.elementType == ElementType.VALUE_ARGUMENT) {
it + 1
} else {
it
}
}.let { indentLevelFix ->
val indentLevel =
editorConfigIndent
.indentLevelFrom(child.treeParent.indent(false))
.plus(indentLevelFix)
"\n" + editorConfigIndent.indent.repeat(indentLevel)
}
private fun wrapArgumentInList(
child: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean,
) {
when (child.elementType) {
ElementType.LPAR -> {
val prevLeaf = child.prevLeaf()
if (prevLeaf is PsiWhiteSpace && prevLeaf.textContains('\n')) {
emit(child.startOffset, errorMessage(child), true)
if (autoCorrect) {
prevLeaf.delete()
}
}
}
ElementType.VALUE_ARGUMENT,
ElementType.RPAR,
-> {
// aiming for
// ... LPAR
// VALUE_PARAMETER...
// RPAR
val intendedIndent = intendedIndent(child)
val prevLeaf = child.prevWhiteSpaceWithNewLine() ?: child.prevLeaf()
if (prevLeaf is PsiWhiteSpace) {
if (prevLeaf.getText().contains("\n")) {
// The current child is already wrapped to a new line. Checking and fixing the
// correct size of the indent is the responsibility of the IndentationRule.
return
} else {
// The current child needs to be wrapped to a newline.
emit(child.startOffset, errorMessage(child), true)
if (autoCorrect) {
// The indentation is purely based on the previous leaf only. Note that in
// autoCorrect mode the indent rule, if enabled, runs after this rule and
// determines the final indentation. But if the indent rule is disabled then the
// indent of this rule is kept.
(prevLeaf as LeafPsiElement).rawReplaceWithText(intendedIndent)
}
}
} else {
// Insert a new whitespace element in order to wrap the current child to a new line.
emit(child.startOffset, errorMessage(child), true)
if (autoCorrect) {
child.treeParent.addChild(PsiWhiteSpaceImpl(intendedIndent), child)
}
}
// Indentation of child nodes need to be fixed by the IndentationRule.
}
}
}
private fun errorMessage(node: ASTNode) =
when (node.elementType) {
ElementType.LPAR -> """Unnecessary newline before "(""""
ElementType.VALUE_ARGUMENT ->
"Argument should be on a separate line (unless all arguments can fit a single line)"
ElementType.RPAR -> """Missing newline before ")""""
else -> throw UnsupportedOperationException()
}
private fun ASTNode.textContainsIgnoringLambda(char: Char): Boolean =
children().any { child ->
val elementType = child.elementType
elementType == ElementType.WHITE_SPACE && child.textContains(char) ||
elementType == ElementType.COLLECTION_LITERAL_EXPRESSION && child.textContains(char) ||
elementType == ElementType.VALUE_ARGUMENT && child.children().any { it.textContainsIgnoringLambda(char) }
}
private fun ASTNode.hasTypeArgumentListInFront(): Boolean =
treeParent
.children()
.firstOrNull { it.elementType == ElementType.TYPE_ARGUMENT_LIST }
?.children()
?.any { it.isWhiteSpaceWithNewline() } == true
private fun ASTNode.isPartOfDotQualifiedAssignmentExpression(): Boolean =
treeParent?.treeParent?.elementType == BINARY_EXPRESSION &&
treeParent?.treeParent?.children()?.find { it.elementType == ElementType.DOT_QUALIFIED_EXPRESSION } != null
private fun ASTNode.prevWhiteSpaceWithNewLine(): ASTNode? {
var prev = prevLeaf()
while (prev != null && (prev.isWhiteSpace() || prev.isPartOfComment())) {
if (prev.isWhiteSpaceWithNewline()) {
return prev
}
prev = prev.prevLeaf()
}
return null
}
private fun ASTNode.isOnSameLineAsControlFlowKeyword(): Boolean {
val containerNode = psi.getStrictParentOfType() ?: return false
if (containerNode.node.elementType == ELSE) return false
val controlFlowKeyword =
when (val parent = containerNode.parent) {
is KtIfExpression -> parent.ifKeyword.node
is KtWhileExpression -> parent.firstChild.node
is KtDoWhileExpression -> parent.whileKeyword?.node
else -> null
} ?: return false
var prevLeaf = prevLeaf() ?: return false
while (prevLeaf != controlFlowKeyword) {
if (prevLeaf.isWhiteSpaceWithNewline()) return false
prevLeaf = prevLeaf.prevLeaf() ?: return false
}
return true
}
}
public val ARGUMENT_LIST_WRAPPING_RULE_ID: RuleId = ArgumentListWrappingRule().ruleId
© 2015 - 2024 Weber Informatics LLC | Privacy Policy