aboutsummaryrefslogtreecommitdiff
path: root/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ParameterListWrappingRule.kt
blob: 81d060380ae3cd5993df81778b8883e95ae9a9ec (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
package com.github.shyiko.ktlint.ruleset.standard

import com.github.shyiko.ktlint.core.Rule
import org.jetbrains.kotlin.KtNodeTypes
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.lang.FileASTNode
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafElement
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.com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.psiUtil.children
import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes

class ParameterListWrappingRule : Rule("parameter-list-wrapping") {

    private var indentSize = -1
    private var maxLineLength = -1

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.elementType == KtStubElementTypes.FILE) {
            val ec = EditorConfig.from(node as FileASTNode)
            indentSize = ec.indentSize
            maxLineLength = ec.maxLineLength
            return
        }
        if (indentSize <= 0) {
            return
        }
        if (node.elementType == KtStubElementTypes.VALUE_PARAMETER_LIST &&
            // skip lambda parameters
            node.treeParent?.elementType != KtNodeTypes.FUNCTION_LITERAL) {
            // each parameter should be on a separate line if
            // - at least one of the parameters is
            // - maxLineLength exceeded (and separating parameters with \n would actually help)
            // in addition, "(" and ")" must be on separates line if any of the parameters are (otherwise on the same)
            val putParametersOnSeparateLines = node.textContains('\n') ||
                // max_line_length exceeded
                maxLineLength > -1 && (node.psi.column - 1 + node.textLength) > maxLineLength
            if (putParametersOnSeparateLines) {
                // aiming for
                // ... LPAR
                // <line indent + indentSize> VALUE_PARAMETER...
                // <line indent> RPAR
                val indent = "\n" + node.psi.lineIndent()
                val paramIndent = indent + " ".repeat(indentSize) // single indent as recommended by Jetbrains/Google
                nextChild@ for (child in node.children()) {
                    when (child.elementType) {
                        KtTokens.LPAR -> {
                            val prevLeaf = child.psi.prevLeaf()
                            if (prevLeaf is PsiWhiteSpace && prevLeaf.textContains('\n')) {
                                emit(child.startOffset, errorMessage(child), true)
                                if (autoCorrect) {
                                    prevLeaf.delete()
                                }
                            }
                        }
                        KtStubElementTypes.VALUE_PARAMETER,
                        KtTokens.RPAR -> {
                            var paramInnerIndentAdjustment = 0
                            val prevLeaf = child.psi.prevLeaf()
                            val intendedIndent = if (child.elementType == KtStubElementTypes.VALUE_PARAMETER)
                                paramIndent else indent
                            if (prevLeaf is PsiWhiteSpace) {
                                val spacing = prevLeaf.text
                                val cut = spacing.lastIndexOf("\n")
                                if (cut > -1) {
                                    val childIndent = spacing.substring(cut)
                                    if (childIndent == intendedIndent) {
                                        continue@nextChild
                                    }
                                    emit(child.startOffset, "Unexpected indentation" +
                                        " (expected ${intendedIndent.length - 1}, actual ${childIndent.length - 1})", true)
                                } else {
                                    emit(child.startOffset, errorMessage(child), true)
                                }
                                if (autoCorrect) {
                                    val adjustedIndent = (if (cut > -1) spacing.substring(0, cut) else "") + intendedIndent
                                    paramInnerIndentAdjustment = adjustedIndent.length - prevLeaf.textLength
                                    (prevLeaf as LeafPsiElement).rawReplaceWithText(adjustedIndent)
                                }
                            } else {
                                emit(child.startOffset, errorMessage(child), true)
                                if (autoCorrect) {
                                    paramInnerIndentAdjustment = intendedIndent.length - child.psi.column
                                    node.addChild(PsiWhiteSpaceImpl(intendedIndent), child)
                                }
                            }
                            if (paramInnerIndentAdjustment != 0 &&
                                child.elementType == KtStubElementTypes.VALUE_PARAMETER) {
                                child.visit { n ->
                                    if (n.elementType == KtTokens.WHITE_SPACE && n.textContains('\n')) {
                                        val split = n.text.split("\n")
                                        (n.psi as LeafElement).rawReplaceWithText(split.joinToString("\n") {
                                            if (paramInnerIndentAdjustment > 0) {
                                                it + " ".repeat(paramInnerIndentAdjustment)
                                            } else {
                                                it.substring(0, Math.max(it.length + paramInnerIndentAdjustment, 0))
                                            }
                                        })
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    private val PsiElement.column: Int
        get() {
            var leaf = PsiTreeUtil.prevLeaf(this)
            var offsetToTheLeft = 0
            while (leaf != null) {
                if (leaf.node.elementType == KtTokens.WHITE_SPACE && leaf.textContains('\n')) {
                    offsetToTheLeft += leaf.textLength - 1 - leaf.text.lastIndexOf('\n')
                    break
                }
                offsetToTheLeft += leaf.textLength
                leaf = PsiTreeUtil.prevLeaf(leaf)
            }
            return offsetToTheLeft + 1
        }

    private fun errorMessage(node: ASTNode) =
        when (node.elementType) {
            KtTokens.LPAR -> """Unnecessary newline before "(""""
            KtStubElementTypes.VALUE_PARAMETER ->
                "Parameter should be on a separate line (unless all parameters can fit a single line)"
            KtTokens.RPAR -> """Missing newline before ")""""
            else -> throw UnsupportedOperationException()
        }

    private fun PsiElement.lineIndent(): String {
        var leaf = PsiTreeUtil.prevLeaf(this)
        while (leaf != null) {
            if (leaf.node.elementType == KtTokens.WHITE_SPACE && leaf.textContains('\n')) {
                return leaf.text.substring(leaf.text.lastIndexOf('\n') + 1)
            }
            leaf = PsiTreeUtil.prevLeaf(leaf)
        }
        return ""
    }
}