Try to keep package names together
Summary: When seeing things that look as package names, according to the first reference name in them, we will now try to keep them together.
Reviewed By: cgrushko
Differential Revision: D20251444
fbshipit-source-id: 54dc01b785500235b209bd8cbde966de26227bb0
diff --git a/core/src/main/java/com/facebook/ktfmt/KotlinInputAstVisitor.kt b/core/src/main/java/com/facebook/ktfmt/KotlinInputAstVisitor.kt
index 370beac..54a88c7 100644
--- a/core/src/main/java/com/facebook/ktfmt/KotlinInputAstVisitor.kt
+++ b/core/src/main/java/com/facebook/ktfmt/KotlinInputAstVisitor.kt
@@ -421,15 +421,25 @@
builder.block(ZERO) {
val leftMostExpression = parts.first()
- leftMostExpression.receiverExpression.accept(this)
+ val leftMostReceiverExpression = leftMostExpression.receiverExpression
+ val typePrefixSections = getTypePrefixLength(expression)
+ var count = 0
+ if (typePrefixSections > 0) {
+ builder.open(ZERO)
+ }
+ leftMostReceiverExpression.accept(this)
for (receiver in parts) {
val isFirst = receiver === leftMostExpression
if (!isFirst || receiver.receiverExpression is KtCallExpression) {
builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakIndent)
}
builder.token(receiver.operationSign.value)
+ val selectorExpression = receiver.selectorExpression
builder.block(if (isFirst) ZERO else expressionBreakIndent) {
- receiver.selectorExpression?.accept(this)
+ selectorExpression?.accept(this)
+ }
+ if (typePrefixSections > 0 && ++count == typePrefixSections) {
+ builder.close()
}
}
}
@@ -1631,7 +1641,7 @@
}
/**
- * Emit a [Doc.Token] .
+ * Emit a [Doc.Token].
*
* @param token the [String] to wrap in a [Doc.Token]
* @param plusIndentCommentsBefore extra block for comments before this token
diff --git a/core/src/main/java/com/facebook/ktfmt/TypePrefix.kt b/core/src/main/java/com/facebook/ktfmt/TypePrefix.kt
new file mode 100644
index 0000000..f0e0055
--- /dev/null
+++ b/core/src/main/java/com/facebook/ktfmt/TypePrefix.kt
@@ -0,0 +1,40 @@
+// (c) Facebook, Inc. and its affiliates. Confidential and proprietary.
+
+package com.facebook.ktfmt
+
+import org.jetbrains.kotlin.psi.KtCallExpression
+import org.jetbrains.kotlin.psi.KtExpression
+import org.jetbrains.kotlin.psi.KtQualifiedExpression
+import org.jetbrains.kotlin.psi.KtReferenceExpression
+
+/**
+ * A list of common package name initial domain names This is used in order to avoid splitting
+ * package names when possible
+ */
+private val packageNames = setOf("com", "gov", "java", "javax", "kotlin", "org")
+
+/**
+ * Guesses the amount of parts in a qualified expression that might represent a type name
+ *
+ * Examples:
+ * ```
+ * com.facebook.ktfmt.Formatter.doIt() -> 3
+ * Formatter.doIt() -> 0
+ * myObject.field.anotherField = 0
+ * ```
+ */
+fun getTypePrefixLength(expression: KtQualifiedExpression): Int {
+ var current: KtExpression = expression
+ var count = 0
+ while (current is KtQualifiedExpression) {
+ val selectorExpression = current.selectorExpression
+ if (selectorExpression is KtCallExpression ||
+ (selectorExpression is KtReferenceExpression &&
+ selectorExpression.text?.first()?.isUpperCase() == true)) {
+ count = 0
+ }
+ count++
+ current = current.receiverExpression
+ }
+ return if ((current as? KtReferenceExpression)?.text in packageNames) count else 0
+}
diff --git a/core/src/test/java/com/facebook/ktfmt/FormatterKtTest.kt b/core/src/test/java/com/facebook/ktfmt/FormatterKtTest.kt
index 5b8a6a3..c0f2b58 100644
--- a/core/src/test/java/com/facebook/ktfmt/FormatterKtTest.kt
+++ b/core/src/test/java/com/facebook/ktfmt/FormatterKtTest.kt
@@ -409,6 +409,31 @@
deduceMaxWidth = true)
@Test
+ fun `avoid breaking suspected package names`() =
+ assertFormatted(
+ """
+ |-----------------------
+ |fun f() {
+ | com.facebook.Foo
+ | .format()
+ | org.facebook.Foo
+ | .format()
+ | java.lang.stuff.Foo
+ | .format()
+ | javax.lang.stuff.Foo
+ | .format()
+ | kotlin.lang.Foo
+ | .format()
+ |
+ | // comparison:
+ | foo.facebook
+ | .Foo
+ | .format()
+ |}
+ |""".trimMargin(),
+ deduceMaxWidth = true)
+
+ @Test
fun `import list`() {
val code =
"""
diff --git a/core/src/test/java/com/facebook/ktfmt/TypePrefixKtTest.kt b/core/src/test/java/com/facebook/ktfmt/TypePrefixKtTest.kt
new file mode 100644
index 0000000..7e5bff3
--- /dev/null
+++ b/core/src/test/java/com/facebook/ktfmt/TypePrefixKtTest.kt
@@ -0,0 +1,51 @@
+// (c) Facebook, Inc. and its affiliates. Confidential and proprietary.
+
+package com.facebook.ktfmt
+
+import com.intellij.psi.PsiElement
+import org.jetbrains.kotlin.psi.KtQualifiedExpression
+import org.jetbrains.kotlin.psi.KtTreeVisitorVoid
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@Suppress("FunctionNaming")
+@RunWith(JUnit4::class)
+class TypePrefixKtTest {
+
+ @Test
+ fun `when first few names look like package names return proper count`() {
+ val expression = getQualifiedExpression("com.facebook.ktfmt.Formatter.doIt()")
+ assertEquals(3, getTypePrefixLength(expression))
+ }
+
+ @Test
+ fun `when a chain of field calls return 0`() {
+ val expression = getQualifiedExpression("myObject.field.anotherField")
+ assertEquals(0, getTypePrefixLength(expression))
+ }
+
+ @Test
+ fun `when starts with possible class name return 0`() {
+ val expression = getQualifiedExpression("Formatter.doIt()")
+ assertEquals(0, getTypePrefixLength(expression))
+ }
+
+ fun getQualifiedExpression(code: String): KtQualifiedExpression {
+ var result: PsiElement? = null
+ Parser.parse("fun f() { $code }")
+ .accept(
+ object : KtTreeVisitorVoid() {
+ override fun visitElement(element: PsiElement) {
+ if (element.text == code && result == null) {
+ result = element
+ return
+ }
+ super.visitElement(element)
+ }
+ })
+
+ return result as KtQualifiedExpression
+ }
+}