aboutsummaryrefslogtreecommitdiff
path: root/core/src/main/java/com/facebook
diff options
context:
space:
mode:
authorOmer Strulovich <ostrulovich@fb.com>2022-04-06 10:27:47 -0700
committerFacebook GitHub Bot <facebook-github-bot@users.noreply.github.com>2022-04-06 10:27:47 -0700
commitc7fffbeac65e15a897d6fae09a4ce82bf7cd01be (patch)
tree303ea0dd9406794731fbf6a96d4676b26f162d8f /core/src/main/java/com/facebook
parentb640331d1e623870a078df7abf0e19e8deab25e2 (diff)
downloadktfmt-c7fffbeac65e15a897d6fae09a4ce82bf7cd01be.tar.gz
Redo qualified chains, without chunks, other modifications and support handling !! and []
Summary: Ok, davidtorosyan and I chatted and I learned that none of us understands emitQualifiedExpression, so we're doomed to rewrite it every time a change is needed. This is an attempt to simplify it a bit - which doesn't mean much. So I will spend the time cleaning and making this more readable later and sit with you all to try and avoid this in the future. What do we have here? - Eliminate usage of chunks - To handle qualified expressions look at the chain parts, then mark where we need to open and close a group. - Then "just" emit stuff using this metadata - We now traverse array expressions (i.e. `[1, 2]`) and unsafe dereferences (i.e. `!!`) as part of the chain. This will improve rendering of them. (Note that `as`, `::` and maybe others may need similar treatment, punting on this Many tests have been updated, to neutral changes (i.e. I have no opinion) but some for the better. Reviewed By: cgrushko Differential Revision: D35224180 fbshipit-source-id: 8de4c633ede5f26d4712d5fb5c9d9b0a32454ee9
Diffstat (limited to 'core/src/main/java/com/facebook')
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt513
1 files changed, 210 insertions, 303 deletions
diff --git a/core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt b/core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt
index f96baa4..da6cc95 100644
--- a/core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt
+++ b/core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt
@@ -56,6 +56,7 @@ import org.jetbrains.kotlin.psi.KtDelegatedSuperTypeEntry
import org.jetbrains.kotlin.psi.KtDestructuringDeclaration
import org.jetbrains.kotlin.psi.KtDestructuringDeclarationEntry
import org.jetbrains.kotlin.psi.KtDoWhileExpression
+import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtDynamicType
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtEnumEntry
@@ -80,6 +81,7 @@ import org.jetbrains.kotlin.psi.KtPackageDirective
import org.jetbrains.kotlin.psi.KtParameter
import org.jetbrains.kotlin.psi.KtParameterList
import org.jetbrains.kotlin.psi.KtParenthesizedExpression
+import org.jetbrains.kotlin.psi.KtPostfixExpression
import org.jetbrains.kotlin.psi.KtPrefixExpression
import org.jetbrains.kotlin.psi.KtPrimaryConstructor
import org.jetbrains.kotlin.psi.KtProjectionKind
@@ -153,27 +155,6 @@ class KotlinInputAstVisitor(
/** Tracks whether we are handling an import directive */
private var inImport = false
- /**
- * Represents a logical "chunk" of [expressions], and whether or not they should be kept on the
- * same line
- *
- * As an example, consider the expression:
- * ```
- * rainbow.red.orange.shine().yellow
- * ```
- * This might be split into chunks as such:
- * ```
- * chunks: [
- * chunk(expressions=[rainbow, red, orange, shine()], shouldKeepOnSameLine=true),
- * chunk(expressions=[yellow], shouldKeepOnSameLine=false)
- * ]
- * ```
- */
- data class Chunk(
- val expressions: List<KtExpression>,
- val shouldKeepOnSameLine: Boolean,
- )
-
/** Example: `fun foo(n: Int) { println(n) }` */
override fun visitNamedFunction(function: KtNamedFunction) {
builder.sync(function)
@@ -485,102 +466,87 @@ class KotlinInputAstVisitor(
return
}
- emitQualifiedExpression(breakIntoChunks(expression))
+ emitQualifiedExpression(expression)
+ }
+
+ /** Extra data to help [emitQualifiedExpression] know when to open and close a group */
+ private class GroupingInfo {
+ var groupOpenCount = 0
+ var shouldCloseGroup = false
}
/**
- * Decomposes a qualified expression into chunks.
+ * Handles a chain of qualified expressions, i.e. `a[5].b!!.c()[4].f()`
*
- * So this expression:
- * ```
- * rainbow.red.orange.shine().yellow
- * ```
+ * This is by far the most complicated part of this formatter. We start by breaking the expression
+ * to the steps it is executed in (which are in the opposite order of how the syntax tree is
+ * built).
*
- * Becomes:
- * ```
- * chunks: [
- * chunk(expressions=[rainbow, red, orange, shine()], shouldKeepOnSameLine=true),
- * chunk(expressions=[yellow], shouldKeepOnSameLine=false)
- * ]
- * ```
+ * We then calculate information to know which parts need to be groups, and finally go part by
+ * part, emitting it to the [builder] while closing and opening groups.
*/
- private fun breakIntoChunks(expression: KtExpression): List<Chunk> {
+ private fun emitQualifiedExpression(expression: KtExpression) {
val parts = breakIntoParts(expression)
-
- val prefixes = mutableSetOf<Int>()
-
- // Check if the dot chain has a prefix that looks like a type name, so we can
- // treat the type name-shaped part as a single syntactic unit.
- TypeNameClassifier.typePrefixLength(simpleNames(parts)).ifPresent { prefixes.add(it) }
-
- val invocationCount = parts.count { it.isCallExpression() }
- val firstInvocationIndex = parts.indexOfFirst { it.isCallExpression() }
- val isFirstInvocationLambda = parts.getOrNull(firstInvocationIndex)?.isLambda() ?: false
val hasTrailingLambda = parts.last().isLambda()
-
- // If there's only one invocation, treat leading field accesses as a single
- // unit. In the normal case we want to preserve the alignment of subsequent
- // method calls, and would emit e.g.:
- //
- // myField
- // .foo()
- // .bar();
- //
- // But if there's no 'bar()' to worry about the alignment of we prefer:
- //
- // myField.foo();
- //
- // to:
- //
- // myField
- // .foo();
- //
- val singleInvocation = invocationCount == 1
-
- // For this case, don't count trailing lambdas as call expressions so they look like:
- // ```
- // blah.foo().bar().map {
- // // blah
- // }
- // ```
- //
- val singleInvocationWithTrailingLambda = invocationCount == 2 && hasTrailingLambda
-
- if ((singleInvocation || singleInvocationWithTrailingLambda) && firstInvocationIndex > 0) {
- prefixes.add(
- if (firstInvocationIndex != parts.size - 1 && isFirstInvocationLambda) {
- firstInvocationIndex - 1
- } else {
- firstInvocationIndex
- })
- }
-
- // keep `super` and `this` attached to the first dereference
- if (prefixes.isEmpty() &&
- (parts.first() is KtSuperExpression || parts.first() is KtThisExpression)) {
- prefixes.add(1)
- }
-
- // now that we've found the prefixes, break the parts into chunks
- val chunks = mutableListOf<Chunk>()
- val currentChunk = mutableListOf<KtExpression>()
- val unconsumedPrefixes = ArrayDeque(prefixes.sorted())
-
- parts.forEachIndexed { index, part ->
- currentChunk.add(part)
- if (!unconsumedPrefixes.isEmpty() && index == unconsumedPrefixes.peekFirst()) {
- unconsumedPrefixes.removeFirst()
- chunks.add(Chunk(currentChunk.toList(), shouldKeepOnSameLine = true))
- currentChunk.clear()
+ val groupingInfos = computeGroupingInfo(parts, hasTrailingLambda)
+ builder.block(expressionBreakIndent) {
+ val nameTag = genSym() // allows adjusting arguments indentation if a break will be made
+ for ((index, ktExpression) in parts.withIndex()) {
+ if (ktExpression is KtQualifiedExpression) {
+ builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO, Optional.of(nameTag))
+ }
+ repeat(groupingInfos[index].groupOpenCount) { builder.open(ZERO) }
+ when (ktExpression) {
+ is KtQualifiedExpression -> {
+ builder.token(ktExpression.operationSign.value)
+ val selectorExpression = ktExpression.selectorExpression
+ if (selectorExpression !is KtCallExpression) {
+ // selector is a simple field access
+ visit(selectorExpression)
+ if (groupingInfos[index].shouldCloseGroup) {
+ builder.close()
+ }
+ } else {
+ // selector is a function call, we may close a group after its name
+ // emit `doIt` from `doIt(1, 2) { it }`
+ visit(selectorExpression.calleeExpression)
+ // close groups according to instructions
+ if (groupingInfos[index].shouldCloseGroup) {
+ builder.close()
+ }
+ // close group due to last lambda to allow block-like style in `as.forEach { ... }`
+ val isTrailingLambda = hasTrailingLambda && index == parts.size - 1
+ if (isTrailingLambda) {
+ builder.close()
+ }
+ val argsIndentElse = if (index == parts.size - 1) ZERO else expressionBreakIndent
+ val lambdaIndentElse = if (isTrailingLambda) expressionBreakNegativeIndent else ZERO
+ // emit `(1, 2) { it }` from `doIt(1, 2) { it }`
+ visitCallElement(
+ null,
+ selectorExpression.typeArgumentList,
+ selectorExpression.valueArgumentList,
+ selectorExpression.lambdaArguments,
+ argumentsIndent = Indent.If.make(nameTag, expressionBreakIndent, argsIndentElse),
+ lambdaIndent = Indent.If.make(nameTag, ZERO, lambdaIndentElse),
+ )
+ }
+ }
+ is KtArrayAccessExpression -> {
+ visitArrayAccessBrackets(ktExpression)
+ builder.close()
+ }
+ is KtPostfixExpression -> {
+ builder.token(ktExpression.operationReference.text)
+ builder.close()
+ }
+ else -> {
+ check(index == 0)
+ visit(ktExpression)
+ }
+ }
}
}
-
- // the last chunk is part of a prefix, so it's not grouped
- if (currentChunk.isNotEmpty()) {
- chunks.add(Chunk(currentChunk.toList(), shouldKeepOnSameLine = false))
- }
-
- return chunks
}
/**
@@ -591,222 +557,151 @@ class KotlinInputAstVisitor(
val parts = ArrayDeque<KtExpression>()
// use an ArrayDeque and add elements to the beginning so the innermost expression comes first
+ // foo.bar.yay -> [yay, bar.yay, foo.bar.yay]
parts.addFirst(expression)
- var node = expression
- while (node is KtQualifiedExpression) {
- node = node.receiverExpression
- parts.addFirst(node)
+ var node: KtExpression? = expression
+ while (node != null) {
+ node =
+ when (node) {
+ is KtQualifiedExpression -> node.receiverExpression
+ is KtArrayAccessExpression -> node.arrayExpression
+ is KtPostfixExpression -> node.baseExpression
+ else -> null
+ }
+ if (node != null) {
+ parts.addFirst(node)
+ }
}
return parts.toList()
}
- /** Returns true if the expression represents an invocation */
- private fun KtExpression.isCallExpression(): Boolean {
- return extractCallExpression(this) != null
- }
-
- /** Returns true if the expression represents an invocation that is also a lambda */
- private fun KtExpression.isLambda(): Boolean {
- return extractCallExpression(this)?.lambdaArguments?.isNotEmpty() ?: false
- }
-
- /**
- * emitQualifiedExpression formats call expressions that are either part of a qualified
- * expression, or standing alone. This method makes it easier to handle both cases uniformly.
- */
- private fun extractCallExpression(expression: KtExpression): KtCallExpression? {
- val ktExpression = (expression as? KtQualifiedExpression)?.selectorExpression ?: expression
- return ktExpression as? KtCallExpression
- }
-
- /**
- * Returns the simple names of expressions in a "." chain, e.g., "foo.bar().zed[5]" --> [foo, bar,
- * zed]
- */
- private fun simpleNames(stack: List<KtExpression>): List<String> {
- val simpleNames = mutableListOf<String>()
- loop@ for (expression in stack) {
- val callExpression = extractCallExpression(expression)
- if (callExpression != null) {
- callExpression.calleeExpression?.text?.let { simpleNames.add(it) }
- break@loop
- }
- when (expression) {
- is KtQualifiedExpression -> expression.selectorExpression?.let { simpleNames.add(it.text) }
- is KtReferenceExpression -> simpleNames.add(expression.text)
- else -> break@loop
- }
- }
- return simpleNames
- }
-
/**
- * Output a chain of dereferences, some of which should be grouped together.
+ * Generates the [GroupingInfo] array to go with an array of [KtQualifiedExpression] parts
+ *
+ * For example, the expression `a.b[2].c.d()` is made of four expressions:
+ * 1. [KtQualifiedExpression] `a.b[2].c . d()` (this will be `parts[4]`)
+ * 1. [KtQualifiedExpression] `a.b[2] . c` (this will be `parts[3]`)
+ * 2. [KtArrayAccessExpression] `a.b [2]` (this will be `parts[2]`)
+ * 3. [KtQualifiedExpression] `a . b` (this will be `parts[1]`)
+ * 4. [KtSimpleNameExpression] `a` (this will be `parts[0]`)
+ *
+ * Once in parts, these are in the reverse order. To render the array correct we need to make sure
+ * `b` and [2] are in a group so we avoid splitting them. To do so we need to open a group for `b`
+ * (that will be done in part 2), and always close a group for an array.
*
- * Example 1:
+ * Here is the same expression, with justified braces marking the groupings it will get:
* ```
- * field1.field2.field3.field4
- * .method1(...)
+ * a . b [2] . c . d ()
+ * {a . b} --> Grouping `a.b` because it can be a package name or simple field access so we add 1
+ * to the number of groups to open at groupingInfos[0], and mark to close a group at
+ * groupingInfos[1]
+ * {a . b [2]} --> Grouping `a.b` with `[2]`, since otherwise we may break inside the brackets
+ * instead of preferring breaks before dots. So we open a group at [0], but since
+ * we always close a group after brackets, we don't store that information.
+ * {c . d} --> another group to attach the first function name to the fields before it
+ * this time we don't start the group in the beginning, and use
+ * lastIndexToOpen to track the spot after the last time we stopped
+ * grouping.
* ```
- *
- * Example 2:
+ * The final expression with groupings:
* ```
- * com.facebook.ktfmt.KotlinInputAstVisitor
- * .method1()
+ * {{a.b}[2]}.{c.d}()
* ```
*/
- private fun emitQualifiedExpression(
- chunks: List<Chunk>,
- ) {
- // Keeps track of how much text we've emitted so far, and is used to avoid line breaks that are
- // shorter than the indentation length. For example:
- // ```
- // user.field1
- // .field2
- // ...
- // .field9
- // ```
- // Since `user` is less than or equal to 4 characters (the indentation length here), `field1` is
- // kept on the same line.
- var textLength = 0
-
- // is the last expression a lambda? e.g.`foo.bar.apply { ... }`
- val hasTrailingLambda = chunks.last().expressions.last().isLambda()
-
- // When the last chunk is meant to be on one line, reduce the indentation of arguments:
- // ```
- // rainbow.shine(
- // infrared,
- // ultraviolet,
- // )
- // ```
- // Here's an example of the line being broken, so we don't reduce the indentation:
- // ```
- // rainbow.red.orange.yellow
- // .shine(
- // infrared,
- // ultraviolet,
- // )
- // ```
- // Here's a negative side effect, can't be fixed unless we can detect that the invocation is not
- // on the same line as the first reference (not currently possible):
- // ```
- // rainbow.red.orange.yellow
- // .key.shine(
- // infrared,
- // ultraviolet,
- // )
- // ```
- val argsIndentElse = if (chunks.last().shouldKeepOnSameLine) ZERO else expressionBreakIndent
-
- // When we have a trailing lambda and the line it's on isn't broken, reduce its indentation:
- // ```
- // rainbow.let {
- // it.shine()
- // }
- // ```
- // Here's an example of the line being broken, so we don't reduce the indentation:
- // ```
- // rainbow
- // .red
- // .orange
- // .let {
- // it.shine()
- // }
- // ```
- // Here's a negative side effect, can't be fixed unless we can detect that the lambda is not on
- // the same line as the first reference (not currently possible):
- // ```
- // rainbow.red.orange
- // .yellow.let {
- // it.shine()
- // }
- // ```
- val lambdaIndentElse = if (hasTrailingLambda) expressionBreakNegativeIndent else ZERO
-
- builder.block(expressionBreakIndent) {
- // trailing lambdas get their own block, so wrap everything before it in a block
- if (hasTrailingLambda) {
- builder.open(ZERO)
- }
-
- // chunks that are grouped get their own block
- chunks.filter { it.shouldKeepOnSameLine }.forEach { builder.open(ZERO) }
-
- // each chunk represents a list of related expressions.
- // if the expressions are "grouped", they'll be in the same block and be on the same line.
- // otherwise they'll be broken onto several lines (assuming they don't fit on one).
- for ((chunkIndex, chunk) in chunks.withIndex()) {
- // get a unique name for this chunk, used for keeping track of indents and line breaks
- val nameTag = genSym()
-
- // each item represents a dereference or a call invocation
- val items = chunk.expressions
- for ((itemIndex, item) in items.withIndex()) {
-
- // for everything after the very first element, emit a break and a dot
- if (chunkIndex > 0 || itemIndex > 0) {
-
- // break if there's a lambda, or the line is long enough
- if (textLength > options.continuationIndent || item.isLambda()) {
- val fillMode =
- if (chunk.shouldKeepOnSameLine) Doc.FillMode.INDEPENDENT else Doc.FillMode.UNIFIED
- builder.breakOp(fillMode, "", ZERO, Optional.of(nameTag))
- }
-
- val operator = (item as KtQualifiedExpression).operationSign.value
- builder.token(operator)
- textLength += operator.length
- }
-
- // emit the reference or method name
- emitSelectorUpToParenthesis(item)
-
- // we've reached the last element of this chunk
- if (itemIndex == items.indices.last()) {
-
- // close the grouping block before visiting the call expression body (if any)
- if (chunk.shouldKeepOnSameLine) {
- builder.close()
- }
-
- // we've reached the trailing lambda, close its block before visiting the body
- if (chunkIndex == chunks.indices.last && hasTrailingLambda) {
- builder.close()
- }
- }
-
- // visit the call expression body (if any)
- extractCallExpression(item)?.apply {
- visitCallElement(
- null,
- typeArgumentList,
- valueArgumentList,
- lambdaArguments,
- argumentsIndent = Indent.If.make(nameTag, expressionBreakIndent, argsIndentElse),
- lambdaIndent = Indent.If.make(nameTag, ZERO, lambdaIndentElse))
+ private fun computeGroupingInfo(
+ parts: List<KtExpression>,
+ hasTrailingLambda: Boolean
+ ): List<GroupingInfo> {
+ val groupingInfos = List(parts.size) { GroupingInfo() }
+ var lastIndexToOpen = 0
+ for ((index, part) in parts.withIndex()) {
+ when (part) {
+ is KtQualifiedExpression -> {
+ val receiverExpression = part.receiverExpression
+ val previous =
+ (receiverExpression as? KtQualifiedExpression)?.selectorExpression
+ ?: receiverExpression
+ val current = checkNotNull(part.selectorExpression)
+ if (shouldGroupPartWithPrevious(parts, part, index, previous, current)) {
+ // this and the previous items should be grouped for better style
+ // we add another group to open in the current index we have been using
+ groupingInfos[lastIndexToOpen].groupOpenCount++
+ // we don't always close a group when emitting this node, so we need this flag to
+ // mark if we need to close a group
+ groupingInfos[index].shouldCloseGroup = true
+ } else {
+ // use this index in to open future groups
+ lastIndexToOpen = index
}
-
- textLength += item.text.length
+ }
+ is KtArrayAccessExpression, is KtPostfixExpression -> {
+ // we group these with the last item with a name, and we always close them
+ groupingInfos[lastIndexToOpen].groupOpenCount++
}
}
}
+ if (hasTrailingLambda) {
+ // a trailing lambda adds a group that we stop before emitting the lambda
+ groupingInfos[0].groupOpenCount++
+ }
+ return groupingInfos
+ }
+
+ /** Decide whether a [KtQualifiedExpression] part should be grouped with the previous part */
+ private fun shouldGroupPartWithPrevious(
+ parts: List<KtExpression>,
+ part: KtExpression,
+ index: Int,
+ previous: KtExpression,
+ current: KtExpression
+ ): Boolean {
+ // this is the second, and the first is short, avoid `.` "hanging in air"
+ if (index == 1 && previous.text.length < options.continuationIndent) {
+ return true
+ }
+ // the previous part is `this` or `super`
+ if (previous is KtSuperExpression || previous is KtThisExpression) {
+ return true
+ }
+ // this and the previous part are a package name, type name, or property
+ if (previous is KtSimpleNameExpression &&
+ current is KtSimpleNameExpression &&
+ part is KtDotQualifiedExpression) {
+ return true
+ }
+ // this is `Foo` in `com.facebook.Foo`, so everything before it is a package name
+ if (current.text.first().isUpperCase() &&
+ current is KtSimpleNameExpression &&
+ part is KtDotQualifiedExpression) {
+ return true
+ }
+ // this is the `foo()` in `com.facebook.Foo.foo()` or in `Foo.foo()`
+ if (current is KtCallExpression &&
+ (previous !is KtCallExpression) &&
+ previous.text?.firstOrNull()?.isUpperCase() == true) {
+ return true
+ }
+ // this is an invocation and the last item, and the previous it not, i.e. `a.b.c()`
+ // keeping it grouped and splitting the arguments makes `a.b(...)` feel like `aab()`
+ return current is KtCallExpression &&
+ previous !is KtCallExpression &&
+ index == parts.indices.last
+ }
+
+ /** Returns true if the expression represents an invocation that is also a lambda */
+ private fun KtExpression.isLambda(): Boolean {
+ return extractCallExpression(this)?.lambdaArguments?.isNotEmpty() ?: false
}
/**
- * Emits a method name up to its parenthesis and arguments.
- *
- * More generally, emits the selector excluding its parameters if it's a method call.
+ * emitQualifiedExpression formats call expressions that are either part of a qualified
+ * expression, or standing alone. This method makes it easier to handle both cases uniformly.
*/
- private fun emitSelectorUpToParenthesis(e: KtExpression) {
- val callExpression = extractCallExpression(e)
- when {
- callExpression != null -> visit(callExpression.calleeExpression)
- e is KtQualifiedExpression -> visit(e.selectorExpression)
- else -> visit(e)
- }
+ private fun extractCallExpression(expression: KtExpression): KtCallExpression? {
+ val ktExpression = (expression as? KtQualifiedExpression)?.selectorExpression ?: expression
+ return ktExpression as? KtCallExpression
}
override fun visitCallExpression(callExpression: KtCallExpression) {
@@ -1898,10 +1793,22 @@ class KotlinInputAstVisitor(
}
}
- /** Example `a[3]` or `b["a", 5]` */
+ /** Example `a[3]`, `b["a", 5]` or `a.b.c[4]` */
override fun visitArrayAccessExpression(expression: KtArrayAccessExpression) {
builder.sync(expression)
- visit(expression.arrayExpression)
+ if (expression.arrayExpression is KtQualifiedExpression) {
+ emitQualifiedExpression(expression)
+ } else {
+ visit(expression.arrayExpression)
+ visitArrayAccessBrackets(expression)
+ }
+ }
+
+ /**
+ * Example `[3]` in `a[3]` or `a[3].b` Separated since it needs to be used from a top level array
+ * expression (`a[3]`) and from within a qualified chain (`a[3].b)
+ */
+ private fun visitArrayAccessBrackets(expression: KtArrayAccessExpression) {
builder.block(ZERO) {
builder.token("[")
builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakIndent)