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

com.pinterest.ktlint.ruleset.standard.rules.ParameterListWrappingRule.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.AutocorrectDecision
import com.pinterest.ktlint.rule.engine.core.api.ElementType
import com.pinterest.ktlint.rule.engine.core.api.ElementType.FUN
import com.pinterest.ktlint.rule.engine.core.api.ElementType.FUNCTION_LITERAL
import com.pinterest.ktlint.rule.engine.core.api.ElementType.FUNCTION_TYPE
import com.pinterest.ktlint.rule.engine.core.api.ElementType.LPAR
import com.pinterest.ktlint.rule.engine.core.api.ElementType.NULLABLE_TYPE
import com.pinterest.ktlint.rule.engine.core.api.ElementType.RPAR
import com.pinterest.ktlint.rule.engine.core.api.ElementType.TYPE_PARAMETER_LIST
import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_PARAMETER
import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_PARAMETER_LIST
import com.pinterest.ktlint.rule.engine.core.api.IndentConfig
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.column
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue.ktlint_official
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.firstChildLeafOrSelf
import com.pinterest.ktlint.rule.engine.core.api.ifAutocorrectAllowed
import com.pinterest.ktlint.rule.engine.core.api.indent
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.leavesOnLine
import com.pinterest.ktlint.rule.engine.core.api.nextLeaf
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.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.psiUtil.children
import org.jetbrains.kotlin.psi.psiUtil.leaves

@SinceKtlint("0.16", STABLE)
public class ParameterListWrappingRule :
    StandardRule(
        id = "parameter-list-wrapping",
        usesEditorConfigProperties =
            setOf(
                CODE_STYLE_PROPERTY,
                INDENT_SIZE_PROPERTY,
                INDENT_STYLE_PROPERTY,
                MAX_LINE_LENGTH_PROPERTY,
            ),
    ) {
    private var codeStyle = CODE_STYLE_PROPERTY.defaultValue
    private var indentConfig = IndentConfig.DEFAULT_INDENT_CONFIG
    private var maxLineLength = MAX_LINE_LENGTH_PROPERTY.defaultValue

    override fun beforeFirstNode(editorConfig: EditorConfig) {
        codeStyle = editorConfig[CODE_STYLE_PROPERTY]
        maxLineLength = editorConfig.maxLineLength()
        indentConfig =
            IndentConfig(
                indentStyle = editorConfig[INDENT_STYLE_PROPERTY],
                tabWidth = editorConfig[INDENT_SIZE_PROPERTY],
            )
        if (indentConfig.disabled) {
            stopTraversalOfAST()
        }
    }

    override fun beforeVisitChildNodes(
        node: ASTNode,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision,
    ) {
        when (node.elementType) {
            NULLABLE_TYPE -> visitNullableType(node, emit)
            VALUE_PARAMETER_LIST -> visitParameterList(node, emit)
        }
    }

    private fun visitNullableType(
        node: ASTNode,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision,
    ) {
        require(node.elementType == NULLABLE_TYPE)
        node
            .takeUnless {
                // skip when max line length is not exceeded
                (node.column - 1 + node.textLength) <= maxLineLength
            }?.takeUnless { it.textContains('\n') }
            ?.takeIf { it.isFunctionTypeWithNonEmptyValueParameterList() }
            ?.let { nullableType ->
                nullableType
                    .findChildByType(LPAR)
                    ?.takeUnless { it.nextLeaf()?.isWhiteSpaceWithNewline() == true }
                    ?.let { lpar ->
                        emit(
                            lpar.startOffset + 1,
                            "Expected new line before function type as it does not fit on a single line",
                            true,
                        ).ifAutocorrectAllowed {
                            lpar.upsertWhitespaceAfterMe(indentConfig.childIndentOf(node))
                        }
                    }
                nullableType
                    .findChildByType(RPAR)
                    ?.takeUnless { it.prevLeaf()?.isWhiteSpaceWithNewline() == true }
                    ?.let { rpar ->
                        emit(
                            rpar.startOffset,
                            "Expected new line after function type as it does not fit on a single line",
                            true,
                        ).ifAutocorrectAllowed {
                            rpar.upsertWhitespaceBeforeMe(indentConfig.parentIndentOf(node))
                        }
                    }
            }
    }

    private fun ASTNode.isFunctionTypeWithNonEmptyValueParameterList() =
        null !=
            findChildByType(FUNCTION_TYPE)
                ?.findChildByType(VALUE_PARAMETER_LIST)
                ?.findChildByType(VALUE_PARAMETER)

    private fun ASTNode.needToWrapParameterList() =
        when {
            hasNoParameters() -> {
                false
            }

            codeStyle != ktlint_official && isPartOfFunctionLiteralInNonKtlintOfficialCodeStyle() -> {
                false
            }

            codeStyle == ktlint_official && containsAnnotatedParameter() -> {
                true
            }

            codeStyle == ktlint_official &&
                isPartOfFunctionLiteralStartingOnSameLineAsClosingParenthesisOfPrecedingReferenceExpression() -> {
                false
            }

            textContains('\n') -> {
                true
            }

            isOnLineExceedingMaxLineLength() -> {
                true
            }

            else -> {
                false
            }
        }

    private fun ASTNode.hasNoParameters(): Boolean {
        require(elementType == VALUE_PARAMETER_LIST)
        return firstChildNode?.treeNext?.elementType == RPAR
    }

    private fun ASTNode.isPartOfFunctionLiteralInNonKtlintOfficialCodeStyle(): Boolean {
        require(elementType == VALUE_PARAMETER_LIST)
        return treeParent?.elementType == FUNCTION_LITERAL
    }

    private fun ASTNode.isPartOfFunctionLiteralStartingOnSameLineAsClosingParenthesisOfPrecedingReferenceExpression(): Boolean {
        require(elementType == VALUE_PARAMETER_LIST)
        return firstChildLeafOrSelf()
            .let { startOfFunctionLiteral ->
                treeParent
                    ?.takeIf { it.elementType == FUNCTION_LITERAL }
                    ?.prevCodeLeaf()
                    ?.takeIf { it.treeParent.elementType == ElementType.VALUE_ARGUMENT_LIST }
                    ?.takeIf { it.treeParent.treeParent.elementType == ElementType.CALL_EXPRESSION }
                    ?.leaves()
                    ?.takeWhile { it != startOfFunctionLiteral }
                    ?.none { it.isWhiteSpaceWithNewline() }
                    ?: false
            }
    }

    private fun ASTNode.containsAnnotatedParameter(): Boolean {
        require(elementType == VALUE_PARAMETER_LIST)
        return this
            .children()
            .filter { it.elementType == VALUE_PARAMETER }
            .any { it.isAnnotated() }
    }

    private fun ASTNode.isAnnotated() =
        findChildByType(ElementType.MODIFIER_LIST)
            ?.children()
            .orEmpty()
            .any { it.elementType == ElementType.ANNOTATION_ENTRY }

    private fun visitParameterList(
        node: ASTNode,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision,
    ) {
        if (isPrecededByComment(node)) {
            emit(node.startOffset, "Parameter list should not be preceded by a comment", false)
        } else if (node.needToWrapParameterList()) {
            node
                .children()
                .forEach { child -> wrapParameterInList(child, emit) }
        }
    }

    private fun isPrecededByComment(node: ASTNode) =
        node
            .prevLeaf { !it.isWhiteSpace() }
            ?.prevLeaf()
            ?.isPartOfComment()
            ?: false

    private fun intendedIndent(child: ASTNode): String =
        when {
            // IDEA quirk:
            // fun <
            //     T,
            //     R> test(
            //     param1: T
            //     param2: R
            // )
            // instead of
            // fun <
            //     T,
            //     R> test(
            //         param1: T
            //         param2: R
            //     )
            child.treeParent.isFunWithTypeParameterListInFront() -> -1

            else -> 0
        }.let {
            if (child.elementType == VALUE_PARAMETER) {
                it + 1
            } else {
                it
            }
        }.let { indentLevelFix ->
            val indentLevel =
                indentConfig
                    .indentLevelFrom(child.treeParent.indent(false))
                    .plus(indentLevelFix)
            "\n" + indentConfig.indent.repeat(indentLevel)
        }

    private fun wrapParameterInList(
        child: ASTNode,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision,
    ) {
        when (child.elementType) {
            LPAR -> {
                val prevLeaf = child.prevLeaf()
                if (!child.treeParent.isValueParameterListInFunctionType() &&
                    prevLeaf.isWhiteSpaceWithNewline()
                ) {
                    emit(child.startOffset, errorMessage(child), true)
                        .ifAutocorrectAllowed {
                            (prevLeaf as PsiWhiteSpace).delete()
                        }
                }
            }

            VALUE_PARAMETER,
            RPAR,
            -> {
                // aiming for
                // ... LPAR
                //  VALUE_PARAMETER...
                //  RPAR
                val intendedIndent = intendedIndent(child)
                val prevLeaf = 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)
                            .ifAutocorrectAllowed {
                                // 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)
                        .ifAutocorrectAllowed {
                            child.treeParent.addChild(PsiWhiteSpaceImpl(intendedIndent), child)
                        }
                }
                // Indentation of child nodes need to be fixed by the IndentationRule.
            }
        }
    }

    private fun ASTNode.isValueParameterListInFunctionType() =
        FUNCTION_TYPE ==
            takeIf { it.elementType == VALUE_PARAMETER_LIST }
                ?.treeParent
                ?.elementType

    private fun ASTNode.isOnLineExceedingMaxLineLength(): Boolean {
        val stopLeaf = nextLeaf { it.textContains('\n') }?.nextLeaf()
        val lineContent =
            leavesOnLine(excludeEolComment = true)
                .takeWhile { it.prevLeaf() != stopLeaf }
                .joinToString(separator = "") { it.text }
                .substringAfter('\n')
                .substringBefore('\n')
        return lineContent.length > maxLineLength
    }

    private fun errorMessage(node: ASTNode) =
        when (node.elementType) {
            LPAR -> """Unnecessary newline before "(""""
            VALUE_PARAMETER -> "Parameter should start on a newline"
            RPAR -> """Missing newline before ")""""
            else -> throw UnsupportedOperationException()
        }

    private fun ASTNode.isFunWithTypeParameterListInFront() =
        treeParent
            .takeIf { elementType == FUN }
            ?.findChildByType(TYPE_PARAMETER_LIST)
            ?.children()
            ?.any { it.isWhiteSpaceWithNewline() }
            ?: false
}

public val PARAMETER_LIST_WRAPPING_RULE_ID: RuleId = ParameterListWrappingRule().ruleId




© 2015 - 2024 Weber Informatics LLC | Privacy Policy