mirror of
https://github.com/apple/pkl.git
synced 2026-04-24 17:28:37 +02:00
Adjust formatting of qualified access chains (#1252)
This adjusts formatting of qualified access chains so that leading dot calls are kept in the same line if possible.
This commit is contained in:
@@ -131,6 +131,7 @@ internal class Builder(sourceText: String) {
|
||||
NodeType.AMENDS_EXPR -> formatNewExpr(node)
|
||||
NodeType.NEW_HEADER -> formatNewHeader(node)
|
||||
NodeType.UNQUALIFIED_ACCESS_EXPR -> formatUnqualifiedAccessExpression(node)
|
||||
NodeType.QUALIFIED_ACCESS_EXPR -> formatQualifiedAccessExpression(node)
|
||||
NodeType.BINARY_OP_EXPR -> formatBinaryOpExpr(node)
|
||||
NodeType.FUNCTION_LITERAL_EXPR -> formatFunctionLiteralExpr(node)
|
||||
NodeType.FUNCTION_LITERAL_BODY -> formatFunctionLiteralBody(node)
|
||||
@@ -193,7 +194,7 @@ internal class Builder(sourceText: String) {
|
||||
return if (prefixes.isEmpty()) {
|
||||
res
|
||||
} else {
|
||||
val sep = getSeparator(prefixes.last(), nodes.first())
|
||||
val sep = getSeparator(prefixes.last(), nodes.first(), spaceOrLine())
|
||||
Nodes(formatGeneric(prefixes, spaceOrLine()) + listOf(sep, res))
|
||||
}
|
||||
}
|
||||
@@ -242,6 +243,123 @@ internal class Builder(sourceText: String) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Special cases when formatting qualified access:
|
||||
*
|
||||
* Case 1: Dot calls followed by closing method call: wrap after the opening paren.
|
||||
*
|
||||
* ```
|
||||
* foo.bar.baz(new {
|
||||
* qux = 1
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* Case 2: Dot calls, then method calls: group the leading access together.
|
||||
*
|
||||
* ```
|
||||
* foo.bar
|
||||
* .baz(new {
|
||||
* qux = 1
|
||||
* })
|
||||
* .baz()
|
||||
* ```
|
||||
*
|
||||
* Case 3: If there are multiple lambdas present, always force a newline.
|
||||
*
|
||||
* ```
|
||||
* foo
|
||||
* .map((it) -> it + 1)
|
||||
* .filter((it) -> it.isEven)
|
||||
* ```
|
||||
*/
|
||||
private fun formatQualifiedAccessExpression(node: Node): FormatNode {
|
||||
var lambdaCount = 0
|
||||
var methodCallCount = 0
|
||||
var indexBeforeFirstMethodCall = 0
|
||||
val flat = mutableListOf<Node>()
|
||||
|
||||
fun gatherFacts(current: Node) {
|
||||
for (child in current.children) {
|
||||
if (child.type == NodeType.QUALIFIED_ACCESS_EXPR) {
|
||||
gatherFacts(child)
|
||||
} else {
|
||||
flat.add(child)
|
||||
when {
|
||||
isMethodCall(child) -> {
|
||||
methodCallCount++
|
||||
if (hasFunctionLiteral(child, 2)) {
|
||||
lambdaCount++
|
||||
}
|
||||
}
|
||||
methodCallCount == 0 -> {
|
||||
indexBeforeFirstMethodCall = flat.lastIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gatherFacts(node)
|
||||
|
||||
val separator: (Node, Node) -> FormatNode? = { prev, next ->
|
||||
when {
|
||||
prev.type == NodeType.OPERATOR -> null
|
||||
next.type == NodeType.OPERATOR -> if (lambdaCount > 1) forceLine() else line()
|
||||
else -> spaceOrLine()
|
||||
}
|
||||
}
|
||||
|
||||
val nodes =
|
||||
when {
|
||||
methodCallCount == 1 && isMethodCall(flat.lastProperNode()!!) -> {
|
||||
// lift argument list into its own node
|
||||
val (callChain, argsList) = splitFunctionCallNode(flat)
|
||||
val leadingNodes = indentAfterFirstNewline(formatGeneric(callChain, separator), true)
|
||||
val trailingNodes = formatGeneric(argsList, separator)
|
||||
val sep = getBaseSeparator(callChain.last(), argsList.first())
|
||||
if (sep != null) {
|
||||
leadingNodes + sep + trailingNodes
|
||||
} else {
|
||||
leadingNodes + trailingNodes
|
||||
}
|
||||
}
|
||||
methodCallCount > 0 && indexBeforeFirstMethodCall > 0 -> {
|
||||
val leading = flat.subList(0, indexBeforeFirstMethodCall)
|
||||
val trailing = flat.subList(indexBeforeFirstMethodCall, flat.size)
|
||||
val leadingNodes = indentAfterFirstNewline(formatGeneric(leading, separator), true)
|
||||
val trailingNodes = formatGeneric(trailing, separator)
|
||||
leadingNodes + line() + trailingNodes
|
||||
}
|
||||
else -> formatGeneric(flat, separator)
|
||||
}
|
||||
|
||||
val shouldGroup = node.children.size == flat.size
|
||||
return Group(newId(), indentAfterFirstNewline(nodes, shouldGroup))
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a function call node to extract its identifier into the leading group. For example,
|
||||
* `foo.bar(5)` becomes: leading gets `foo.bar`, rest gets `(5)`.
|
||||
*/
|
||||
private fun splitFunctionCallNode(nodes: List<Node>): Pair<List<Node>, List<Node>> {
|
||||
assert(nodes.isNotEmpty())
|
||||
val lastNode = nodes.last()
|
||||
val argListIdx = lastNode.children.indexOfFirst { it.type == NodeType.ARGUMENT_LIST }
|
||||
val leading = nodes.subList(0, nodes.lastIndex) + lastNode.children.subList(0, argListIdx)
|
||||
val trailing = lastNode.children.subList(argListIdx, lastNode.children.size)
|
||||
return leading to trailing
|
||||
}
|
||||
|
||||
private fun isMethodCall(node: Node): Boolean {
|
||||
if (node.type != NodeType.UNQUALIFIED_ACCESS_EXPR) return false
|
||||
for (child in node.children) {
|
||||
if (child.type == NodeType.ARGUMENT_LIST) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun formatAmendsExtendsClause(node: Node): FormatNode {
|
||||
val prefix = formatGeneric(node.children.dropLast(1), spaceOrLine())
|
||||
// string constant
|
||||
@@ -688,7 +806,7 @@ internal class Builder(sourceText: String) {
|
||||
val prevNoNewlines = noNewlines
|
||||
noNewlines = true
|
||||
val elems = cursor.takeUntilBefore { it.isTerminal(")") }
|
||||
getSeparator(prev!!, elems.first(), { _, _ -> null })?.let { add(it) }
|
||||
getBaseSeparator(prev!!, elems.first())?.let { add(it) }
|
||||
val formatted =
|
||||
try {
|
||||
formatGeneric(elems, null)
|
||||
@@ -697,7 +815,7 @@ internal class Builder(sourceText: String) {
|
||||
formatGeneric(elems, null)
|
||||
}
|
||||
addAll(formatted)
|
||||
getSeparator(elems.last(), cursor.peek(), { _, _ -> null })?.let { add(it) }
|
||||
getBaseSeparator(elems.last(), cursor.peek())?.let { add(it) }
|
||||
noNewlines = prevNoNewlines
|
||||
isInStringInterpolation = false
|
||||
continue
|
||||
@@ -848,26 +966,21 @@ internal class Builder(sourceText: String) {
|
||||
|
||||
private fun formatBinaryOpExpr(node: Node): FormatNode {
|
||||
val flat = flattenBinaryOperatorExprs(node)
|
||||
val callChainSize = flat.count { it.isOperator(".", "?.") }
|
||||
val hasMultipleLambdas = callChainSize > 1 && flat.hasMoreThan(1) { hasFunctionLiteral(it, 2) }
|
||||
val nodes =
|
||||
formatGeneric(flat) { prev, next ->
|
||||
if (prev.type == NodeType.OPERATOR) {
|
||||
when (prev.text()) {
|
||||
".",
|
||||
"?." -> null
|
||||
"-" -> spaceOrLine()
|
||||
else -> Space
|
||||
}
|
||||
} else if (next.type == NodeType.OPERATOR) {
|
||||
when (next.text()) {
|
||||
".",
|
||||
"?." -> if (hasMultipleLambdas) forceLine() else line()
|
||||
"-" -> Space
|
||||
else -> spaceOrLine()
|
||||
}
|
||||
} else spaceOrLine()
|
||||
}
|
||||
|
||||
val shouldGroup = node.children.size == flat.size
|
||||
return Group(newId(), indentAfterFirstNewline(nodes, shouldGroup))
|
||||
}
|
||||
@@ -1095,7 +1208,7 @@ internal class Builder(sourceText: String) {
|
||||
val nodes = children.subList(index, children.size)
|
||||
val res = mutableListOf<FormatNode>()
|
||||
res += formatGeneric(prefixes, spaceOrLine())
|
||||
res += getSeparator(prefixes.last(), nodes.first())
|
||||
res += getSeparator(prefixes.last(), nodes.first(), spaceOrLine())
|
||||
res += groupFn(nodes)
|
||||
return res
|
||||
}
|
||||
@@ -1103,12 +1216,8 @@ internal class Builder(sourceText: String) {
|
||||
private fun getImportUrl(node: Node): String =
|
||||
node.findChildByType(NodeType.STRING_CHARS)!!.text().drop(1).dropLast(1)
|
||||
|
||||
private fun getSeparator(
|
||||
prev: Node,
|
||||
next: Node,
|
||||
separator: FormatNode = spaceOrLine(),
|
||||
): FormatNode {
|
||||
return getSeparator(prev, next) { _, _ -> separator }!!
|
||||
private fun getSeparator(prev: Node, next: Node, separator: FormatNode): FormatNode {
|
||||
return getBaseSeparator(prev, next) ?: separator
|
||||
}
|
||||
|
||||
private fun getSeparator(
|
||||
@@ -1116,6 +1225,10 @@ internal class Builder(sourceText: String) {
|
||||
next: Node,
|
||||
separatorFn: (Node, Node) -> FormatNode?,
|
||||
): FormatNode? {
|
||||
return getBaseSeparator(prev, next) ?: separatorFn(prev, next)
|
||||
}
|
||||
|
||||
private fun getBaseSeparator(prev: Node, next: Node): FormatNode? {
|
||||
return when {
|
||||
prevNode?.type == NodeType.LINE_COMMENT -> {
|
||||
if (prev.linesBetween(next) > 1) {
|
||||
@@ -1124,6 +1237,7 @@ internal class Builder(sourceText: String) {
|
||||
mustForceLine()
|
||||
}
|
||||
}
|
||||
|
||||
hasTrailingAffix(prev, next) -> Space
|
||||
prev.type == NodeType.DOC_COMMENT -> mustForceLine()
|
||||
prev.type == NodeType.ANNOTATION -> forceLine()
|
||||
@@ -1134,17 +1248,21 @@ internal class Builder(sourceText: String) {
|
||||
mustForceLine()
|
||||
}
|
||||
}
|
||||
|
||||
prev.type == NodeType.BLOCK_COMMENT ->
|
||||
if (prev.linesBetween(next) > 0) forceSpaceyLine() else Space
|
||||
|
||||
next.type in EMPTY_SUFFIXES ||
|
||||
prev.isTerminal("[", "!", "@", "[[") ||
|
||||
next.isTerminal("]", "?", ",") -> null
|
||||
next.isTerminal("]", "?", ",") -> Empty
|
||||
|
||||
prev.isTerminal("class", "function", "new") ||
|
||||
next.isTerminal("=", "{", "->", "class", "function") ||
|
||||
next.type == NodeType.OBJECT_BODY ||
|
||||
prev.type == NodeType.MODIFIER_LIST -> Space
|
||||
|
||||
next.type == NodeType.DOC_COMMENT -> TWO_NEWLINES
|
||||
else -> separatorFn(prev, next)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1260,9 +1378,6 @@ internal class Builder(sourceText: String) {
|
||||
private fun Node.isTerminal(vararg texts: String): Boolean =
|
||||
type == NodeType.TERMINAL && text(source) in texts
|
||||
|
||||
private fun Node.isOperator(vararg texts: String): Boolean =
|
||||
type == NodeType.OPERATOR && text(source) in texts
|
||||
|
||||
private fun newId(): Int {
|
||||
return id++
|
||||
}
|
||||
@@ -1293,6 +1408,13 @@ internal class Builder(sourceText: String) {
|
||||
return null
|
||||
}
|
||||
|
||||
private fun List<Node>.lastProperNode(): Node? {
|
||||
for (i in lastIndex downTo 0) {
|
||||
if (this[i].isProper()) return this[i]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// returns true if this node is not an affix or terminal
|
||||
private fun Node.isProper(): Boolean = !type.isAffix && type != NodeType.TERMINAL
|
||||
|
||||
@@ -1305,18 +1427,6 @@ internal class Builder(sourceText: String) {
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <T> Iterable<T>.hasMoreThan(num: Int, predicate: (T) -> Boolean): Boolean {
|
||||
if (this is Collection && isEmpty()) return false
|
||||
var count = 0
|
||||
for (element in this) {
|
||||
if (predicate(element)) count++
|
||||
if (count > num) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
class PeekableIterator<T>(private val iterator: Iterator<T>) : Iterator<T> {
|
||||
private var peek: T? = null
|
||||
|
||||
|
||||
31
pkl-formatter/src/test/files/FormatterSnippetTests/input/expr-chain-grouping.pkl
vendored
Normal file
31
pkl-formatter/src/test/files/FormatterSnippetTests/input/expr-chain-grouping.pkl
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
res1 = foo.bar(new {
|
||||
foo = 1
|
||||
})
|
||||
|
||||
res2 = foo.bar.baz(new {
|
||||
foo =1
|
||||
})
|
||||
|
||||
res3 = foo.bar(3).baz(new {
|
||||
qux = 3
|
||||
})
|
||||
|
||||
res4 = foooooooooooooooooooooooooooooooooooooo.foooooooooooooooooooooooooooooooooooooo.foooooooooooooooooooooooooooooooooooooo.bar(5)
|
||||
|
||||
res5 = foooooooooooooo.foooooooooooooo.foooooooooooooo.foooooooooooooo.foooooooooooooo(4).foooooooooooooo.bar(5)
|
||||
|
||||
res6 = foooooooooooooooooooooooooooooooooooooo.foooooooooooooooooooooooooooooooooooooo.foooooooooooooooooooooooooooooooooooooo.foooooooooooooo(4).foooooooooooooo.bar(5)
|
||||
|
||||
res7 = foooooooooooooo.foooooooooooooo.foooooooooooooo.foooooooooooooo /* hello */ .foooooooooooooo(4).foooooooooooooo.bar(5)
|
||||
|
||||
res8 = foooooooooooooo.foooooooooooooo.foooooooooooooo().foooooooooooooo.foooooooooooooo(4).foooooooooooooo.bar(5)
|
||||
|
||||
res9 = foo.bar(new {
|
||||
foo = 1
|
||||
bar = 2
|
||||
}).qux
|
||||
|
||||
res10 = foo.bar /* some comment */ (5)
|
||||
|
||||
res11 = foo.bar /* some comment */.baz(new { foooooooooooooooooooooooooo = 1; baaaaaaaaaaaaaaaaaaaaar = 2 }).buz
|
||||
|
||||
65
pkl-formatter/src/test/files/FormatterSnippetTests/output/expr-chain-grouping.pkl
vendored
Normal file
65
pkl-formatter/src/test/files/FormatterSnippetTests/output/expr-chain-grouping.pkl
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
res1 =
|
||||
foo.bar(new {
|
||||
foo = 1
|
||||
})
|
||||
|
||||
res2 =
|
||||
foo.bar.baz(new {
|
||||
foo = 1
|
||||
})
|
||||
|
||||
res3 =
|
||||
foo
|
||||
.bar(3)
|
||||
.baz(new {
|
||||
qux = 3
|
||||
})
|
||||
|
||||
res4 =
|
||||
foooooooooooooooooooooooooooooooooooooo
|
||||
.foooooooooooooooooooooooooooooooooooooo
|
||||
.foooooooooooooooooooooooooooooooooooooo
|
||||
.bar(5)
|
||||
|
||||
res5 =
|
||||
foooooooooooooo.foooooooooooooo.foooooooooooooo.foooooooooooooo
|
||||
.foooooooooooooo(4)
|
||||
.foooooooooooooo
|
||||
.bar(5)
|
||||
|
||||
res6 =
|
||||
foooooooooooooooooooooooooooooooooooooo
|
||||
.foooooooooooooooooooooooooooooooooooooo
|
||||
.foooooooooooooooooooooooooooooooooooooo
|
||||
.foooooooooooooo(4)
|
||||
.foooooooooooooo
|
||||
.bar(5)
|
||||
|
||||
res7 =
|
||||
foooooooooooooo.foooooooooooooo.foooooooooooooo.foooooooooooooo /* hello */
|
||||
.foooooooooooooo(4)
|
||||
.foooooooooooooo
|
||||
.bar(5)
|
||||
|
||||
res8 =
|
||||
foooooooooooooo.foooooooooooooo
|
||||
.foooooooooooooo()
|
||||
.foooooooooooooo
|
||||
.foooooooooooooo(4)
|
||||
.foooooooooooooo
|
||||
.bar(5)
|
||||
|
||||
res9 =
|
||||
foo
|
||||
.bar(new {
|
||||
foo = 1
|
||||
bar = 2
|
||||
})
|
||||
.qux
|
||||
|
||||
res10 = foo.bar /* some comment */ (5)
|
||||
|
||||
res11 =
|
||||
foo.bar /* some comment */
|
||||
.baz(new { foooooooooooooooooooooooooo = 1; baaaaaaaaaaaaaaaaaaaaar = 2 })
|
||||
.buz
|
||||
Reference in New Issue
Block a user