blob: 15835fa84c52e600b8d51c81c4072892813f690e [file] [log] [blame]
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.PsiComment
import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtBinaryExpression
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtParameterList
import org.jetbrains.kotlin.psi.KtParenthesizedExpression
import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression
import org.jetbrains.kotlin.psi.KtSecondaryConstructor
import org.jetbrains.kotlin.psi.KtSuperTypeList
import org.jetbrains.kotlin.psi.KtSuperTypeListEntry
import org.jetbrains.kotlin.psi.KtTypeProjection
import org.jetbrains.kotlin.psi.KtValueArgumentList
import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespaceAndComments
import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes
class IndentationRule : Rule("indent") {
private var indentSize = -1
private var continuationIndentSize = -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
continuationIndentSize = ec.continuationIndentSize
return
}
if (indentSize <= 0 || continuationIndentSize <= 0) {
return
}
if (node is PsiWhiteSpace && !node.isPartOf(PsiComment::class)) {
val lines = node.getText().split("\n")
if (lines.size > 1) {
var offset = node.startOffset + lines.first().length + 1
val previousIndentSize = node.previousIndentSize()
val expectedIndentSize = if (continuationIndentSize == indentSize || shouldUseContinuationIndent(node))
continuationIndentSize else indentSize
lines.tail().forEach { line ->
if (line.isNotEmpty() && (line.length - previousIndentSize) % expectedIndentSize != 0) {
if (!node.isPartOf(KtParameterList::class)) { // parameter list wrapping enforced by ParameterListWrappingRule
emit(offset,
"Unexpected indentation (${line.length - previousIndentSize}) " +
"(it should be $expectedIndentSize)",
false)
}
}
offset += line.length + 1
}
}
}
}
private fun shouldUseContinuationIndent(node: PsiWhiteSpace): Boolean {
val parentNode = node.parent
val prevNode = node.getPrevSiblingIgnoringWhitespaceAndComments()?.node?.elementType
val nextNode = node.nextSibling?.node?.elementType
return (
prevNode in KtTokens.ALL_ASSIGNMENTS ||
parentNode is KtSecondaryConstructor ||
nextNode == KtStubElementTypes.TYPE_REFERENCE ||
node.nextSibling is KtSuperTypeList ||
node.nextSibling is KtSuperTypeListEntry ||
node.nextSibling is KtTypeProjection ||
parentNode is KtValueArgumentList ||
parentNode is KtBinaryExpression ||
parentNode is KtDotQualifiedExpression ||
parentNode is KtSafeQualifiedExpression ||
parentNode is KtParenthesizedExpression
)
}
private fun ASTNode.previousIndentSize(): Int {
val parentNode = this.treeParent?.psi
var prevSibling = parentNode
while (prevSibling != null) {
val nextNode = prevSibling.nextSibling?.node?.elementType
if (prevSibling is PsiWhiteSpace &&
nextNode != KtStubElementTypes.TYPE_REFERENCE &&
nextNode != KtStubElementTypes.SUPER_TYPE_LIST &&
nextNode != KtNodeTypes.CONSTRUCTOR_DELEGATION_CALL) {
val prevLines = prevSibling.text.split('\n')
if (prevLines.size > 1) {
return prevLines.last().length
}
}
prevSibling = prevSibling.prevSibling ?: prevSibling.parent
}
return 0
}
}