Formatter: support Chop down if long for Chained method calls

#KT-23929 Fixed
#KT-33553 Fixed
This commit is contained in:
Dmitry Gridin
2019-11-11 14:33:04 +07:00
parent 351fef5628
commit b6b755506c
13 changed files with 726 additions and 54 deletions
@@ -25,10 +25,12 @@ import org.jetbrains.kotlin.kdoc.parser.KDocElementTypes
import org.jetbrains.kotlin.lexer.KtTokens.*
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.*
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
private val QUALIFIED_OPERATION = TokenSet.create(DOT, SAFE_ACCESS)
private val QUALIFIED_EXPRESSIONS = TokenSet.create(DOT_QUALIFIED_EXPRESSION, SAFE_ACCESS_EXPRESSION)
private val ELVIS_SET = TokenSet.create(ELVIS)
private val QUALIFIED_EXPRESSIONS_WITHOUT_WRAP = TokenSet.create(IMPORT_DIRECTIVE, PACKAGE_DIRECTIVE)
private const val KDOC_COMMENT_INDENT = 1
@@ -120,55 +122,122 @@ abstract class KotlinCommonBlock(
return nodeSubBlocks
}
private fun splitSubBlocksOnDot(nodeSubBlocks: List<ASTBlock>): List<ASTBlock> {
val operationBlockIndex = nodeSubBlocks.indexOfBlockWithType(QUALIFIED_OPERATION)
if (operationBlockIndex != -1) {
// Create fake ".something" or "?.something" block here, so child indentation will be
// relative to it when it starts from new line (see Indent javadoc).
val isNonFirstChainedCall = operationBlockIndex > 0 && isCallBlock(nodeSubBlocks[operationBlockIndex - 1])
// 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(nodeSubBlocks[operationBlockIndex - 1])
val indentType = if (settings.kotlinCustomSettings.CONTINUATION_INDENT_FOR_CHAINED_CALLS) {
if (enforceIndentToChildren) Indent.Type.CONTINUATION else Indent.Type.CONTINUATION_WITHOUT_FIRST
} else {
Indent.Type.NORMAL
}
val indent = Indent.getIndent(
indentType, false,
enforceIndentToChildren
)
val wrap = if ((settings.kotlinCommonSettings.WRAP_FIRST_METHOD_IN_CALL_CHAIN || isNonFirstChainedCall) &&
canWrapCallChain(node)
)
Wrap.createWrap(settings.kotlinCommonSettings.METHOD_CALL_CHAIN_WRAP, true)
else
null
return nodeSubBlocks.splitAtIndex(operationBlockIndex, indent, wrap)
private fun ASTNode.isQualifiedNode(): Boolean {
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 nodeSubBlocks
return false
}
private fun List<ASTBlock>.splitAtIndex(index: Int, indent: Indent?, wrap: Wrap?): List<ASTBlock> {
val operationBlock = this[index]
val operationSyntheticBlock = SyntheticKotlinBlock(
operationBlock.requireNode(),
subList(index, size),
null, indent, wrap, spacingBuilder
) {
val parent = it.treeParent ?: node
val skipOperationNodeParent = if (parent.elementType === OPERATION_REFERENCE) {
parent.treeParent ?: parent
} else {
parent
private fun splitSubBlocksOnDot(nodeSubBlocks: List<ASTBlock>): List<ASTBlock> {
if (node.treeParent?.isQualifiedNode() == true) return nodeSubBlocks
if (!canWrapCallChain(node)) return nodeSubBlocks
val operationBlockIndex = nodeSubBlocks.indexOfBlockWithType(QUALIFIED_OPERATION)
if (operationBlockIndex == -1) return nodeSubBlocks
val block = nodeSubBlocks.first()
val wrap = createWrapForQualifiedExpression(block.requireNode())
val indent = createIndentForQualifiedExpression(block)
val newBlock = block.processBlock(indent, wrap)
return nodeSubBlocks.replaceBlock(newBlock, 0).splitAtIndex(operationBlockIndex, indent, wrap)
}
private fun ASTBlock.processBlock(indent: Indent, wrap: Wrap?): ASTBlock {
val currentNode = requireNode()
@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
}
createSyntheticSpacingNodeBlock(skipOperationNodeParent)
POSTFIX_EXPRESSION, in QUALIFIED_EXPRESSIONS -> 0
else -> return this
}
val resultWrap = wrap.takeIf { elementType != PARENTHESIZED } ?: createWrapForQualifiedExpression(currentNode)
val newBlock = subBlocks.elementAt(index).processBlock(indent, resultWrap)
return subBlocks.replaceBlock(newBlock, index).let {
val operationIndex = subBlocks.indexOfBlockWithType(QUALIFIED_OPERATION)
if (operationIndex != -1)
it.splitAtIndex(operationIndex, indent, resultWrap)
else
it
}.wrapToBlock(currentNode, this)
}
private fun List<ASTBlock>.replaceBlock(
block: ASTBlock,
index: Int = 0
): List<ASTBlock> = subList(0, index) + block + (takeIf { index + 1 < it.size }?.subList(index + 1, size) ?: emptyList())
private fun createWrapForQualifiedExpression(node: ASTNode): Wrap? {
val isNonFirstChainedCall = (node.treeParent)?.isQualifiedNode() == true
return if (isNonFirstChainedCall)
Wrap.createWrap(
settings.kotlinCommonSettings.METHOD_CALL_CHAIN_WRAP,
settings.kotlinCommonSettings.WRAP_FIRST_METHOD_IN_CALL_CHAIN
)
else
null
}
private fun createIndentForQualifiedExpression(block: ASTBlock): 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 indentType = if (settings.kotlinCustomSettings.CONTINUATION_INDENT_FOR_CHAINED_CALLS) {
if (enforceIndentToChildren) Indent.Type.CONTINUATION else Indent.Type.CONTINUATION_WITHOUT_FIRST
} else {
Indent.Type.NORMAL
}
return Indent.getIndent(
indentType, false,
enforceIndentToChildren
)
}
private fun List<ASTBlock>.wrapToBlock(
anchor: ASTNode?,
parentBlock: ASTBlock?
): ASTBlock = splitAtIndex(0, null, null, anchor, parentBlock).single()
private fun List<ASTBlock>.splitAtIndex(
index: Int,
indent: Indent?,
wrap: Wrap?,
anchor: ASTNode? = null,
parentBlock: ASTBlock? = null
): List<ASTBlock> {
val operationBlock = this[index]
val createParentSyntheticSpacingBlock: (ASTNode) -> ASTBlock = if (parentBlock != null) {
{ parentBlock }
} else {
{
val parent = it.treeParent ?: node
val skipOperationNodeParent = if (parent.elementType === OPERATION_REFERENCE) {
parent.treeParent ?: parent
} else {
parent
}
createSyntheticSpacingNodeBlock(skipOperationNodeParent)
}
}
val operationSyntheticBlock = SyntheticKotlinBlock(
anchor ?: operationBlock.requireNode(),
subList(index, size),
null, indent, wrap, spacingBuilder, createParentSyntheticSpacingBlock
)
return subList(0, index) + operationSyntheticBlock
}
@@ -186,16 +255,13 @@ abstract class KotlinCommonBlock(
private fun isCallBlock(astBlock: ASTBlock): Boolean {
val node = astBlock.requireNode()
return node.elementType in QUALIFIED_EXPRESSIONS && node.lastChildNode?.elementType == CALL_EXPRESSION
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.elementType !in QUALIFIED_EXPRESSIONS } ?: return true
return callChainParent.elementType in CODE_BLOCKS ||
callChainParent.elementType == PROPERTY ||
(callChainParent.elementType == BINARY_EXPRESSION &&
(callChainParent.psi as KtBinaryExpression).operationToken in ALL_ASSIGNMENTS) ||
callChainParent.elementType == RETURN
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> {
@@ -4,7 +4,9 @@ interface Test {
fun test(t: Test) {
t.foo()
t().foo()
t()!!.foo().foo()
((t()!!).foo().foo().foo()).foo()
t.foo()
t.foo()
+3 -1
View File
@@ -4,7 +4,9 @@ interface Test {
fun test(t: Test) {
t . foo()
t() . foo()
t() !! . foo() .foo()
( ( t() !!) . foo() .foo() .foo()) .foo()
t.
foo()
@@ -3,7 +3,10 @@ val x = foo
.baz()
.quux()
val y = xyzzy(foo.bar().baz().quux())
val y = xyzzy(foo
.bar()
.baz()
.quux())
fun foo() {
foo
@@ -27,5 +30,25 @@ fun foo() {
.quux()
}
fun top() = ""
.plus("")
.plus("")
class C {
fun member() = ""
.plus("")
.plus("")
}
fun foo() {
fun local() = ""
.plus("")
.plus("")
val anonymous = fun() = ""
.plus("")
.plus("")
}
// SET_INT: METHOD_CALL_CHAIN_WRAP = 2
// SET_FALSE: WRAP_FIRST_METHOD_IN_CALL_CHAIN
@@ -2,7 +2,9 @@ val x = foo.bar()
.baz()
.quux()
val y = xyzzy(foo.bar().baz().quux())
val y = xyzzy(foo.bar()
.baz()
.quux())
fun foo() {
foo.bar()
@@ -22,5 +24,21 @@ fun foo() {
.quux()
}
fun top() = "".plus("")
.plus("")
class C {
fun member() = "".plus("")
.plus("")
}
fun foo() {
fun local() = "".plus("")
.plus("")
val anonymous = fun() = "".plus("")
.plus("")
}
// SET_INT: METHOD_CALL_CHAIN_WRAP = 2
// SET_FALSE: WRAP_FIRST_METHOD_IN_CALL_CHAIN
@@ -12,5 +12,14 @@ fun foo() {
return foo.bar().baz().quux()
}
fun top() = "".plus("").plus("")
class C {
fun member() = "".plus("").plus("")
}
fun foo() {
fun local() = "".plus("").plus("")
val anonymous = fun() = "".plus("").plus("")
}
// SET_INT: METHOD_CALL_CHAIN_WRAP = 2
// SET_FALSE: WRAP_FIRST_METHOD_IN_CALL_CHAIN
@@ -0,0 +1,244 @@
val x = foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
val y = xyzzy(foo.bar().baz().quux())
fun foo() {
foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
z = (((foo()))!!).bar()
z += (foo.bar().baz().quux().foo.bar().baz().quux().foo.bar()!!)
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
return ((foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux())
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar())
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
}
// SET_INT: METHOD_CALL_CHAIN_WRAP = 5
// SET_FALSE: WRAP_FIRST_METHOD_IN_CALL_CHAIN
// SET_FALSE: CONTINUATION_INDENT_FOR_CHAINED_CALLS
@@ -0,0 +1,239 @@
val x = foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
val y = xyzzy(foo.bar().baz().quux())
fun foo() {
foo.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
z = (((foo()))!!).bar()
z += (foo.bar().baz().quux().foo.bar().baz().quux().foo.bar()!!).baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
return ((foo.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()).foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()).baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
.foo
.bar()
.baz()
.quux()
}
// SET_INT: METHOD_CALL_CHAIN_WRAP = 5
// SET_FALSE: WRAP_FIRST_METHOD_IN_CALL_CHAIN
// SET_FALSE: CONTINUATION_INDENT_FOR_CHAINED_CALLS
@@ -0,0 +1,19 @@
val x = foo
.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux()
val y = xyzzy(foo.bar().baz().quux())
fun foo() {
foo.bar().baz().quux()
.foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux()
z = (( ( foo()) )!! ).bar()
z += (foo.bar().baz().quux().foo.bar().baz().quux().foo.bar() !!) .baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux()
return ((foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux()).foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar()).baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux().foo.bar().baz().quux()
}
// SET_INT: METHOD_CALL_CHAIN_WRAP = 5
// SET_FALSE: WRAP_FIRST_METHOD_IN_CALL_CHAIN
// SET_FALSE: CONTINUATION_INDENT_FOR_CHAINED_CALLS
@@ -0,0 +1,12 @@
fun f(list: List<Int>) {
val result = list
.filter { it % 2 == 0 }
.max() ?: -1
list
.filter { it % 2 == 0 }
.max()
?: -1
}
// SET_INT: METHOD_CALL_CHAIN_WRAP = 2
// SET_FALSE: WRAP_FIRST_METHOD_IN_CALL_CHAIN
@@ -0,0 +1,10 @@
fun f(list: List<Int>) {
val result = list.filter { it % 2 == 0 }
.max() ?: -1
list.filter { it % 2 == 0 }
.max()
?: -1
}
// SET_INT: METHOD_CALL_CHAIN_WRAP = 2
// SET_FALSE: WRAP_FIRST_METHOD_IN_CALL_CHAIN
@@ -0,0 +1,8 @@
fun f(list: List<Int>) {
val result = list.filter { it % 2 == 0 }.max() ?: -1
list.filter { it % 2 == 0 }.max()
?: -1
}
// SET_INT: METHOD_CALL_CHAIN_WRAP = 2
// SET_FALSE: WRAP_FIRST_METHOD_IN_CALL_CHAIN
@@ -852,6 +852,16 @@ public class FormatterTestGenerated extends AbstractFormatterTest {
runTest("idea/testData/formatter/callChain/CallChainWrapping.after.kt");
}
@TestMetadata("CallChainWrappingChopDown.after.kt")
public void testCallChainWrappingChopDown() throws Exception {
runTest("idea/testData/formatter/callChain/CallChainWrappingChopDown.after.kt");
}
@TestMetadata("CallChainWrappingInElvis.after.kt")
public void testCallChainWrappingInElvis() throws Exception {
runTest("idea/testData/formatter/callChain/CallChainWrappingInElvis.after.kt");
}
@TestMetadata("ConsecutiveCalls.after.kt")
public void testConsecutiveCalls() throws Exception {
runTest("idea/testData/formatter/callChain/ConsecutiveCalls.after.kt");
@@ -1397,6 +1407,16 @@ public class FormatterTestGenerated extends AbstractFormatterTest {
runTest("idea/testData/formatter/callChain/CallChainWrapping.after.inv.kt");
}
@TestMetadata("CallChainWrappingChopDown.after.inv.kt")
public void testCallChainWrappingChopDown() throws Exception {
runTest("idea/testData/formatter/callChain/CallChainWrappingChopDown.after.inv.kt");
}
@TestMetadata("CallChainWrappingInElvis.after.inv.kt")
public void testCallChainWrappingInElvis() throws Exception {
runTest("idea/testData/formatter/callChain/CallChainWrappingInElvis.after.inv.kt");
}
@TestMetadata("ConsecutiveSafeCallsIndent.after.inv.kt")
public void testConsecutiveSafeCallsIndent() throws Exception {
runTest("idea/testData/formatter/callChain/ConsecutiveSafeCallsIndent.after.inv.kt");