All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.pinterest.ktlint.ruleset.standard.rules.ArgumentListWrappingRule.kt Maven / Gradle / Ivy

There is a newer version: 1.5.0
Show newest version
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