Formatter: shouldn't format property chains

#KT-36387 Fixed
#KT-36466 Fixed
This commit is contained in:
Dmitry Gridin
2020-02-13 17:29:40 +07:00
parent 522eeae062
commit feaa53c4f2
16 changed files with 887 additions and 331 deletions
@@ -123,31 +123,15 @@ abstract class KotlinCommonBlock(
return nodeSubBlocks
}
private fun ASTNode.isQualifiedNode(strict: Boolean = false): Boolean {
if (strict) return elementType in QUALIFIED_EXPRESSIONS
var currentNode: ASTNode? = this
while (currentNode != null) {
if (currentNode.elementType in QUALIFIED_EXPRESSIONS) return true
if (currentNode.elementType != PARENTHESIZED && currentNode.psi?.safeAs<KtPostfixExpression>()
?.operationToken != EXCLEXCL
) return false
currentNode = currentNode.treeParent
}
return false
}
private fun splitSubBlocksOnDot(nodeSubBlocks: List<ASTBlock>): List<ASTBlock> {
if (node.treeParent?.isQualifiedNode() == true) return nodeSubBlocks
if (!canWrapCallChain(node)) return nodeSubBlocks
if (node.treeParent?.isQualifier == true || node.isCallChainWithoutWrap) return nodeSubBlocks
val operationBlockIndex = nodeSubBlocks.indexOfBlockWithType(QUALIFIED_OPERATION)
if (operationBlockIndex == -1) return nodeSubBlocks
val block = nodeSubBlocks.first()
val wrap = createWrapForQualifiedExpression(node)
val indent = createIndentForQualifiedExpression(block)
val wrap = createWrapForQualifierExpression(node)
val indent = createIndentForQualifierExpression(node)
val newBlock = block.processBlock(indent, wrap)
return nodeSubBlocks.replaceBlock(newBlock, 0).splitAtIndex(operationBlockIndex, indent, wrap)
}
@@ -158,21 +142,16 @@ abstract class KotlinCommonBlock(
@Suppress("UNCHECKED_CAST")
val subBlocks = subBlocks as List<ASTBlock>
val elementType = currentNode.elementType
val index = when (elementType) {
PARENTHESIZED -> subBlocks.indexOfFirst { block ->
val type = block.requireNode().elementType
type != LPAR && type !in WHITE_SPACE_OR_COMMENT_BIT_SET
}
POSTFIX_EXPRESSION, in QUALIFIED_EXPRESSIONS -> 0
else -> return this
}
if (elementType != POSTFIX_EXPRESSION && elementType !in QUALIFIED_EXPRESSIONS) return this
val index = 0
val resultWrap = if (currentNode.wrapForFirstCallInChainIsAllowed)
wrap.takeIf { elementType != PARENTHESIZED } ?: createWrapForQualifiedExpression(currentNode)
wrap ?: createWrapForQualifierExpression(currentNode)
else
null
val newBlock = subBlocks.elementAt(index).processBlock(indent, resultWrap)
return subBlocks.replaceBlock(newBlock, index).let {
val operationIndex = subBlocks.indexOfBlockWithType(QUALIFIED_OPERATION)
if (operationIndex != -1)
@@ -184,23 +163,25 @@ abstract class KotlinCommonBlock(
private fun List<ASTBlock>.replaceBlock(block: ASTBlock, index: Int = 0): List<ASTBlock> = toMutableList().apply { this[index] = block }
private val ASTNode.wrapForFirstCallInChainIsAllowed
get() = settings.kotlinCommonSettings.WRAP_FIRST_METHOD_IN_CALL_CHAIN ||
settings.kotlinCommonSettings.METHOD_CALL_CHAIN_WRAP != CommonCodeStyleSettings.WRAP_AS_NEEDED ||
firstChildNode?.isQualifiedNode(true) == true
private val ASTNode.wrapForFirstCallInChainIsAllowed: Boolean
get() {
if (unwrapQualifier()?.isCall != true) return false
return settings.kotlinCommonSettings.WRAP_FIRST_METHOD_IN_CALL_CHAIN || receiverIsCall()
}
private fun createWrapForQualifiedExpression(node: ASTNode): Wrap? = if (node.wrapForFirstCallInChainIsAllowed)
Wrap.createWrap(
settings.kotlinCommonSettings.METHOD_CALL_CHAIN_WRAP,
settings.kotlinCommonSettings.WRAP_FIRST_METHOD_IN_CALL_CHAIN,
)
else
null
private fun createWrapForQualifierExpression(node: ASTNode): Wrap? =
if (node.wrapForFirstCallInChainIsAllowed && node.receiverIsCall())
Wrap.createWrap(
settings.kotlinCommonSettings.METHOD_CALL_CHAIN_WRAP,
true /* wrapFirstElement */,
)
else
null
private fun createIndentForQualifiedExpression(block: ASTBlock): Indent {
private fun createIndentForQualifierExpression(node: ASTNode): Indent {
// enforce indent to children when there's a line break before the dot in any call in the chain (meaning that
// the call chain following that call is indented)
val enforceIndentToChildren = anyCallInCallChainIsWrapped(block)
val enforceIndentToChildren = anyCallInCallChainIsWrapped(node)
val indentType = if (settings.kotlinCustomSettings.CONTINUATION_INDENT_FOR_CHAINED_CALLS) {
if (enforceIndentToChildren) Indent.Type.CONTINUATION else Indent.Type.CONTINUATION_WITHOUT_FIRST
@@ -249,29 +230,6 @@ abstract class KotlinCommonBlock(
return subList(0, index) + operationSyntheticBlock
}
private fun anyCallInCallChainIsWrapped(astBlock: ASTBlock): Boolean {
var result: ASTBlock? = astBlock
while (true) {
if (result == null || !isCallBlock(result)) return false
val dot = result.node?.findChildByType(QUALIFIED_OPERATION)
if (dot != null && hasLineBreakBefore(dot)) {
return true
}
result = result.subBlocks.firstOrNull() as? ASTBlock?
}
}
private fun isCallBlock(astBlock: ASTBlock): Boolean {
val node = astBlock.requireNode()
val lastChildElementType = node.lastChildNode?.elementType
return node.isQualifiedNode() && (lastChildElementType == CALL_EXPRESSION || lastChildElementType == REFERENCE_EXPRESSION)
}
private fun canWrapCallChain(node: ASTNode): Boolean {
val callChainParent = node.parents().firstOrNull { !it.isQualifiedNode() } ?: return true
return callChainParent.elementType !in QUALIFIED_EXPRESSIONS_WITHOUT_WRAP
}
private fun splitSubBlocksOnElvis(nodeSubBlocks: List<ASTBlock>): List<ASTBlock> {
val elvisIndex = nodeSubBlocks.indexOfBlockWithType(ELVIS_SET)
if (elvisIndex >= 0) {
@@ -827,6 +785,54 @@ abstract class KotlinCommonBlock(
}
}
private fun ASTNode.qualifierReceiver(): ASTNode? = unwrapQualifier()?.psi
?.safeAs<KtQualifiedExpression>()
?.receiverExpression
?.node
?.unwrapQualifier()
private tailrec fun ASTNode.unwrapQualifier(): ASTNode? {
if (elementType in QUALIFIED_EXPRESSIONS) return this
val psi = psi as? KtPostfixExpression ?: return null
if (psi.operationToken != EXCLEXCL) return null
return psi.baseExpression?.node?.unwrapQualifier()
}
private fun ASTNode.receiverIsCall(): Boolean = qualifierReceiver()?.isCall == true
private val ASTNode.isCallChainWithoutWrap: Boolean
get() {
val callChainParent = parents().firstOrNull { !it.isQualifier } ?: return true
return callChainParent.elementType in QUALIFIED_EXPRESSIONS_WITHOUT_WRAP
}
private val ASTNode.isQualifier: Boolean
get() {
var currentNode: ASTNode? = this
while (currentNode != null) {
if (currentNode.elementType in QUALIFIED_EXPRESSIONS) return true
if (currentNode.psi?.safeAs<KtPostfixExpression>()?.operationToken != EXCLEXCL) return false
currentNode = currentNode.treeParent
}
return false
}
private val ASTNode.isCall: Boolean
get() = unwrapQualifier()?.lastChildNode?.elementType == CALL_EXPRESSION
private fun anyCallInCallChainIsWrapped(node: ASTNode): Boolean {
val sequentialCalls = generateSequence(node) { it.firstChildNode }.takeWhile { it.isCall }
return sequentialCalls.any { call ->
val qualifier = call.findChildByType(QUALIFIED_OPERATION)
qualifier != null && hasLineBreakBefore(qualifier)
}
}
private fun ASTNode.isFirstParameter(): Boolean = treePrev?.elementType == LPAR
private fun wrapAfterAnnotation(wrapType: Int): WrappingStrategy {
@@ -983,8 +989,7 @@ private val INDENT_RULES = arrayOf(
strategy("Chained calls")
.within(QUALIFIED_EXPRESSIONS)
.notForType(DOT, SAFE_ACCESS)
.forElement { it.treeParent.firstChildNode != it }
.forType(EOL_COMMENT, BLOCK_COMMENT, DOC_COMMENT, SHEBANG_COMMENT)
.continuationIf(KotlinCodeStyleSettings::CONTINUATION_INDENT_FOR_CHAINED_CALLS),
strategy("Colon of delegation list")