aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/publish_artifacts_on_release.yaml10
-rw-r--r--.gitmodules3
-rw-r--r--METADATA22
-rw-r--r--README.md11
-rw-r--r--RELEASING.md2
-rw-r--r--core/pom.xml17
-rw-r--r--core/src/main/java/com/facebook/ktfmt/cli/Main.kt19
-rw-r--r--core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt3
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/EnumEntryList.kt88
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/FenceCommentsOp.kt4
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/Formatter.kt23
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/FormattingOptions.kt11
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt620
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/ParseError.kt19
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/PsiUtils.kt48
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/RedundantElementManager.kt (renamed from core/src/main/java/com/facebook/ktfmt/format/RedundantElementRemover.kt)54
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/RedundantImportDetector.kt27
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/RedundantSemicolonDetector.kt38
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/Tokenizer.kt80
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/TrailingCommas.kt162
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/TypeNameClassifier.kt147
-rw-r--r--core/src/main/java/com/facebook/ktfmt/kdoc/Paragraph.kt16
-rw-r--r--core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphList.kt2
-rw-r--r--core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphListBuilder.kt2
-rw-r--r--core/src/test/java/com/facebook/ktfmt/cli/MainTest.kt141
-rw-r--r--core/src/test/java/com/facebook/ktfmt/format/FormatterTest.kt1594
-rw-r--r--core/src/test/java/com/facebook/ktfmt/format/GoogleStyleFormatterKtTest.kt720
-rw-r--r--core/src/test/java/com/facebook/ktfmt/format/TokenizerTest.kt112
-rw-r--r--core/src/test/java/com/facebook/ktfmt/kdoc/KDocFormatterTest.kt35
-rw-r--r--core/src/test/java/com/facebook/ktfmt/testutil/KtfmtTruth.kt32
-rw-r--r--docs/editorconfig/.editorconfig-dropbox1
-rw-r--r--docs/editorconfig/.editorconfig-kotlinlang3
-rw-r--r--ktfmt_idea_plugin/build.gradle.kts25
-rw-r--r--ktfmt_idea_plugin/gradle/wrapper/gradle-wrapper.jarbin58910 -> 63375 bytes
-rw-r--r--ktfmt_idea_plugin/gradle/wrapper/gradle-wrapper.properties4
-rwxr-xr-xktfmt_idea_plugin/gradlew281
-rw-r--r--ktfmt_idea_plugin/gradlew.bat39
-rw-r--r--ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/CodeStyleManagerDecorator.java239
-rw-r--r--ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/FormatterUtil.java51
-rw-r--r--ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/InitialConfigurationStartupActivity.java (renamed from ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/InitialConfigurationProjectManagerListener.java)20
-rw-r--r--ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtCodeStyleManager.java154
-rw-r--r--ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtConfigurable.java4
-rw-r--r--ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtFormattingService.java94
-rw-r--r--ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtInstaller.java58
-rw-r--r--ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtSettings.java8
-rw-r--r--ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/Notifications.java (renamed from ktfmt_idea_plugin/src/test/java/com/facebook/ktfmt/intellij/FormatterUtilTest.java)18
-rw-r--r--ktfmt_idea_plugin/src/main/resources/META-INF/plugin.xml16
-rw-r--r--online_formatter/build.gradle.kts10
-rw-r--r--online_formatter/gradle/wrapper/gradle-wrapper.properties4
-rw-r--r--pom.xml12
-rw-r--r--stable_version.txt1
-rw-r--r--version.txt2
-rw-r--r--website/index.html4
-rw-r--r--website/package-lock.json12
54 files changed, 3362 insertions, 1760 deletions
diff --git a/.github/workflows/publish_artifacts_on_release.yaml b/.github/workflows/publish_artifacts_on_release.yaml
index 85ecb96..a48a70a 100644
--- a/.github/workflows/publish_artifacts_on_release.yaml
+++ b/.github/workflows/publish_artifacts_on_release.yaml
@@ -10,11 +10,19 @@ name: Publish package to Maven Central and JetBrains Marketplace
on:
release:
types: [created]
+ workflow_dispatch:
+ inputs:
+ release_tag:
+ description: 'Release tag'
+ required: true
+ type: stringe
jobs:
publish:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.events.release.tag_name || inputs.release_tag || github.ref }}
- name: Set up Maven Central Repository
uses: actions/setup-java@v1
with:
diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index ded40a0..0000000
--- a/.gitmodules
+++ /dev/null
@@ -1,3 +0,0 @@
-[submodule "vendor/google-java-format"]
- path = vendor/google-java-format
- url = https://github.com/google/google-java-format.git
diff --git a/METADATA b/METADATA
index 23da49e..a498e59 100644
--- a/METADATA
+++ b/METADATA
@@ -1,13 +1,19 @@
-name: "ktfmt"
-description:
- "A formatter for Kotlin code."
+# This project was upgraded with external_updater.
+# Usage: tools/external_updater/updater.sh update external/ktfmt
+# For more info, check https://cs.android.com/android/platform/superproject/+/main:tools/external_updater/README.md
+name: "ktfmt"
+description: "A formatter for Kotlin code."
third_party {
- url {
- type: GIT
+ license_type: NOTICE
+ last_upgrade_date {
+ year: 2024
+ month: 5
+ day: 22
+ }
+ identifier {
+ type: "Git"
value: "https://github.com/facebookincubator/ktfmt.git"
+ version: "v0.49"
}
- version: "v0.39"
- last_upgrade_date { year: 2022 month: 7 day: 25 }
- license_type: NOTICE
}
diff --git a/README.md b/README.md
index 71b7c8b..22762f4 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,4 @@
-[![](https://github.com/facebookincubator/ktfmt/workflows/build/badge.svg)](https://github.com/facebookincubator/ktfmt/actions?query=workflow%3Abuild)
-
-# ktfmt
+# ktfmt [![GitHub release](https://img.shields.io/github/release/facebook/ktfmt?sort=semver)](https://github.com/facebook/ktfmt/releases/) [![](https://github.com/facebook/ktfmt/workflows/Build%20and%20Test/badge.svg)](https://github.com/facebook/ktfmt/actions/workflows/build_and_test.yml "GitHub Actions workflow status") [![slack](https://img.shields.io/badge/Slack-ktfmt-purple.svg?logo=slack)](https://slack-chats.kotlinlang.org/c/ktfmt) [![invite](https://img.shields.io/badge/Request%20a%20Slack%20invite-8A2BE2)](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up) [![issues - ktfmt](https://img.shields.io/github/issues/facebook/ktfmt)](https://github.com/facebook/ktfmt/issues)
`ktfmt` is a program that pretty-prints (formats) Kotlin code, based on [google-java-format](https://github.com/google/google-java-format).
@@ -20,6 +18,11 @@ For comparison, the same code formatted by [`ktlint`](https://github.com/pintere
| ------ | --------|
| ![ktlint](docs/images/ktlint.png) | ![IntelliJ](docs/images/intellij.png) |
+## Playground
+
+We have a [live playground](https://facebook.github.io/ktfmt/) where you can easily see how ktfmt would format your code.
+Give it a try! https://facebook.github.io/ktfmt/
+
## Using the formatter
### IntelliJ, Android Studio, and other JetBrains IDEs
@@ -53,7 +56,7 @@ the project's code style settings.
### from the command-line
-[Download the formatter](https://github.com/facebookincubator/ktfmt/releases)
+[Download the formatter](https://github.com/facebook/ktfmt/releases)
and run it with:
```
diff --git a/RELEASING.md b/RELEASING.md
index 64d5745..f98cda5 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -4,7 +4,7 @@
2. Create a new Release in GitHub. A GitHub Action is automatically triggered and builds and publishes the artifacts to
1. Maven
2. IntelliJ Plugin marketplace
-3. TODO: also automate website generation (https://facebookincubator.github.io/ktfmt/) and the AWS Lambda that powers it. For now, you must clone the repo locally, and manually run some steps.
+3. TODO: also automate website generation (https://facebook.github.io/ktfmt/) and the AWS Lambda that powers it. For now, you must clone the repo locally, and manually run some steps.
1. pushd online_formatter; ./build_and_deploy.sh; popd
1. Credentials should be configured using https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#credentials
2. Follow instructions in website/README.md
diff --git a/core/pom.xml b/core/pom.xml
index c5e5119..e892ae3 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -11,12 +11,12 @@
<parent>
<groupId>com.facebook</groupId>
<artifactId>ktfmt-parent</artifactId>
- <version>0.43</version>
+ <version>0.49</version>
</parent>
<properties>
<dokka.version>0.10.1</dokka.version>
- <kotlin.version>1.6.10</kotlin.version>
+ <kotlin.version>1.8.22</kotlin.version>
<kotlin.compiler.incremental>true</kotlin.compiler.incremental>
<kotlin.compiler.jvmTarget>1.8</kotlin.compiler.jvmTarget>
<main.class>com.facebook.ktfmt.cli.Main</main.class>
@@ -137,6 +137,15 @@
</execution>
</executions>
</plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>3.2.5</version>
+ <configuration>
+ <!-- Tests that everything works when using an unusual default Charset. -->
+ <argLine>-Dfile.encoding=UTF-16</argLine>
+ </configuration>
+ </plugin>
</plugins>
</build>
@@ -144,7 +153,7 @@
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
- <version>29.0-jre</version>
+ <version>32.0.0-jre</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
@@ -170,7 +179,7 @@
<dependency>
<groupId>com.google.googlejavaformat</groupId>
<artifactId>google-java-format</artifactId>
- <version>1.8</version>
+ <version>1.22.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
diff --git a/core/src/main/java/com/facebook/ktfmt/cli/Main.kt b/core/src/main/java/com/facebook/ktfmt/cli/Main.kt
index 980897d..5f46eb9 100644
--- a/core/src/main/java/com/facebook/ktfmt/cli/Main.kt
+++ b/core/src/main/java/com/facebook/ktfmt/cli/Main.kt
@@ -20,11 +20,15 @@ import com.facebook.ktfmt.format.Formatter
import com.facebook.ktfmt.format.ParseError
import com.google.googlejavaformat.FormattingError
import java.io.BufferedReader
+import java.io.BufferedWriter
import java.io.File
+import java.io.FileInputStream
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
+import java.io.OutputStreamWriter
import java.io.PrintStream
+import java.nio.charset.StandardCharsets.UTF_8
import java.util.concurrent.atomic.AtomicInteger
import kotlin.system.exitProcess
@@ -125,7 +129,8 @@ class Main(
private fun format(file: File?): Boolean {
val fileName = file?.toString() ?: parsedArgs.stdinName ?: "<stdin>"
try {
- val code = file?.readText() ?: BufferedReader(InputStreamReader(input)).readText()
+ val bytes = if (file == null) input else FileInputStream(file)
+ val code = BufferedReader(InputStreamReader(bytes, UTF_8)).readText()
val formattedCode = Formatter.format(parsedArgs.formattingOptions, code)
val alreadyFormatted = code == formattedCode
@@ -136,7 +141,7 @@ class Main(
out.println(fileName)
}
} else {
- out.print(formattedCode)
+ BufferedWriter(OutputStreamWriter(out, UTF_8)).use { it.write(formattedCode) }
}
return alreadyFormatted
}
@@ -148,7 +153,7 @@ class Main(
} else {
// TODO(T111284144): Add tests
if (!alreadyFormatted) {
- file.writeText(formattedCode)
+ file.writeText(formattedCode, UTF_8)
}
err.println("Done formatting $fileName")
}
@@ -158,18 +163,14 @@ class Main(
err.println("Error formatting $fileName: ${e.message}; skipping.")
throw e
} catch (e: ParseError) {
- handleParseError(fileName, e)
+ err.println("$fileName:${e.message}")
throw e
} catch (e: FormattingError) {
for (diagnostic in e.diagnostics()) {
- System.err.println("$fileName:$diagnostic")
+ err.println("$fileName:$diagnostic")
}
e.printStackTrace(err)
throw e
}
}
-
- private fun handleParseError(fileName: String, e: ParseError) {
- err.println("$fileName:${e.message}")
- }
}
diff --git a/core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt b/core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt
index 2c84972..e0009f7 100644
--- a/core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt
+++ b/core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt
@@ -20,6 +20,7 @@ import com.facebook.ktfmt.format.Formatter
import com.facebook.ktfmt.format.FormattingOptions
import java.io.File
import java.io.PrintStream
+import java.nio.charset.StandardCharsets.UTF_8
/** ParsedArgs holds the arguments passed to ktfmt on the command-line, after parsing. */
data class ParsedArgs(
@@ -40,7 +41,7 @@ data class ParsedArgs(
fun processArgs(err: PrintStream, args: Array<String>): ParsedArgs {
if (args.size == 1 && args[0].startsWith("@")) {
- return parseOptions(err, File(args[0].substring(1)).readLines().toTypedArray())
+ return parseOptions(err, File(args[0].substring(1)).readLines(UTF_8).toTypedArray())
} else {
return parseOptions(err, args)
}
diff --git a/core/src/main/java/com/facebook/ktfmt/format/EnumEntryList.kt b/core/src/main/java/com/facebook/ktfmt/format/EnumEntryList.kt
new file mode 100644
index 0000000..9d76c5f
--- /dev/null
+++ b/core/src/main/java/com/facebook/ktfmt/format/EnumEntryList.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.facebook.ktfmt.format
+
+import org.jetbrains.kotlin.com.intellij.psi.PsiElement
+import org.jetbrains.kotlin.psi.KtClass
+import org.jetbrains.kotlin.psi.KtClassBody
+import org.jetbrains.kotlin.psi.KtEnumEntry
+import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespaceAndComments
+
+/**
+ * PSI-like model of a list of enum entries.
+ *
+ * See https://youtrack.jetbrains.com/issue/KT-65157
+ */
+class EnumEntryList
+private constructor(
+ val enumEntries: List<KtEnumEntry>,
+ val trailingComma: PsiElement?,
+ val terminatingSemicolon: PsiElement?,
+) {
+ companion object {
+ fun extractParentList(enumEntry: KtEnumEntry): EnumEntryList {
+ return extractChildList(enumEntry.parent as KtClassBody)!!
+ }
+
+ fun extractChildList(classBody: KtClassBody): EnumEntryList? {
+ val clazz = classBody.parent
+ if (clazz !is KtClass || !clazz.isEnum()) return null
+
+ val enumEntries = classBody.children.filterIsInstance<KtEnumEntry>()
+
+ if (enumEntries.isEmpty()) {
+ var semicolon = classBody.firstChild
+ while (semicolon != null) {
+ if (semicolon.text == ";") break
+ semicolon = semicolon.nextSibling
+ }
+
+ return EnumEntryList(
+ enumEntries = enumEntries,
+ trailingComma = null,
+ terminatingSemicolon = semicolon,
+ )
+ }
+
+ var semicolon: PsiElement? = null
+ var comma: PsiElement? = null
+ val lastToken =
+ enumEntries
+ .last()
+ .lastChild
+ .getPrevSiblingIgnoringWhitespaceAndComments(withItself = true)!!
+ when (lastToken.text) {
+ "," -> {
+ comma = lastToken
+ }
+ ";" -> {
+ semicolon = lastToken
+ val prevSibling = semicolon.getPrevSiblingIgnoringWhitespaceAndComments()
+ if (prevSibling?.text == ",") {
+ comma = prevSibling
+ }
+ }
+ }
+
+ return EnumEntryList(
+ enumEntries = enumEntries,
+ trailingComma = comma,
+ terminatingSemicolon = semicolon,
+ )
+ }
+ }
+}
diff --git a/core/src/main/java/com/facebook/ktfmt/format/FenceCommentsOp.kt b/core/src/main/java/com/facebook/ktfmt/format/FenceCommentsOp.kt
index 721bd05..d425d31 100644
--- a/core/src/main/java/com/facebook/ktfmt/format/FenceCommentsOp.kt
+++ b/core/src/main/java/com/facebook/ktfmt/format/FenceCommentsOp.kt
@@ -29,9 +29,11 @@ import com.google.googlejavaformat.Op
* indentation.
*/
object FenceCommentsOp : Op {
- val AS_LIST = ImmutableList.of<Op>(FenceCommentsOp)
+ val AS_LIST: ImmutableList<Op> = ImmutableList.of(FenceCommentsOp)
override fun add(builder: DocBuilder) {
// Do nothing. This Op simply needs to be in the OpsBuilder.
}
+
+ override fun toString(): String = "FenceComments"
}
diff --git a/core/src/main/java/com/facebook/ktfmt/format/Formatter.kt b/core/src/main/java/com/facebook/ktfmt/format/Formatter.kt
index 4cdb589..c02f51a 100644
--- a/core/src/main/java/com/facebook/ktfmt/format/Formatter.kt
+++ b/core/src/main/java/com/facebook/ktfmt/format/Formatter.kt
@@ -19,7 +19,8 @@ package com.facebook.ktfmt.format
import com.facebook.ktfmt.debughelpers.printOps
import com.facebook.ktfmt.format.FormattingOptions.Style.DROPBOX
import com.facebook.ktfmt.format.FormattingOptions.Style.GOOGLE
-import com.facebook.ktfmt.format.RedundantElementRemover.dropRedundantElements
+import com.facebook.ktfmt.format.RedundantElementManager.addRedundantElements
+import com.facebook.ktfmt.format.RedundantElementManager.dropRedundantElements
import com.facebook.ktfmt.format.WhitespaceTombstones.indexOfWhitespaceTombstone
import com.facebook.ktfmt.kdoc.Escaping
import com.facebook.ktfmt.kdoc.KDocCommentsHelper
@@ -32,7 +33,7 @@ import com.google.googlejavaformat.OpsBuilder
import com.google.googlejavaformat.java.FormatterException
import com.google.googlejavaformat.java.JavaOutput
import org.jetbrains.kotlin.com.intellij.openapi.util.text.StringUtil
-import org.jetbrains.kotlin.com.intellij.openapi.util.text.StringUtilRt
+import org.jetbrains.kotlin.com.intellij.openapi.util.text.StringUtilRt.convertLineSeparators
import org.jetbrains.kotlin.com.intellij.psi.PsiComment
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
import org.jetbrains.kotlin.com.intellij.psi.PsiElementVisitor
@@ -44,7 +45,9 @@ import org.jetbrains.kotlin.psi.psiUtil.startOffset
object Formatter {
@JvmField
- val GOOGLE_FORMAT = FormattingOptions(style = GOOGLE, blockIndent = 2, continuationIndent = 2)
+ val GOOGLE_FORMAT =
+ FormattingOptions(
+ style = GOOGLE, blockIndent = 2, continuationIndent = 2, manageTrailingCommas = true)
/** A format that attempts to reflect https://kotlinlang.org/docs/coding-conventions.html. */
@JvmField
@@ -86,12 +89,14 @@ object Formatter {
}
checkEscapeSequences(kotlinCode)
- val lfCode = StringUtilRt.convertLineSeparators(kotlinCode)
- val sortedImports = sortedAndDistinctImports(lfCode)
- val noRedundantElements = dropRedundantElements(sortedImports, options)
- val prettyCode =
- prettyPrint(noRedundantElements, options, Newlines.guessLineSeparator(kotlinCode)!!)
- return if (shebang.isNotEmpty()) shebang + "\n" + prettyCode else prettyCode
+ return kotlinCode
+ .let { convertLineSeparators(it) }
+ .let { sortedAndDistinctImports(it) }
+ .let { dropRedundantElements(it, options) }
+ .let { prettyPrint(it, options, "\n") }
+ .let { addRedundantElements(it, options) }
+ .let { convertLineSeparators(it, Newlines.guessLineSeparator(kotlinCode)!!) }
+ .let { if (shebang.isEmpty()) it else shebang + "\n" + it }
}
/** prettyPrint reflows 'code' using google-java-format's engine. */
diff --git a/core/src/main/java/com/facebook/ktfmt/format/FormattingOptions.kt b/core/src/main/java/com/facebook/ktfmt/format/FormattingOptions.kt
index 791cde3..fb82682 100644
--- a/core/src/main/java/com/facebook/ktfmt/format/FormattingOptions.kt
+++ b/core/src/main/java/com/facebook/ktfmt/format/FormattingOptions.kt
@@ -53,7 +53,16 @@ data class FormattingOptions(
* Print the Ops generated by KotlinInputAstVisitor to help reason about formatting (i.e.,
* newline) decisions
*/
- val debuggingPrintOpsAfterFormatting: Boolean = false
+ val debuggingPrintOpsAfterFormatting: Boolean = false,
+
+ /**
+ * Automatically remove and insert trialing commas.
+ *
+ * Lists that cannot fit on one line will have trailing commas inserted. Lists that span
+ * multiple lines will have them removed. Manually inserted trailing commas cannot be used as a
+ * hint to force breaking lists to multiple lines.
+ */
+ val manageTrailingCommas: Boolean = false,
) {
companion object {
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 433ae1a..21d93e8 100644
--- a/core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt
+++ b/core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt
@@ -23,7 +23,6 @@ import com.google.googlejavaformat.FormattingError
import com.google.googlejavaformat.Indent
import com.google.googlejavaformat.Indent.Const.ZERO
import com.google.googlejavaformat.OpsBuilder
-import com.google.googlejavaformat.Output
import com.google.googlejavaformat.Output.BreakTag
import java.util.ArrayDeque
import java.util.Optional
@@ -45,6 +44,7 @@ import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtCallableReferenceExpression
import org.jetbrains.kotlin.psi.KtCatchClause
import org.jetbrains.kotlin.psi.KtClass
+import org.jetbrains.kotlin.psi.KtClassBody
import org.jetbrains.kotlin.psi.KtClassInitializer
import org.jetbrains.kotlin.psi.KtClassLiteralExpression
import org.jetbrains.kotlin.psi.KtClassOrObject
@@ -52,6 +52,7 @@ import org.jetbrains.kotlin.psi.KtCollectionLiteralExpression
import org.jetbrains.kotlin.psi.KtConstantExpression
import org.jetbrains.kotlin.psi.KtConstructorDelegationCall
import org.jetbrains.kotlin.psi.KtContainerNode
+import org.jetbrains.kotlin.psi.KtContextReceiverList
import org.jetbrains.kotlin.psi.KtContinueExpression
import org.jetbrains.kotlin.psi.KtDelegatedSuperTypeEntry
import org.jetbrains.kotlin.psi.KtDestructuringDeclaration
@@ -94,6 +95,7 @@ import org.jetbrains.kotlin.psi.KtQualifiedExpression
import org.jetbrains.kotlin.psi.KtReferenceExpression
import org.jetbrains.kotlin.psi.KtReturnExpression
import org.jetbrains.kotlin.psi.KtScript
+import org.jetbrains.kotlin.psi.KtScriptInitializer
import org.jetbrains.kotlin.psi.KtSecondaryConstructor
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
@@ -124,6 +126,8 @@ import org.jetbrains.kotlin.psi.psiUtil.children
import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespace
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.psi.psiUtil.startsWithComment
+import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes
+import org.jetbrains.kotlin.psi.stubs.impl.KotlinPlaceHolderStubImpl
/** An AST visitor that builds a stream of {@link Op}s to format. */
class KotlinInputAstVisitor(
@@ -162,18 +166,18 @@ class KotlinInputAstVisitor(
builder.sync(function)
builder.block(ZERO) {
visitFunctionLikeExpression(
- function.modifierList,
- "fun",
- function.typeParameterList,
- function.receiverTypeReference,
- function.nameIdentifier?.text,
- true,
- function.valueParameterList,
- function.typeConstraintList,
- function.bodyBlockExpression,
- function.bodyExpression,
- function.typeReference,
- function.bodyBlockExpression?.lBrace != null)
+ contextReceiverList =
+ function.getStubOrPsiChild(KtStubElementTypes.CONTEXT_RECEIVER_LIST),
+ modifierList = function.modifierList,
+ keyword = "fun",
+ typeParameters = function.typeParameterList,
+ receiverTypeReference = function.receiverTypeReference,
+ name = function.nameIdentifier?.text,
+ parameterList = function.valueParameterList,
+ typeConstraintList = function.typeConstraintList,
+ bodyExpression = function.bodyBlockExpression ?: function.bodyExpression,
+ typeOrDelegationCall = function.typeReference,
+ )
}
}
@@ -205,15 +209,17 @@ class KotlinInputAstVisitor(
/** Example: `String?` or `((Int) -> Unit)?` */
override fun visitNullableType(nullableType: KtNullableType) {
builder.sync(nullableType)
+
+ // Normally we wouldn't loop over children, but there can be multiple layers of parens.
+ val modifierList = nullableType.modifierList
val innerType = nullableType.innerType
- val addParenthesis = innerType is KtFunctionType
- if (addParenthesis) {
- builder.token("(")
- }
- visit(nullableType.modifierList)
- visit(innerType)
- if (addParenthesis) {
- builder.token(")")
+ for (child in nullableType.node.children()) {
+ when {
+ child.psi == modifierList -> visit(modifierList)
+ child.psi == innerType -> visit(innerType)
+ child.elementType == KtTokens.LPAR -> builder.token("(")
+ child.elementType == KtTokens.RPAR -> builder.token(")")
+ }
}
builder.token("?")
}
@@ -251,6 +257,7 @@ class KotlinInputAstVisitor(
visitEachCommaSeparated(
typeArgumentList.arguments,
typeArgumentList.trailingComma != null,
+ wrapInBlock = !isGoogleStyle,
prefix = "<",
postfix = ">",
)
@@ -281,24 +288,40 @@ class KotlinInputAstVisitor(
* list of supertypes.
*/
private fun visitFunctionLikeExpression(
+ contextReceiverList: KtContextReceiverList?,
modifierList: KtModifierList?,
- keyword: String,
+ keyword: String?,
typeParameters: KtTypeParameterList?,
receiverTypeReference: KtTypeReference?,
name: String?,
- emitParenthesis: Boolean,
parameterList: KtParameterList?,
typeConstraintList: KtTypeConstraintList?,
- bodyBlockExpression: KtBlockExpression?,
- nonBlockBodyExpressions: KtExpression?,
+ bodyExpression: KtExpression?,
typeOrDelegationCall: KtElement?,
- emitBraces: Boolean
) {
- builder.block(ZERO) {
+ fun emitTypeOrDelegationCall(block: () -> Unit) {
+ if (typeOrDelegationCall != null) {
+ builder.block(ZERO) {
+ if (typeOrDelegationCall is KtConstructorDelegationCall) {
+ builder.space()
+ }
+ builder.token(":")
+ block()
+ }
+ }
+ }
+
+ val forceTrailingBreak = name != null
+ builder.block(ZERO, isEnabled = forceTrailingBreak) {
+ if (contextReceiverList != null) {
+ visitContextReceiverList(contextReceiverList)
+ }
if (modifierList != null) {
visitModifierList(modifierList)
}
- builder.token(keyword)
+ if (keyword != null) {
+ builder.token(keyword)
+ }
if (typeParameters != null) {
builder.space()
builder.block(ZERO) { visit(typeParameters) }
@@ -317,95 +340,83 @@ class KotlinInputAstVisitor(
builder.token(name)
}
}
- if (emitParenthesis) {
- builder.token("(")
- }
- var paramBlockNeedsClosing = false
- builder.block(ZERO) {
- if (parameterList != null && parameterList.parameters.isNotEmpty()) {
- paramBlockNeedsClosing = true
- builder.open(expressionBreakIndent)
- builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakIndent)
- visit(parameterList)
- }
- if (emitParenthesis) {
- if (parameterList != null && parameterList.parameters.isNotEmpty()) {
- builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakNegativeIndent)
- }
+
+ if (parameterList != null && parameterList.hasEmptyParens()) {
+ builder.block(ZERO) {
+ builder.token("(")
builder.token(")")
- } else {
- if (paramBlockNeedsClosing) {
- builder.close()
+ emitTypeOrDelegationCall {
+ builder.breakOp(Doc.FillMode.INDEPENDENT, " ", expressionBreakIndent)
+ builder.block(expressionBreakIndent) { visit(typeOrDelegationCall) }
}
}
- if (typeOrDelegationCall != null) {
- builder.block(ZERO) {
- if (typeOrDelegationCall is KtConstructorDelegationCall) {
- builder.space()
- }
- builder.token(":")
- if (parameterList?.parameters.isNullOrEmpty()) {
- builder.breakOp(Doc.FillMode.INDEPENDENT, " ", expressionBreakIndent)
- builder.block(expressionBreakIndent) { visit(typeOrDelegationCall) }
- } else {
- builder.space()
- builder.block(expressionBreakNegativeIndent) { visit(typeOrDelegationCall) }
- }
+ } else {
+ builder.block(expressionBreakIndent) {
+ if (parameterList != null) {
+ visitEachCommaSeparated(
+ list = parameterList.parameters,
+ hasTrailingComma = parameterList.trailingComma != null,
+ prefix = "(",
+ postfix = ")",
+ wrapInBlock = false,
+ breakBeforePostfix = true,
+ )
+ }
+ emitTypeOrDelegationCall {
+ builder.space()
+ builder.block(expressionBreakNegativeIndent) { visit(typeOrDelegationCall) }
}
}
}
- if (paramBlockNeedsClosing) {
- builder.close()
- }
+
if (typeConstraintList != null) {
builder.space()
visit(typeConstraintList)
}
- if (bodyBlockExpression != null) {
+ if (bodyExpression is KtBlockExpression) {
builder.space()
- visitBlockBody(bodyBlockExpression, emitBraces)
- } else if (nonBlockBodyExpressions != null) {
+ visit(bodyExpression)
+ } else if (bodyExpression != null) {
builder.space()
builder.block(ZERO) {
builder.token("=")
- if (isLambdaOrScopingFunction(nonBlockBodyExpressions)) {
- visitLambdaOrScopingFunction(nonBlockBodyExpressions)
+ if (isLambdaOrScopingFunction(bodyExpression)) {
+ visitLambdaOrScopingFunction(bodyExpression)
} else {
builder.block(expressionBreakIndent) {
builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO)
- builder.block(ZERO) { visit(nonBlockBodyExpressions) }
+ builder.block(ZERO) { visit(bodyExpression) }
}
}
}
}
builder.guessToken(";")
}
- if (name != null) {
+ if (forceTrailingBreak) {
builder.forcedBreak()
}
}
- private fun genSym(): Output.BreakTag {
- return Output.BreakTag()
+ private fun genSym(): BreakTag {
+ return BreakTag()
}
- private fun visitBlockBody(bodyBlockExpression: PsiElement, emitBraces: Boolean) {
- if (emitBraces) {
- builder.token("{", Doc.Token.RealOrImaginary.REAL, blockIndent, Optional.of(blockIndent))
- }
+ private fun emitBracedBlock(
+ bodyBlockExpression: PsiElement,
+ emitChildren: (Array<PsiElement>) -> Unit,
+ ) {
+ builder.token("{", Doc.Token.RealOrImaginary.REAL, blockIndent, Optional.of(blockIndent))
val statements = bodyBlockExpression.children
if (statements.isNotEmpty()) {
builder.block(blockIndent) {
builder.forcedBreak()
builder.blankLineWanted(OpsBuilder.BlankLineWanted.PRESERVE)
- visitStatements(statements)
+ emitChildren(statements)
}
builder.forcedBreak()
builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO)
}
- if (emitBraces) {
- builder.token("}", blockIndent)
- }
+ builder.token("}", blockIndent)
}
private fun visitStatement(statement: PsiElement) {
@@ -468,12 +479,9 @@ class KotlinInputAstVisitor(
}
}
receiver is KtStringTemplateExpression -> {
- val isMultiline = receiver.text.contains('\n')
- builder.block(if (isMultiline) expressionBreakIndent else ZERO) {
+ builder.block(expressionBreakIndent) {
visit(receiver)
- if (isMultiline) {
- builder.forcedBreak()
- }
+ builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO)
builder.token(expression.operationSign.value)
visit(expression.selectorExpression)
}
@@ -717,20 +725,6 @@ class KotlinInputAstVisitor(
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
- }
-
- /**
- * 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
- }
-
override fun visitCallExpression(callExpression: KtCallExpression) {
builder.sync(callExpression)
with(callExpression) {
@@ -776,13 +770,17 @@ class KotlinInputAstVisitor(
}
}
}
- if (lambdaArguments.isNotEmpty()) {
- builder.space()
- visitArgumentInternal(
- lambdaArguments.single(),
- wrapInBlock = false,
- brokeBeforeBrace = brokeBeforeBrace,
- )
+ when (lambdaArguments.size) {
+ 0 -> {}
+ 1 -> {
+ builder.space()
+ visitArgumentInternal(
+ lambdaArguments.single(),
+ wrapInBlock = false,
+ brokeBeforeBrace = brokeBeforeBrace,
+ )
+ }
+ else -> throw ParseError("Maximum one trailing lambda is allowed", lambdaArguments[1])
}
}
}
@@ -807,26 +805,26 @@ class KotlinInputAstVisitor(
arguments.first().getArgumentExpression() is KtLambdaExpression &&
arguments.first().getArgumentName() == null
val hasTrailingComma = list.trailingComma != null
+ val hasEmptyParens = list.hasEmptyParens()
val wrapInBlock: Boolean
val breakBeforePostfix: Boolean
val leadingBreak: Boolean
val breakAfterPrefix: Boolean
-
if (isSingleUnnamedLambda) {
wrapInBlock = true
breakBeforePostfix = false
- leadingBreak = arguments.isNotEmpty() && hasTrailingComma
+ leadingBreak = !hasEmptyParens && hasTrailingComma
breakAfterPrefix = false
} else {
wrapInBlock = !isGoogleStyle
- breakBeforePostfix = isGoogleStyle && arguments.isNotEmpty()
- leadingBreak = arguments.isNotEmpty()
- breakAfterPrefix = arguments.isNotEmpty()
+ breakBeforePostfix = isGoogleStyle && !hasEmptyParens
+ leadingBreak = !hasEmptyParens
+ breakAfterPrefix = !hasEmptyParens
}
return visitEachCommaSeparated(
- list.arguments,
+ arguments,
hasTrailingComma,
wrapInBlock = wrapInBlock,
breakBeforePostfix = breakBeforePostfix,
@@ -865,8 +863,10 @@ class KotlinInputAstVisitor(
val valueParams = lambdaExpression.valueParameters
val hasParams = valueParams.isNotEmpty()
- val statements = (lambdaExpression.bodyExpression ?: fail()).children
- val hasStatements = statements.isNotEmpty()
+ val bodyExpression = lambdaExpression.bodyExpression ?: fail()
+ val expressionStatements = bodyExpression.children
+ val hasStatements = expressionStatements.isNotEmpty()
+ val hasComments = bodyExpression.children().any { it is PsiComment }
val hasArrow = lambdaExpression.functionLiteral.arrow != null
fun ifBrokeBeforeBrace(onTrue: Indent, onFalse: Indent): Indent {
@@ -904,26 +904,30 @@ class KotlinInputAstVisitor(
}
builder.token("->")
}
- builder.breakOp(Doc.FillMode.UNIFIED, "", bracePlusZeroIndent)
+ }
+
+ if (hasParams || hasArrow || hasStatements || hasComments) {
+ builder.breakOp(Doc.FillMode.UNIFIED, " ", bracePlusZeroIndent)
}
if (hasStatements) {
- builder.breakOp(Doc.FillMode.UNIFIED, " ", bracePlusBlockIndent)
+ builder.breakOp(Doc.FillMode.UNIFIED, "", bracePlusBlockIndent)
builder.block(bracePlusBlockIndent) {
builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO)
- if (statements.size == 1 &&
- statements.first() !is KtReturnExpression &&
- lambdaExpression.bodyExpression?.startsWithComment() != true) {
- visitStatement(statements[0])
+ if (expressionStatements.size == 1 &&
+ expressionStatements.first() !is KtReturnExpression &&
+ !bodyExpression.startsWithComment()) {
+ visitStatement(expressionStatements[0])
} else {
- visitStatements(statements)
+ visitStatements(expressionStatements)
}
+ builder.breakOp(Doc.FillMode.UNIFIED, " ", bracePlusZeroIndent)
}
}
if (hasParams || hasArrow || hasStatements) {
// If we had to break in the body, ensure there is a break before the closing brace
- builder.breakOp(Doc.FillMode.UNIFIED, " ", bracePlusZeroIndent)
+ builder.breakOp(Doc.FillMode.UNIFIED, "", bracePlusZeroIndent)
}
builder.block(bracePlusZeroIndent) {
builder.fenceComments()
@@ -1203,20 +1207,32 @@ class KotlinInputAstVisitor(
val leftMostExpression = parts.first()
visit(leftMostExpression.left)
for (leftExpression in parts) {
- when (leftExpression.operationToken) {
- KtTokens.RANGE -> {}
- KtTokens.ELVIS -> builder.breakOp(Doc.FillMode.INDEPENDENT, " ", expressionBreakIndent)
- else -> builder.space()
- }
- builder.token(leftExpression.operationReference.text)
val isFirst = leftExpression === leftMostExpression
- if (isFirst) {
- builder.open(expressionBreakIndent)
- }
+
when (leftExpression.operationToken) {
- KtTokens.RANGE -> {}
- KtTokens.ELVIS -> builder.space()
- else -> builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO)
+ KtTokens.RANGE,
+ KtTokens.RANGE_UNTIL -> {
+ if (isFirst) {
+ builder.open(expressionBreakIndent)
+ }
+ builder.token(leftExpression.operationReference.text)
+ }
+ KtTokens.ELVIS -> {
+ if (isFirst) {
+ builder.open(expressionBreakIndent)
+ }
+ builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO)
+ builder.token(leftExpression.operationReference.text)
+ builder.space()
+ }
+ else -> {
+ builder.space()
+ if (isFirst) {
+ builder.open(expressionBreakIndent)
+ }
+ builder.token(leftExpression.operationReference.text)
+ builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO)
+ }
}
visit(leftExpression.right)
}
@@ -1376,18 +1392,17 @@ class KotlinInputAstVisitor(
builder.block(ZERO) {
visitFunctionLikeExpression(
- accessor.modifierList,
- accessor.namePlaceholder.text,
- null,
- null,
- null,
- accessor.bodyExpression != null || accessor.bodyBlockExpression != null,
- accessor.parameterList,
- null,
- accessor.bodyBlockExpression,
- accessor.bodyExpression,
- accessor.returnTypeReference,
- accessor.bodyBlockExpression?.lBrace != null)
+ contextReceiverList = null,
+ modifierList = accessor.modifierList,
+ keyword = accessor.namePlaceholder.text,
+ typeParameters = null,
+ receiverTypeReference = null,
+ name = null,
+ parameterList = getParameterListWithBugFixes(accessor),
+ typeConstraintList = null,
+ bodyExpression = accessor.bodyBlockExpression ?: accessor.bodyExpression,
+ typeOrDelegationCall = accessor.returnTypeReference,
+ )
}
}
}
@@ -1402,6 +1417,33 @@ class KotlinInputAstVisitor(
return 0
}
+ // Bug in Kotlin 1.9.10: KtProperyAccessor is the direct parent of the left and right paren
+ // elements. Also parameterList is always null for getters. As a workaround, we create our own
+ // fake KtParameterList.
+ private fun getParameterListWithBugFixes(accessor: KtPropertyAccessor): KtParameterList? {
+ if (accessor.bodyExpression == null && accessor.bodyBlockExpression == null) return null
+
+ return object :
+ KtParameterList(
+ KotlinPlaceHolderStubImpl(accessor.stub, KtStubElementTypes.VALUE_PARAMETER_LIST)) {
+ override fun getParameters(): List<KtParameter> {
+ return accessor.valueParameters
+ }
+
+ override fun getTrailingComma(): PsiElement? {
+ return accessor.parameterList?.trailingComma
+ }
+
+ override fun getLeftParenthesis(): PsiElement? {
+ return accessor.leftParenthesis
+ }
+
+ override fun getRightParenthesis(): PsiElement? {
+ return accessor.rightParenthesis
+ }
+ }
+ }
+
/**
* Returns whether an expression is a lambda or initializer expression in which case we will want
* to avoid indenting the lambda block
@@ -1420,16 +1462,24 @@ class KotlinInputAstVisitor(
if (expression.getPrevSiblingIgnoringWhitespace() is PsiComment) {
return false // Leading comments cause weird indentation.
}
- if (expression is KtLambdaExpression) {
- return true
+
+ var carry = expression
+ if (carry is KtCallExpression) {
+ if (carry.valueArgumentList?.leftParenthesis == null &&
+ carry.lambdaArguments.isNotEmpty() &&
+ carry.typeArgumentList?.arguments.isNullOrEmpty()) {
+ carry = carry.lambdaArguments[0].getArgumentExpression()
+ } else {
+ return false
+ }
+ }
+ if (carry is KtLabeledExpression) {
+ carry = carry.baseExpression
}
- if (expression is KtCallExpression &&
- expression.valueArgumentList?.leftParenthesis == null &&
- expression.lambdaArguments.isNotEmpty() &&
- expression.typeArgumentList?.arguments.isNullOrEmpty() &&
- expression.lambdaArguments.first().getArgumentExpression() is KtLambdaExpression) {
+ if (carry is KtLambdaExpression) {
return true
}
+
return false
}
@@ -1438,24 +1488,33 @@ class KotlinInputAstVisitor(
val breakToExpr = genSym()
builder.breakOp(Doc.FillMode.INDEPENDENT, " ", expressionBreakIndent, Optional.of(breakToExpr))
- val lambdaExpression =
- when (expr) {
- is KtLambdaExpression -> expr
- is KtCallExpression -> {
- visit(expr.calleeExpression)
- builder.space()
- expr.lambdaArguments[0].getLambdaExpression() ?: fail()
- }
- else -> throw AssertionError(expr)
- }
+ var carry = expr
+ if (carry is KtCallExpression) {
+ visit(carry.calleeExpression)
+ builder.space()
+ carry = carry.lambdaArguments[0].getArgumentExpression()
+ }
+ if (carry is KtLabeledExpression) {
+ visit(carry.labelQualifier)
+ carry = carry.baseExpression ?: fail()
+ }
+ if (carry is KtLambdaExpression) {
+ visitLambdaExpressionInternal(carry, brokeBeforeBrace = breakToExpr)
+ return
+ }
- visitLambdaExpressionInternal(lambdaExpression, brokeBeforeBrace = breakToExpr)
+ throw AssertionError(carry)
}
override fun visitClassOrObject(classOrObject: KtClassOrObject) {
builder.sync(classOrObject)
+ val contextReceiverList =
+ classOrObject.getStubOrPsiChild(KtStubElementTypes.CONTEXT_RECEIVER_LIST)
val modifierList = classOrObject.modifierList
builder.block(ZERO) {
+ if (contextReceiverList != null) {
+ visitContextReceiverList(contextReceiverList)
+ }
if (modifierList != null) {
visitModifierList(modifierList)
}
@@ -1488,104 +1547,53 @@ class KotlinInputAstVisitor(
visit(typeConstraintList)
builder.space()
}
- val body = classOrObject.body
- if (classOrObject.hasModifier(KtTokens.ENUM_KEYWORD)) {
- visitEnumBody(classOrObject as KtClass)
- } else if (body != null) {
- visitBlockBody(body, true)
- }
+ visit(classOrObject.body)
}
if (classOrObject.nameIdentifier != null) {
builder.forcedBreak()
}
}
- /** Example `{ RED, GREEN; fun foo() { ... } }` for an enum class */
- private fun visitEnumBody(enumClass: KtClass) {
- val body = enumClass.body
- if (body == null) {
- return
- }
- builder.token("{", Doc.Token.RealOrImaginary.REAL, blockIndent, Optional.of(blockIndent))
- builder.open(ZERO)
- builder.block(blockIndent) {
- builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO)
- val (enumEntries, nonEnumEntryStatements) = body.children.partition { it is KtEnumEntry }
- builder.forcedBreak()
- visitEnumEntries(enumEntries)
-
- if (nonEnumEntryStatements.isNotEmpty()) {
- builder.forcedBreak()
- builder.blankLineWanted(OpsBuilder.BlankLineWanted.PRESERVE)
- visitStatements(nonEnumEntryStatements.toTypedArray())
- }
- }
- builder.forcedBreak()
- builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO)
- builder.token("}", blockIndent)
- builder.close()
- }
-
- /** Example `RED, GREEN, BLUE,` in an enum class, or `RED, GREEN;` */
- private fun visitEnumEntries(enumEntries: List<PsiElement>) {
- builder.block(ZERO) {
- builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO)
- for (value in enumEntries) {
- visit(value)
- if (builder.peekToken() == Optional.of(",")) {
- builder.token(",")
- builder.forcedBreak()
- }
- }
- }
- builder.guessToken(";")
- }
-
override fun visitPrimaryConstructor(constructor: KtPrimaryConstructor) {
builder.sync(constructor)
builder.block(ZERO) {
if (constructor.hasConstructorKeyword()) {
- builder.open(ZERO)
builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO)
- visit(constructor.modifierList)
- builder.token("constructor")
- }
-
- builder.block(ZERO) {
- builder.token("(")
- builder.block(expressionBreakIndent) {
- builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakIndent)
- visit(constructor.valueParameterList)
- builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakNegativeIndent)
- if (constructor.hasConstructorKeyword()) {
- builder.close()
- }
- }
- builder.token(")")
}
+ visitFunctionLikeExpression(
+ contextReceiverList = null,
+ modifierList = constructor.modifierList,
+ keyword = if (constructor.hasConstructorKeyword()) "constructor" else null,
+ typeParameters = null,
+ receiverTypeReference = null,
+ name = null,
+ parameterList = constructor.valueParameterList,
+ typeConstraintList = null,
+ bodyExpression = constructor.bodyExpression,
+ typeOrDelegationCall = null,
+ )
}
}
/** Example `private constructor(n: Int) : this(4, 5) { ... }` inside a class's body */
override fun visitSecondaryConstructor(constructor: KtSecondaryConstructor) {
- val delegationCall = constructor.getDelegationCall()
- val bodyExpression = constructor.bodyExpression
-
builder.sync(constructor)
-
- visitFunctionLikeExpression(
- constructor.modifierList,
- "constructor",
- null,
- null,
- null,
- true,
- constructor.valueParameterList,
- null,
- bodyExpression,
- null,
- if (!delegationCall.isImplicit) delegationCall else null,
- true)
+ builder.block(ZERO) {
+ val delegationCall = constructor.getDelegationCall()
+ visitFunctionLikeExpression(
+ contextReceiverList =
+ constructor.getStubOrPsiChild(KtStubElementTypes.CONTEXT_RECEIVER_LIST),
+ modifierList = constructor.modifierList,
+ keyword = "constructor",
+ typeParameters = null,
+ receiverTypeReference = null,
+ name = null,
+ parameterList = constructor.valueParameterList,
+ typeConstraintList = null,
+ bodyExpression = constructor.bodyExpression,
+ typeOrDelegationCall = if (!delegationCall.isImplicit) delegationCall else null,
+ )
+ }
}
override fun visitConstructorDelegationCall(call: KtConstructorDelegationCall) {
@@ -1680,6 +1688,20 @@ class KotlinInputAstVisitor(
builder.forcedBreak()
}
+ /** Example `context(Logger, Raise<Error>)` */
+ override fun visitContextReceiverList(contextReceiverList: KtContextReceiverList) {
+ builder.sync(contextReceiverList)
+ builder.token("context")
+ visitEachCommaSeparated(
+ contextReceiverList.contextReceivers(),
+ prefix = "(",
+ postfix = ")",
+ breakAfterPrefix = false,
+ breakBeforePostfix = false,
+ )
+ builder.forcedBreak()
+ }
+
/** For example `@Magic private final` */
override fun visitModifierList(list: KtModifierList) {
builder.sync(list)
@@ -1897,9 +1919,61 @@ class KotlinInputAstVisitor(
}
}
+ override fun visitClassBody(body: KtClassBody) {
+ builder.sync(body)
+ emitBracedBlock(body) { children ->
+ val enumEntryList = EnumEntryList.extractChildList(body)
+ val members = children.filter { it !is KtEnumEntry }
+
+ if (enumEntryList != null) {
+ builder.block(ZERO) {
+ builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO)
+ for (value in enumEntryList.enumEntries) {
+ visit(value)
+ if (builder.peekToken() == Optional.of(",")) {
+ builder.token(",")
+ builder.forcedBreak()
+ }
+ }
+ }
+ builder.guessToken(";")
+
+ if (members.isNotEmpty()) {
+ builder.forcedBreak()
+ builder.blankLineWanted(OpsBuilder.BlankLineWanted.YES)
+ }
+ } else {
+ val parent = body.parent
+ if (parent is KtClass && parent.isEnum() && children.isNotEmpty()) {
+ builder.token(";")
+ builder.forcedBreak()
+ }
+ }
+
+ var prev: PsiElement? = null
+ for (curr in members) {
+ val blankLineBetweenMembers =
+ when {
+ prev == null -> OpsBuilder.BlankLineWanted.PRESERVE
+ prev !is KtProperty -> OpsBuilder.BlankLineWanted.YES
+ prev.getter != null || prev.setter != null -> OpsBuilder.BlankLineWanted.YES
+ curr is KtProperty -> OpsBuilder.BlankLineWanted.PRESERVE
+ else -> OpsBuilder.BlankLineWanted.YES
+ }
+ builder.blankLineWanted(blankLineBetweenMembers)
+
+ builder.block(ZERO) { visit(curr) }
+ builder.guessToken(";")
+ builder.forcedBreak()
+
+ prev = curr
+ }
+ }
+ }
+
override fun visitBlockExpression(expression: KtBlockExpression) {
builder.sync(expression)
- visitBlockBody(expression, true)
+ emitBracedBlock(expression) { children -> visitStatements(children) }
}
override fun visitWhenConditionWithExpression(condition: KtWhenConditionWithExpression) {
@@ -2054,17 +2128,14 @@ class KotlinInputAstVisitor(
/** Example `<T, S>` */
override fun visitTypeParameterList(list: KtTypeParameterList) {
builder.sync(list)
- builder.block(ZERO) {
- builder.token("<")
- val parameters = list.parameters
- if (parameters.isNotEmpty()) {
- // Break before args.
- builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakIndent)
- builder.block(expressionBreakIndent) {
- visitEachCommaSeparated(list.parameters, list.trailingComma != null, wrapInBlock = true)
- }
- }
- builder.token(">")
+ builder.block(expressionBreakIndent) {
+ visitEachCommaSeparated(
+ list = list.parameters,
+ hasTrailingComma = list.trailingComma != null,
+ prefix = "<",
+ postfix = ">",
+ wrapInBlock = !isGoogleStyle,
+ )
}
}
@@ -2292,7 +2363,7 @@ class KotlinInputAstVisitor(
expression.trailingComma != null,
prefix = "[",
postfix = "]",
- wrapInBlock = true)
+ wrapInBlock = !isGoogleStyle)
}
}
@@ -2347,10 +2418,9 @@ class KotlinInputAstVisitor(
visit(enumEntry.modifierList)
builder.token(enumEntry.nameIdentifier?.text ?: fail())
enumEntry.initializerList?.initializers?.forEach { visit(it) }
- val body = enumEntry.body
- if (body != null) {
+ enumEntry.body?.let {
builder.space()
- visitBlockBody(body, true)
+ visit(it)
}
}
}
@@ -2384,7 +2454,7 @@ class KotlinInputAstVisitor(
* @throws FormattingError
*/
override fun visitElement(element: PsiElement) {
- inExpression.addLast(element is KtExpression || inExpression.peekLast())
+ inExpression.addLast(element is KtExpression || inExpression.last())
val previous = builder.depth()
try {
super.visitElement(element)
@@ -2400,16 +2470,22 @@ class KotlinInputAstVisitor(
override fun visitKtFile(file: KtFile) {
markForPartialFormat()
- var importListEmpty = false
+ val importListEmpty = file.importList?.text?.isBlank() ?: true
+
var isFirst = true
for (child in file.children) {
if (child.text.isBlank()) {
- importListEmpty = child is KtImportList
continue
}
- if (!isFirst && child !is PsiComment && (child !is KtScript || !importListEmpty)) {
- builder.blankLineWanted(OpsBuilder.BlankLineWanted.YES)
- }
+
+ builder.blankLineWanted(
+ when {
+ isFirst -> OpsBuilder.BlankLineWanted.NO
+ child is PsiComment -> continue
+ child is KtScript && importListEmpty -> OpsBuilder.BlankLineWanted.PRESERVE
+ else -> OpsBuilder.BlankLineWanted.YES
+ })
+
visit(child)
isFirst = false
}
@@ -2419,6 +2495,7 @@ class KotlinInputAstVisitor(
override fun visitScript(script: KtScript) {
markForPartialFormat()
var lastChildHadBlankLineBefore = false
+ var lastChildIsContextReceiver = false
var first = true
for (child in script.blockExpression.children) {
if (child.text.isBlank()) {
@@ -2428,6 +2505,8 @@ class KotlinInputAstVisitor(
val childGetsBlankLineBefore = child !is KtProperty
if (first) {
builder.blankLineWanted(OpsBuilder.BlankLineWanted.PRESERVE)
+ } else if (lastChildIsContextReceiver) {
+ builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO)
} else if (child !is PsiComment &&
(childGetsBlankLineBefore || lastChildHadBlankLineBefore)) {
builder.blankLineWanted(OpsBuilder.BlankLineWanted.YES)
@@ -2435,15 +2514,14 @@ class KotlinInputAstVisitor(
visit(child)
builder.guessToken(";")
lastChildHadBlankLineBefore = childGetsBlankLineBefore
+ lastChildIsContextReceiver =
+ child is KtScriptInitializer &&
+ child.firstChild?.firstChild?.firstChild?.text == "context"
first = false
}
markForPartialFormat()
}
- private fun inExpression(): Boolean {
- return inExpression.peekLast()
- }
-
/**
* markForPartialFormat is used to delineate the smallest areas of code that must be formatted
* together.
@@ -2452,7 +2530,7 @@ class KotlinInputAstVisitor(
* covered by an area marked by this method.
*/
private fun markForPartialFormat() {
- if (!inExpression()) {
+ if (!inExpression.last()) {
builder.markForPartialFormat()
}
}
@@ -2495,7 +2573,7 @@ class KotlinInputAstVisitor(
sync(psiElement.startOffset)
}
- /** Prevent susequent comments from being moved ahead of this point, into parent [Level]s. */
+ /** Prevent subsequent comments from being moved ahead of this point, into parent [Level]s. */
private fun OpsBuilder.fenceComments() {
addAll(FenceCommentsOp.AS_LIST)
}
diff --git a/core/src/main/java/com/facebook/ktfmt/format/ParseError.kt b/core/src/main/java/com/facebook/ktfmt/format/ParseError.kt
index 216dfb6..432c664 100644
--- a/core/src/main/java/com/facebook/ktfmt/format/ParseError.kt
+++ b/core/src/main/java/com/facebook/ktfmt/format/ParseError.kt
@@ -17,7 +17,24 @@
package com.facebook.ktfmt.format
import org.jetbrains.kotlin.com.intellij.openapi.util.text.LineColumn
+import org.jetbrains.kotlin.com.intellij.psi.PsiElement
class ParseError(val errorDescription: String, val lineColumn: LineColumn) :
IllegalArgumentException(
- "${lineColumn.line + 1}:${lineColumn.column + 1}: error: $errorDescription")
+ "${lineColumn.line + 1}:${lineColumn.column + 1}: error: $errorDescription") {
+
+ constructor(
+ errorDescription: String,
+ element: PsiElement,
+ ) : this(errorDescription, positionOf(element))
+
+ companion object {
+ private fun positionOf(element: PsiElement): LineColumn {
+ val doc = element.containingFile.viewProvider.document!!
+ val offset = element.textOffset
+ val lineZero = doc.getLineNumber(offset)
+ val colZero = offset - doc.getLineStartOffset(lineZero)
+ return LineColumn.of(lineZero, colZero)
+ }
+ }
+}
diff --git a/core/src/main/java/com/facebook/ktfmt/format/PsiUtils.kt b/core/src/main/java/com/facebook/ktfmt/format/PsiUtils.kt
new file mode 100644
index 0000000..3e33dab
--- /dev/null
+++ b/core/src/main/java/com/facebook/ktfmt/format/PsiUtils.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.facebook.ktfmt.format
+
+import org.jetbrains.kotlin.psi.KtCallExpression
+import org.jetbrains.kotlin.psi.KtExpression
+import org.jetbrains.kotlin.psi.KtParameterList
+import org.jetbrains.kotlin.psi.KtQualifiedExpression
+import org.jetbrains.kotlin.psi.KtValueArgumentList
+import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespace
+
+/** Returns true if the expression represents an invocation that is also a lambda */
+fun KtExpression.isLambda(): Boolean = this.callExpression?.lambdaArguments?.isNotEmpty() ?: false
+
+/** Does this list have parens with only whitespace between them? */
+fun KtParameterList.hasEmptyParens(): Boolean {
+ val left = this.leftParenthesis ?: return false
+ val right = this.rightParenthesis ?: return false
+ return left.getNextSiblingIgnoringWhitespace() == right
+}
+
+/** Does this list have parens with only whitespace between them? */
+fun KtValueArgumentList.hasEmptyParens(): Boolean {
+ val left = this.leftParenthesis ?: return false
+ val right = this.rightParenthesis ?: return false
+ return left.getNextSiblingIgnoringWhitespace() == right
+}
+
+/**
+ * [Formatter.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 val KtExpression.callExpression: KtCallExpression?
+ get() = ((this as? KtQualifiedExpression)?.selectorExpression ?: this) as? KtCallExpression
diff --git a/core/src/main/java/com/facebook/ktfmt/format/RedundantElementRemover.kt b/core/src/main/java/com/facebook/ktfmt/format/RedundantElementManager.kt
index 1c090fe..b6423f0 100644
--- a/core/src/main/java/com/facebook/ktfmt/format/RedundantElementRemover.kt
+++ b/core/src/main/java/com/facebook/ktfmt/format/RedundantElementManager.kt
@@ -19,6 +19,7 @@ package com.facebook.ktfmt.format
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace
import org.jetbrains.kotlin.kdoc.psi.impl.KDocImpl
+import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtImportList
import org.jetbrains.kotlin.psi.KtPackageDirective
import org.jetbrains.kotlin.psi.KtReferenceExpression
@@ -26,22 +27,31 @@ import org.jetbrains.kotlin.psi.KtTreeVisitorVoid
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.startOffset
-/** Removes elements that are not needed in the code, such as semicolons and unused imports. */
-object RedundantElementRemover {
+/**
+ * Adds and removes elements that are not strictly needed in the code, such as semicolons and unused
+ * imports.
+ */
+object RedundantElementManager {
/** Remove extra semicolons and unused imports, if enabled in the [options] */
fun dropRedundantElements(code: String, options: FormattingOptions): String {
val file = Parser.parse(code)
val redundantImportDetector = RedundantImportDetector(enabled = options.removeUnusedImports)
val redundantSemicolonDetector = RedundantSemicolonDetector()
+ val trailingCommaDetector = TrailingCommas.Detector()
file.accept(
object : KtTreeVisitorVoid() {
override fun visitElement(element: PsiElement) {
if (element is KDocImpl) {
redundantImportDetector.takeKdoc(element)
- } else {
- redundantSemicolonDetector.takeElement(element) { super.visitElement(element) }
+ return
+ }
+
+ redundantSemicolonDetector.takeElement(element)
+ if (options.manageTrailingCommas) {
+ trailingCommaDetector.takeElement(element)
}
+ super.visitElement(element)
}
override fun visitPackageDirective(directive: KtPackageDirective) {
@@ -63,17 +73,49 @@ object RedundantElementRemover {
val result = StringBuilder(code)
val elementsToRemove =
redundantSemicolonDetector.getRedundantSemicolonElements() +
- redundantImportDetector.getRedundantImportElements()
+ redundantImportDetector.getRedundantImportElements() +
+ trailingCommaDetector.getTrailingCommaElements()
for (element in elementsToRemove.sortedByDescending(PsiElement::endOffset)) {
// Don't insert extra newlines when the semicolon is already a line terminator
- val replacement = if (element.nextSibling.containsNewline()) "" else "\n"
+ val replacement =
+ if (element.text == ";" && !element.nextSibling.containsNewline()) {
+ "\n"
+ } else {
+ ""
+ }
result.replace(element.startOffset, element.endOffset, replacement)
}
return result.toString()
}
+ fun addRedundantElements(code: String, options: FormattingOptions): String {
+ if (!options.manageTrailingCommas) {
+ return code
+ }
+
+ val file = Parser.parse(code)
+ val trailingCommaSuggestor = TrailingCommas.Suggestor()
+
+ file.accept(
+ object : KtTreeVisitorVoid() {
+ override fun visitKtElement(element: KtElement) {
+ trailingCommaSuggestor.takeElement(element)
+ super.visitElement(element)
+ }
+ })
+
+ val result = StringBuilder(code)
+ val suggestionElements = trailingCommaSuggestor.getTrailingCommaSuggestions()
+
+ for (element in suggestionElements.sortedByDescending(PsiElement::endOffset)) {
+ result.insert(element.endOffset, ',')
+ }
+
+ return result.toString()
+ }
+
private fun PsiElement?.containsNewline(): Boolean {
if (this !is PsiWhiteSpace) return false
return this.text.contains('\n')
diff --git a/core/src/main/java/com/facebook/ktfmt/format/RedundantImportDetector.kt b/core/src/main/java/com/facebook/ktfmt/format/RedundantImportDetector.kt
index 7efa893..ba285f6 100644
--- a/core/src/main/java/com/facebook/ktfmt/format/RedundantImportDetector.kt
+++ b/core/src/main/java/com/facebook/ktfmt/format/RedundantImportDetector.kt
@@ -112,11 +112,10 @@ internal class RedundantImportDetector(val enabled: Boolean) {
importCleanUpCandidates =
importList.imports
.filter { import ->
+ val identifier = import.identifier ?: return@filter false
import.isValidImport &&
- !import.isAllUnder &&
- import.identifier != null &&
- requireNotNull(import.identifier) !in OPERATORS &&
- !COMPONENT_OPERATOR_REGEX.matches(import.identifier.orEmpty())
+ identifier !in OPERATORS &&
+ !COMPONENT_OPERATOR_REGEX.matches(identifier)
}
.toSet()
@@ -160,20 +159,20 @@ internal class RedundantImportDetector(val enabled: Boolean) {
fun getRedundantImportElements(): List<PsiElement> {
if (!enabled) return emptyList()
- val redundantImports = mutableListOf<PsiElement>()
+ val identifierCounts =
+ importCleanUpCandidates.groupBy { it.identifier }.mapValues { it.value.size }
- // Collect unused imports
- for (import in importCleanUpCandidates) {
- val isUnused = import.aliasName !in usedReferences && import.identifier !in usedReferences
- val isFromSamePackage = import.importedFqName?.parent() == thisPackage && import.alias == null
- if (isUnused || isFromSamePackage) {
- redundantImports += import
- }
+ return importCleanUpCandidates.filter {
+ val isUsed = it.identifier in usedReferences
+ val isFromThisPackage = it.importedFqName?.parent() == thisPackage
+ val hasAlias = it.alias != null
+ val isOverload = requireNotNull(identifierCounts[it.identifier]) > 1
+ // Remove if...
+ !isUsed || (isFromThisPackage && !hasAlias && !isOverload)
}
-
- return redundantImports
}
+ /** The imported short name, possibly an alias name, if any. */
private inline val KtImportDirective.identifier: String?
get() = importPath?.importedName?.identifier?.trim('`')
}
diff --git a/core/src/main/java/com/facebook/ktfmt/format/RedundantSemicolonDetector.kt b/core/src/main/java/com/facebook/ktfmt/format/RedundantSemicolonDetector.kt
index 3f6601b..d541a43 100644
--- a/core/src/main/java/com/facebook/ktfmt/format/RedundantSemicolonDetector.kt
+++ b/core/src/main/java/com/facebook/ktfmt/format/RedundantSemicolonDetector.kt
@@ -16,9 +16,12 @@
package com.facebook.ktfmt.format
+import org.jetbrains.kotlin.com.intellij.psi.PsiComment
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
+import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace
+import org.jetbrains.kotlin.psi.KtClass
+import org.jetbrains.kotlin.psi.KtClassBody
import org.jetbrains.kotlin.psi.KtContainerNodeForControlStructureBody
-import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtEnumEntry
import org.jetbrains.kotlin.psi.KtIfExpression
import org.jetbrains.kotlin.psi.KtLambdaExpression
@@ -35,15 +38,13 @@ internal class RedundantSemicolonDetector {
fun getRedundantSemicolonElements(): List<PsiElement> = extraSemicolons
- /** returns **true** if this element was an extra comma, **false** otherwise. */
- fun takeElement(element: PsiElement, superBlock: () -> Unit) {
+ fun takeElement(element: PsiElement) {
if (isExtraSemicolon(element)) {
extraSemicolons += element
- } else {
- superBlock.invoke()
}
}
+ /** returns **true** if this element was an extra comma, **false** otherwise. */
private fun isExtraSemicolon(element: PsiElement): Boolean {
if (element.text != ";") {
return false
@@ -53,9 +54,30 @@ internal class RedundantSemicolonDetector {
if (parent is KtStringTemplateExpression || parent is KtStringTemplateEntry) {
return false
}
- if (parent is KtEnumEntry &&
- parent.siblings(forward = true, withItself = false).any { it is KtDeclaration }) {
- return false
+
+ if (parent is KtEnumEntry) {
+ val classBody = parent.parent as KtClassBody
+ // Terminating semicolon with no other class members.
+ return classBody.children.last() == parent
+ }
+ if (parent is KtClassBody) {
+ val enumEntryList = EnumEntryList.extractChildList(parent) ?: return true
+ // Is not terminating semicolon or is terminating with no members.
+ return element != enumEntryList.terminatingSemicolon || parent.children.isEmpty()
+ }
+
+ if (parent is KtClassBody) {
+ val grandParent = parent.parent
+ if (grandParent is KtClass && grandParent.isEnum()) {
+ // Don't remove the first semicolon on non-empty enum.
+ if (element.getPrevSiblingIgnoringWhitespaceAndComments()?.text == "{" &&
+ element
+ .siblings(forward = true, withItself = false)
+ .filter { it !is PsiWhiteSpace && it !is PsiComment && it.text != ";" }
+ .firstOrNull()
+ ?.text != "}")
+ return false
+ }
}
val prevLeaf = element.prevLeaf(false)
diff --git a/core/src/main/java/com/facebook/ktfmt/format/Tokenizer.kt b/core/src/main/java/com/facebook/ktfmt/format/Tokenizer.kt
index ababba0..a373008 100644
--- a/core/src/main/java/com/facebook/ktfmt/format/Tokenizer.kt
+++ b/core/src/main/java/com/facebook/ktfmt/format/Tokenizer.kt
@@ -42,66 +42,80 @@ class Tokenizer(private val fileText: String, val file: KtFile) : KtTreeVisitorV
private val WHITESPACE_NEWLINE_REGEX: Pattern = Pattern.compile("\\R|( )+")
}
- val toks = mutableListOf<KotlinTok>()
- var index = 0
+ val toks: MutableList<KotlinTok> = mutableListOf()
+ var index: Int = 0
+ private set
override fun visitElement(element: PsiElement) {
val startIndex = element.startOffset
+ val endIndex = element.endOffset
+ val elementText = element.text
+ val originalText = fileText.substring(startIndex, endIndex)
when (element) {
is PsiComment -> {
toks.add(
KotlinTok(
- index,
- fileText.substring(startIndex, element.endOffset),
- element.text,
- startIndex,
- 0,
- false,
- KtTokens.EOF))
+ index = index,
+ originalText = originalText,
+ text = elementText,
+ position = startIndex,
+ column = 0,
+ isToken = false,
+ kind = KtTokens.EOF,
+ ),
+ )
index++
return
}
is KtStringTemplateExpression -> {
toks.add(
KotlinTok(
- index,
- WhitespaceTombstones.replaceTrailingWhitespaceWithTombstone(
- fileText.substring(startIndex, element.endOffset)),
- element.text,
- startIndex,
- 0,
- true,
- KtTokens.EOF))
+ index = index,
+ originalText =
+ WhitespaceTombstones.replaceTrailingWhitespaceWithTombstone(
+ originalText,
+ ),
+ text = elementText,
+ position = startIndex,
+ column = 0,
+ isToken = true,
+ kind = KtTokens.EOF,
+ ),
+ )
index++
return
}
is LeafPsiElement -> {
- val elementText = element.text
- val endIndex = element.endOffset
if (element is PsiWhiteSpace) {
val matcher = WHITESPACE_NEWLINE_REGEX.matcher(elementText)
while (matcher.find()) {
val text = matcher.group()
toks.add(
KotlinTok(
- -1,
- fileText.substring(startIndex + matcher.start(), startIndex + matcher.end()),
- text,
- startIndex + matcher.start(),
- 0,
- false,
- KtTokens.EOF))
+ index = -1,
+ originalText =
+ fileText.substring(
+ startIndex + matcher.start(), startIndex + matcher.end()),
+ text = text,
+ position = startIndex + matcher.start(),
+ column = 0,
+ isToken = false,
+ kind = KtTokens.EOF,
+ ),
+ )
}
} else {
toks.add(
KotlinTok(
- index,
- fileText.substring(startIndex, endIndex),
- elementText,
- startIndex,
- 0,
- true,
- KtTokens.EOF))
+ index = index,
+ originalText = originalText,
+ text = elementText,
+ position = startIndex,
+ column = 0,
+ isToken = true,
+ kind = KtTokens.EOF,
+ ),
+ )
index++
}
}
diff --git a/core/src/main/java/com/facebook/ktfmt/format/TrailingCommas.kt b/core/src/main/java/com/facebook/ktfmt/format/TrailingCommas.kt
new file mode 100644
index 0000000..e19337c
--- /dev/null
+++ b/core/src/main/java/com/facebook/ktfmt/format/TrailingCommas.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.facebook.ktfmt.format
+
+import org.jetbrains.kotlin.com.intellij.psi.PsiComment
+import org.jetbrains.kotlin.com.intellij.psi.PsiElement
+import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace
+import org.jetbrains.kotlin.psi.KtClassBody
+import org.jetbrains.kotlin.psi.KtCollectionLiteralExpression
+import org.jetbrains.kotlin.psi.KtElement
+import org.jetbrains.kotlin.psi.KtEnumEntry
+import org.jetbrains.kotlin.psi.KtFunctionLiteral
+import org.jetbrains.kotlin.psi.KtLambdaExpression
+import org.jetbrains.kotlin.psi.KtParameterList
+import org.jetbrains.kotlin.psi.KtTypeArgumentList
+import org.jetbrains.kotlin.psi.KtTypeParameterList
+import org.jetbrains.kotlin.psi.KtValueArgumentList
+import org.jetbrains.kotlin.psi.KtWhenEntry
+
+/** Detects trailing commas or elements that should have trailing commas. */
+object TrailingCommas {
+
+ class Detector {
+ private val trailingCommas = mutableListOf<PsiElement>()
+
+ fun getTrailingCommaElements(): List<PsiElement> = trailingCommas
+
+ /** returns **true** if this element was a traling comma, **false** otherwise. */
+ fun takeElement(element: PsiElement) {
+ if (isTrailingComma(element)) {
+ trailingCommas += element
+ }
+ }
+
+ private fun isTrailingComma(element: PsiElement): Boolean {
+ if (element.text != ",") {
+ return false
+ }
+
+ return extractManagedList(element.parent)?.trailingComma == element
+ }
+ }
+
+ class Suggestor {
+ private val suggestionElements = mutableListOf<PsiElement>()
+
+ fun getTrailingCommaSuggestions(): List<PsiElement> = suggestionElements
+
+ /**
+ * Record elements which should have trailing commas inserted.
+ *
+ * This function determines which element type which may need trailing commas, as well as logic
+ * for when they shold be inserted.
+ *
+ * Example:
+ * ```
+ * fun foo(
+ * x: VeryLongName,
+ * y: MoreThanLineLimit // Record this list
+ * ) { }
+ *
+ * fun bar(x: ShortName, y: FitsOnLine) { } // Ignore this list
+ * ```
+ */
+ fun takeElement(element: KtElement) {
+ if (!element.text.contains("\n")) {
+ return // Only suggest trailing commas where there is already a line break
+ }
+
+ when (element) {
+ is KtEnumEntry, // Only suggest on the KtClassBody container
+ is KtWhenEntry -> return
+ is KtParameterList -> {
+ if (element.parent is KtFunctionLiteral && element.parent.parent is KtLambdaExpression) {
+ return // Never add trailing commas to lambda param lists
+ }
+ }
+ is KtClassBody -> {
+ EnumEntryList.extractChildList(element)?.also {
+ if (it.terminatingSemicolon != null) {
+ return // Never add a trailing comma after there is already a terminating semicolon
+ }
+ }
+ }
+ }
+
+ val list = extractManagedList(element) ?: return
+ if (list.items.size <= 1) {
+ return // Never insert commas to single-element lists
+ }
+ if (list.trailingComma != null) {
+ return // Never insert a comma if there already is one somehow
+ }
+
+ suggestionElements.add(list.items.last().leftLeafIgnoringCommentsAndWhitespace())
+ }
+ }
+
+ private class ManagedList(val items: List<KtElement>, val trailingComma: PsiElement?)
+
+ private fun extractManagedList(element: PsiElement): ManagedList? {
+ return when (element) {
+ is KtValueArgumentList -> ManagedList(element.arguments, element.trailingComma)
+ is KtParameterList -> ManagedList(element.parameters, element.trailingComma)
+ is KtTypeArgumentList -> ManagedList(element.arguments, element.trailingComma)
+ is KtTypeParameterList -> ManagedList(element.parameters, element.trailingComma)
+ is KtCollectionLiteralExpression -> {
+ ManagedList(element.getInnerExpressions(), element.trailingComma)
+ }
+ is KtWhenEntry -> ManagedList(element.conditions.toList(), element.trailingComma)
+ is KtEnumEntry -> {
+ EnumEntryList.extractParentList(element).let {
+ ManagedList(it.enumEntries, it.trailingComma)
+ }
+ }
+ is KtClassBody -> {
+ EnumEntryList.extractChildList(element)?.let {
+ ManagedList(it.enumEntries, it.trailingComma)
+ }
+ }
+ else -> null
+ }
+ }
+
+ /**
+ * Return the element ahead of the where a comma would be appropriate for a list item.
+ *
+ * Example:
+ * ```
+ * fun foo(
+ * x: VeryLongName,
+ * y: MoreThanLineLimit /# Comment #/ = { it } /# Comment #/
+ * ^^^^^^ // After this element
+ * ) { }
+ * ```
+ */
+ private fun PsiElement.leftLeafIgnoringCommentsAndWhitespace(): PsiElement {
+ var child = this.lastChild
+ while (child != null) {
+ if (child is PsiWhiteSpace || child is PsiComment) {
+ child = child.prevSibling
+ } else {
+ return child.leftLeafIgnoringCommentsAndWhitespace()
+ }
+ }
+ return this
+ }
+}
diff --git a/core/src/main/java/com/facebook/ktfmt/format/TypeNameClassifier.kt b/core/src/main/java/com/facebook/ktfmt/format/TypeNameClassifier.kt
deleted file mode 100644
index 76fac3e..0000000
--- a/core/src/main/java/com/facebook/ktfmt/format/TypeNameClassifier.kt
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Copyright 2015 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-
-// This was copied from https://github.com/google/google-java-format and converted to Kotlin,
-// because the original is package-private.
-
-package com.facebook.ktfmt.format
-
-import com.google.common.base.Verify
-import java.util.Optional
-
-/** Heuristics for classifying qualified names as types. */
-object TypeNameClassifier {
-
- /** A state machine for classifying qualified names. */
- private enum class TyParseState(val isSingleUnit: Boolean) {
-
- /** The start state. */
- START(false) {
- override fun next(n: JavaCaseFormat): TyParseState {
- return when (n) {
- JavaCaseFormat.UPPERCASE ->
- // if we see an UpperCamel later, assume this was a class
- // e.g. com.google.FOO.Bar
- AMBIGUOUS
- JavaCaseFormat.LOWER_CAMEL -> REJECT
- JavaCaseFormat.LOWERCASE ->
- // could be a package
- START
- JavaCaseFormat.UPPER_CAMEL -> TYPE
- }
- }
- },
-
- /** The current prefix is a type. */
- TYPE(true) {
- override fun next(n: JavaCaseFormat): TyParseState {
- return when (n) {
- JavaCaseFormat.UPPERCASE,
- JavaCaseFormat.LOWER_CAMEL,
- JavaCaseFormat.LOWERCASE -> FIRST_STATIC_MEMBER
- JavaCaseFormat.UPPER_CAMEL -> TYPE
- }
- }
- },
-
- /** The current prefix is a type, followed by a single static member access. */
- FIRST_STATIC_MEMBER(true) {
- override fun next(n: JavaCaseFormat): TyParseState {
- return REJECT
- }
- },
-
- /** Anything not represented by one of the other states. */
- REJECT(false) {
- override fun next(n: JavaCaseFormat): TyParseState {
- return REJECT
- }
- },
-
- /** An ambiguous type prefix. */
- AMBIGUOUS(false) {
- override fun next(n: JavaCaseFormat): TyParseState {
- return when (n) {
- JavaCaseFormat.UPPERCASE -> AMBIGUOUS
- JavaCaseFormat.LOWER_CAMEL,
- JavaCaseFormat.LOWERCASE -> REJECT
- JavaCaseFormat.UPPER_CAMEL -> TYPE
- }
- }
- };
-
- /** Transition function. */
- abstract fun next(n: JavaCaseFormat): TyParseState
- }
-
- /**
- * Returns the end index (inclusive) of the longest prefix that matches the naming conventions of
- * a type or static field access, or -1 if no such prefix was found.
- *
- * Examples:
- * * ClassName
- * * ClassName.staticMemberName
- * * com.google.ClassName.InnerClass.staticMemberName
- */
- internal fun typePrefixLength(nameParts: List<String>): Optional<Int> {
- var state = TyParseState.START
- var typeLength = Optional.empty<Int>()
- for (i in nameParts.indices) {
- state = state.next(JavaCaseFormat.from(nameParts[i]))
- if (state === TyParseState.REJECT) {
- break
- }
- if (state.isSingleUnit) {
- typeLength = Optional.of(i)
- }
- }
- return typeLength
- }
-
- /** Case formats used in Java identifiers. */
- enum class JavaCaseFormat {
- UPPERCASE,
- LOWERCASE,
- UPPER_CAMEL,
- LOWER_CAMEL;
-
- companion object {
-
- /** Classifies an identifier's case format. */
- internal fun from(name: String): JavaCaseFormat {
- Verify.verify(name.isNotEmpty())
- var firstUppercase = false
- var hasUppercase = false
- var hasLowercase = false
- var first = true
- for (char in name) {
- if (!Character.isAlphabetic(char.code)) {
- continue
- }
- if (first) {
- firstUppercase = Character.isUpperCase(char)
- first = false
- }
- hasUppercase = hasUppercase or Character.isUpperCase(char)
- hasLowercase = hasLowercase or Character.isLowerCase(char)
- }
- return if (firstUppercase) {
- if (hasLowercase) UPPER_CAMEL else UPPERCASE
- } else {
- if (hasUppercase) LOWER_CAMEL else LOWERCASE
- }
- }
- }
- }
-}
diff --git a/core/src/main/java/com/facebook/ktfmt/kdoc/Paragraph.kt b/core/src/main/java/com/facebook/ktfmt/kdoc/Paragraph.kt
index 6d905e5..a3e6cbd 100644
--- a/core/src/main/java/com/facebook/ktfmt/kdoc/Paragraph.kt
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/Paragraph.kt
@@ -37,9 +37,11 @@ import kotlin.math.min
class Paragraph(private val task: FormattingTask) {
private val options: KDocFormattingOptions
get() = task.options
+
var content = StringBuilder()
val text
get() = content.toString()
+
var prev: Paragraph? = null
var next: Paragraph? = null
@@ -409,7 +411,7 @@ class Paragraph(private val task: FormattingTask) {
* need to make sure we don't make it the first word on the next line since that would change the
* documentation.
*/
- private fun canBreakAt(word: String): Boolean {
+ private fun canBreakAt(prev: String, word: String): Boolean {
// Can we start a new line with this without interpreting it in a special
// way?
@@ -422,6 +424,10 @@ class Paragraph(private val task: FormattingTask) {
return false
}
+ if (prev == "@sample") {
+ return false // https://github.com/facebook/ktfmt/issues/310
+ }
+
if (!word.first().isLetter()) {
val wordWithSpace = "$word " // for regex matching in below checks
if (wordWithSpace.isListItem() && !word.equals("<li>", true) || wordWithSpace.isQuoted()) {
@@ -480,18 +486,18 @@ class Paragraph(private val task: FormattingTask) {
}
}
if (j != -1) {
- // combine everything in the string; we can't break link text
- if (start == from + 1 && canBreakAt(words[start])) {
+ // combine everything in the string; we can't break link text or @sample tags
+ if (start == from + 1 && canBreakAt(words[start - 1], words[start])) {
combined.add(words[from])
from = start
}
// Maybe not break; what if the next word isn't okay?
to = j + 1
- if (to == words.size || canBreakAt(words[to])) {
+ if (to == words.size || canBreakAt(words[to - 1], words[to])) {
break
}
} // else: unterminated [, ignore
- } else if (canBreakAt(next)) {
+ } else if (canBreakAt(words[i - 1], next)) {
to = i
break
}
diff --git a/core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphList.kt b/core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphList.kt
index 5130824..a45d73f 100644
--- a/core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphList.kt
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphList.kt
@@ -23,6 +23,8 @@ package com.facebook.ktfmt.kdoc
*/
class ParagraphList(private val paragraphs: List<Paragraph>) : Iterable<Paragraph> {
fun isSingleParagraph() = paragraphs.size <= 1
+
override fun iterator(): Iterator<Paragraph> = paragraphs.iterator()
+
override fun toString(): String = paragraphs.joinToString { it.content }
}
diff --git a/core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphListBuilder.kt b/core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphListBuilder.kt
index 928a786..3d26e8d 100644
--- a/core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphListBuilder.kt
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphListBuilder.kt
@@ -232,7 +232,7 @@ class ParagraphListBuilder(
}
if (lineWithIndentation.startsWith(" ") && // markdown preformatted text
- (i == 1 || lineContent(lines[i - 2]).isBlank()) && // we've already ++'ed i above
+ (i == 1 || lineContent(lines[i - 2]).isBlank()) && // we've already ++'ed i above
// Make sure it's not just deeply indented inside a different block
(paragraph.prev == null ||
lineWithIndentation.length - lineWithoutIndentation.length >=
diff --git a/core/src/test/java/com/facebook/ktfmt/cli/MainTest.kt b/core/src/test/java/com/facebook/ktfmt/cli/MainTest.kt
index 3697d72..925ad27 100644
--- a/core/src/test/java/com/facebook/ktfmt/cli/MainTest.kt
+++ b/core/src/test/java/com/facebook/ktfmt/cli/MainTest.kt
@@ -21,11 +21,15 @@ import java.io.ByteArrayOutputStream
import java.io.File
import java.io.PrintStream
import java.lang.IllegalStateException
+import java.nio.charset.Charset
+import java.nio.charset.StandardCharsets
+import java.nio.charset.StandardCharsets.UTF_8
import java.nio.file.Files
import java.util.concurrent.ForkJoinPool
import kotlin.io.path.createTempDirectory
import org.junit.After
import org.junit.Assert.fail
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -40,6 +44,13 @@ class MainTest {
private val out = ByteArrayOutputStream()
private val err = ByteArrayOutputStream()
+ private val testCharset = StandardCharsets.UTF_16
+
+ @Before
+ fun setUp() {
+ assertThat(Charset.defaultCharset()).isEqualTo(testCharset) // Verify the test JVM flags
+ }
+
@After
fun tearDown() {
root.deleteRecursively()
@@ -52,7 +63,7 @@ class MainTest {
@Test
fun `expandArgsToFileNames - single file arg is used as is`() {
val fooBar = root.resolve("foo.bar")
- fooBar.writeText("hi")
+ fooBar.writeText("hi", UTF_8)
assertThat(Main.expandArgsToFileNames(listOf(fooBar.toString()))).containsExactly(fooBar)
}
@@ -67,9 +78,9 @@ class MainTest {
val dir = root.resolve("dir")
dir.mkdirs()
val foo = dir.resolve("foo.kt")
- foo.writeText("")
+ foo.writeText("", UTF_8)
val bar = dir.resolve("bar.kt")
- bar.writeText("")
+ bar.writeText("", UTF_8)
assertThat(Main.expandArgsToFileNames(listOf(dir.toString()))).containsExactly(foo, bar)
}
@@ -78,16 +89,16 @@ class MainTest {
val dir1 = root.resolve("dir1")
dir1.mkdirs()
val foo1 = dir1.resolve("foo1.kt")
- foo1.writeText("")
+ foo1.writeText("", UTF_8)
val bar1 = dir1.resolve("bar1.kt")
- bar1.writeText("")
+ bar1.writeText("", UTF_8)
val dir2 = root.resolve("dir2")
dir1.mkdirs()
val foo2 = dir1.resolve("foo2.kt")
- foo2.writeText("")
+ foo2.writeText("", UTF_8)
val bar2 = dir1.resolve("bar2.kt")
- bar2.writeText("")
+ bar2.writeText("", UTF_8)
assertThat(Main.expandArgsToFileNames(listOf(dir1.toString(), dir2.toString())))
.containsExactly(foo1, bar1, foo2, bar2)
@@ -109,7 +120,7 @@ class MainTest {
Main(code.byteInputStream(), PrintStream(out), PrintStream(err), arrayOf("-")).run()
val expected = "fun f1(): Int = 0\n"
- assertThat(out.toString("UTF-8")).isEqualTo(expected)
+ assertThat(out.toString(UTF_8)).isEqualTo(expected)
}
@Test
@@ -119,7 +130,7 @@ class MainTest {
Main(code.byteInputStream(), PrintStream(out), PrintStream(err), arrayOf("-")).run()
assertThat(returnValue).isEqualTo(1)
- assertThat(err.toString("UTF-8")).startsWith("<stdin>:1:14: error: ")
+ assertThat(err.toString(testCharset)).startsWith("<stdin>:1:14: error: ")
}
@Test
@@ -134,18 +145,30 @@ class MainTest {
.run()
assertThat(returnValue).isEqualTo(1)
- assertThat(err.toString("UTF-8")).startsWith("file/Foo.kt:1:14: error: ")
+ assertThat(err.toString(testCharset)).startsWith("file/Foo.kt:1:14: error: ")
}
@Test
fun `Parsing errors are reported (file)`() {
val fooBar = root.resolve("foo.kt")
- fooBar.writeText("fun f1 ( ")
+ fooBar.writeText("fun f1 ( ", UTF_8)
+ val returnValue =
+ Main(emptyInput, PrintStream(out), PrintStream(err), arrayOf(fooBar.toString())).run()
+
+ assertThat(returnValue).isEqualTo(1)
+ assertThat(err.toString(testCharset)).contains("foo.kt:1:14: error: ")
+ }
+
+ @Test
+ fun `Parsing error for multiple trailing lambdas`() {
+ val fooBar = root.resolve("foo.kt")
+ fooBar.writeText("val x = foo(bar { } { zap = 2 })")
val returnValue =
Main(emptyInput, PrintStream(out), PrintStream(err), arrayOf(fooBar.toString())).run()
assertThat(returnValue).isEqualTo(1)
- assertThat(err.toString("UTF-8")).contains("foo.kt:1:14: error: ")
+ assertThat(err.toString(testCharset))
+ .contains("foo.kt:1:21: error: Maximum one trailing lambda is allowed")
}
@Test
@@ -153,9 +176,9 @@ class MainTest {
val file1 = root.resolve("file1.kt")
val file2Broken = root.resolve("file2.kt")
val file3 = root.resolve("file3.kt")
- file1.writeText("fun f1 () ")
- file2Broken.writeText("fun f1 ( ")
- file3.writeText("fun f1 () ")
+ file1.writeText("fun f1 () ", UTF_8)
+ file2Broken.writeText("fun f1 ( ", UTF_8)
+ file3.writeText("fun f1 () ", UTF_8)
// Make Main() process files serially.
val forkJoinPool = ForkJoinPool(1)
@@ -173,16 +196,16 @@ class MainTest {
.get()
assertThat(returnValue).isEqualTo(1)
- assertThat(err.toString("UTF-8")).contains("Done formatting $file1")
- assertThat(err.toString("UTF-8")).contains("file2.kt:1:14: error: ")
- assertThat(err.toString("UTF-8")).contains("Done formatting $file3")
+ assertThat(err.toString(testCharset)).contains("Done formatting $file1")
+ assertThat(err.toString(testCharset)).contains("file2.kt:1:14: error: ")
+ assertThat(err.toString(testCharset)).contains("Done formatting $file3")
}
@Test
fun `file is not modified if it is already formatted`() {
val code = """fun f() = println("hello, world")""" + "\n"
val formattedFile = root.resolve("formatted_file.kt")
- formattedFile.writeText(code)
+ formattedFile.writeText(code, UTF_8)
val formattedFilePath = formattedFile.toPath()
val lastModifiedTimeBeforeRunningFormatter =
@@ -199,7 +222,7 @@ class MainTest {
fun `file is modified if it is not formatted`() {
val code = """fun f() = println( "hello, world")""" + "\n"
val unformattedFile = root.resolve("unformatted_file.kt")
- unformattedFile.writeText(code)
+ unformattedFile.writeText(code, UTF_8)
val unformattedFilePath = unformattedFile.toPath()
val lastModifiedTimeBeforeRunningFormatter =
@@ -225,7 +248,7 @@ class MainTest {
}
"""
val fooBar = root.resolve("foo.kt")
- fooBar.writeText(code)
+ fooBar.writeText(code, UTF_8)
Main(
emptyInput,
@@ -264,7 +287,7 @@ class MainTest {
arrayOf("--dropbox-style", "-"))
.run()
- assertThat(out.toString("UTF-8")).isEqualTo(formatted)
+ assertThat(out.toString(UTF_8)).isEqualTo(formatted)
}
@Test
@@ -298,7 +321,7 @@ class MainTest {
PrintStream(err),
arrayOf("-"))
.run()
- assertThat(out.toString("UTF-8")).isEqualTo(expected)
+ assertThat(out.toString(UTF_8)).isEqualTo(expected)
out.reset()
@@ -308,20 +331,20 @@ class MainTest {
PrintStream(err),
arrayOf("-"))
.run()
- assertThat(out.toString("UTF-8")).isEqualTo(expected)
+ assertThat(out.toString(UTF_8)).isEqualTo(expected)
}
@Test
fun `--dry-run prints filename and does not change file`() {
val code = """fun f () = println( "hello, world" )"""
val file = root.resolve("foo.kt")
- file.writeText(code)
+ file.writeText(code, UTF_8)
Main(emptyInput, PrintStream(out), PrintStream(err), arrayOf("--dry-run", file.toString()))
.run()
assertThat(file.readText()).isEqualTo(code)
- assertThat(out.toString("UTF-8")).contains(file.toString())
+ assertThat(out.toString(testCharset)).contains(file.toString())
}
@Test
@@ -331,20 +354,20 @@ class MainTest {
Main(code.byteInputStream(), PrintStream(out), PrintStream(err), arrayOf("--dry-run", "-"))
.run()
- assertThat(out.toString("UTF-8")).doesNotContain("hello, world")
- assertThat(out.toString("UTF-8")).isEqualTo("<stdin>\n")
+ assertThat(out.toString(UTF_8)).doesNotContain("hello, world")
+ assertThat(out.toString(testCharset)).isEqualTo("<stdin>\n")
}
@Test
fun `--dry-run prints nothing when there are no changes needed (file)`() {
val code = """fun f() = println("hello, world")\n"""
val file = root.resolve("foo.kt")
- file.writeText(code)
+ file.writeText(code, UTF_8)
Main(emptyInput, PrintStream(out), PrintStream(err), arrayOf("--dry-run", file.toString()))
.run()
- assertThat(out.toString("UTF-8")).isEmpty()
+ assertThat(out.toString(UTF_8)).isEmpty()
}
@Test
@@ -354,14 +377,14 @@ class MainTest {
Main(code.byteInputStream(), PrintStream(out), PrintStream(err), arrayOf("--dry-run", "-"))
.run()
- assertThat(out.toString("UTF-8")).isEmpty()
+ assertThat(out.toString(UTF_8)).isEmpty()
}
@Test
fun `Exit code is 0 when there are changes (file)`() {
val code = """fun f () = println( "hello, world" )"""
val file = root.resolve("foo.kt")
- file.writeText(code)
+ file.writeText(code, UTF_8)
val exitCode =
Main(emptyInput, PrintStream(out), PrintStream(err), arrayOf(file.toString())).run()
@@ -383,7 +406,7 @@ class MainTest {
fun `Exit code is 1 when there are changes and --set-exit-if-changed is set (file)`() {
val code = """fun f () = println( "hello, world" )"""
val file = root.resolve("foo.kt")
- file.writeText(code)
+ file.writeText(code, UTF_8)
val exitCode =
Main(
@@ -415,7 +438,7 @@ class MainTest {
fun `--set-exit-if-changed and --dry-run changes nothing, prints filenames, and exits with 1 (file)`() {
val code = """fun f () = println( "hello, world" )"""
val file = root.resolve("foo.kt")
- file.writeText(code)
+ file.writeText(code, UTF_8)
val exitCode =
Main(
@@ -426,7 +449,7 @@ class MainTest {
.run()
assertThat(file.readText()).isEqualTo(code)
- assertThat(out.toString("UTF-8")).contains(file.toString())
+ assertThat(out.toString(testCharset)).contains(file.toString())
assertThat(exitCode).isEqualTo(1)
}
@@ -442,8 +465,8 @@ class MainTest {
arrayOf("--dry-run", "--set-exit-if-changed", "-"))
.run()
- assertThat(out.toString("UTF-8")).doesNotContain("hello, world")
- assertThat(out.toString("UTF-8")).isEqualTo("<stdin>\n")
+ assertThat(out.toString(UTF_8)).doesNotContain("hello, world")
+ assertThat(out.toString(testCharset)).isEqualTo("<stdin>\n")
assertThat(exitCode).isEqualTo(1)
}
@@ -451,7 +474,7 @@ class MainTest {
fun `--stdin-name can only be used with stdin`() {
val code = """fun f () = println( "hello, world" )"""
val file = root.resolve("foo.kt")
- file.writeText(code)
+ file.writeText(code, UTF_8)
val exitCode =
Main(
@@ -462,8 +485,46 @@ class MainTest {
.run()
assertThat(file.readText()).isEqualTo(code)
- assertThat(out.toString("UTF-8")).isEmpty()
- assertThat(err.toString("UTF-8")).isEqualTo("Error: --stdin-name can only be used with stdin\n")
+ assertThat(out.toString(UTF_8)).isEmpty()
+ assertThat(err.toString(testCharset))
+ .isEqualTo("Error: --stdin-name can only be used with stdin\n")
assertThat(exitCode).isEqualTo(1)
}
+
+ @Test
+ fun `Always use UTF8 encoding (stdin, stdout)`() {
+ val code = """fun f () = println( "hello, world" )"""
+ val expected = """fun f() = println("hello, world")""" + "\n"
+
+ val exitCode =
+ Main(
+ code.byteInputStream(UTF_8),
+ PrintStream(out, true, testCharset),
+ PrintStream(err),
+ arrayOf("-"),
+ )
+ .run()
+
+ assertThat(exitCode).isEqualTo(0)
+ assertThat(out.toString(UTF_8)).isEqualTo(expected)
+ }
+
+ @Test
+ fun `Always use UTF8 encoding (file)`() {
+ val code = """fun f() = println( "hello, world")""" + "\n"
+ val file = root.resolve("unformatted_file.kt")
+ file.writeText(code, UTF_8)
+
+ val exitCode =
+ Main(
+ emptyInput,
+ PrintStream(out),
+ PrintStream(err),
+ arrayOf(file.toString()),
+ )
+ .run()
+
+ assertThat(exitCode).isEqualTo(0)
+ assertThat(file.readText(UTF_8)).isEqualTo("""fun f() = println("hello, world")""" + "\n")
+ }
}
diff --git a/core/src/test/java/com/facebook/ktfmt/format/FormatterTest.kt b/core/src/test/java/com/facebook/ktfmt/format/FormatterTest.kt
index ae9d05e..4e8b587 100644
--- a/core/src/test/java/com/facebook/ktfmt/format/FormatterTest.kt
+++ b/core/src/test/java/com/facebook/ktfmt/format/FormatterTest.kt
@@ -65,7 +65,7 @@ class FormatterTest {
fun `call chains`() =
assertFormatted(
"""
- |--------------------------------------------------
+ |//////////////////////////////////////////////////
|fun f() {
| // Static method calls are attached to the class name.
| ImmutableList.newBuilder()
@@ -104,7 +104,7 @@ class FormatterTest {
fun `line breaks in function arguments`() =
assertFormatted(
"""
- |--------------------------------------------------
+ |//////////////////////////////////////////////////
|fun f() {
| computeBreaks(
| javaOutput.commentsHelper,
@@ -127,7 +127,7 @@ class FormatterTest {
fun `parameters and return type in function definitions`() =
assertFormatted(
"""
- |----------------------------------------
+ |////////////////////////////////////////
|fun format(
| code: String,
| maxWidth: Int =
@@ -264,7 +264,9 @@ class FormatterTest {
"""
|class Foo(a: Int, var b: Double, val c: String) {
| val x = 2
+ |
| fun method() {}
+ |
| class Bar
|}
|"""
@@ -276,8 +278,11 @@ class FormatterTest {
"""
|class Foo(public val p1: Int, private val p2: Int, open val p3: Int, final val p4: Int) {
| private var f1 = 0
+ |
| public var f2 = 0
+ |
| open var f3 = 0
+ |
| final var f4 = 0
|}
|"""
@@ -308,7 +313,7 @@ class FormatterTest {
fun `breaking long binary operations`() =
assertFormatted(
"""
- |--------------------
+ |////////////////////
|fun foo() {
| val finalWidth =
| value1 +
@@ -331,7 +336,7 @@ class FormatterTest {
fun `prioritize according to associativity`() =
assertFormatted(
"""
- |--------------------------------------
+ |//////////////////////////////////////
|fun foo() {
| return expression1 != expression2 ||
| expression2 != expression1
@@ -344,7 +349,7 @@ class FormatterTest {
fun `once a binary expression is broken, split on every line`() =
assertFormatted(
"""
- |--------------------------------------
+ |//////////////////////////////////////
|fun foo() {
| val sentence =
| "The" +
@@ -364,12 +369,13 @@ class FormatterTest {
fun `long binary expressions with ranges in the middle`() =
assertFormatted(
"""
- |--------------------------------------
+ |//////////////////////////////////////
|fun foo() {
| val sentence =
| "The" +
| "quick" +
| ("brown".."fox") +
+ | ("brown"..<"fox") +
| "jumps" +
| "over" +
| "the".."lazy" + "dog"
@@ -382,7 +388,7 @@ class FormatterTest {
fun `assignment expressions with scoping functions are block-like`() =
assertFormatted(
"""
- |---------------------------
+ |///////////////////////////
|fun f() {
| name.sub = scope { x ->
| //
@@ -435,15 +441,46 @@ class FormatterTest {
deduceMaxWidth = true)
@Test
- fun `don't keep adding newlines between these two comments when they're at end of file`() =
- assertFormatted(
- """
+ fun `don't keep adding newlines between these two comments when they're at end of file`() {
+ assertFormatted(
+ """
|package foo
|// a
|
|/* Another comment */
|"""
- .trimMargin())
+ .trimMargin())
+
+ assertFormatted(
+ """
+ |// Comment as first element
+ |package foo
+ |// a
+ |
+ |/* Another comment */
+ |"""
+ .trimMargin())
+
+ assertFormatted(
+ """
+ |// Comment as first element then blank line
+ |
+ |package foo
+ |// a
+ |
+ |/* Another comment */
+ |"""
+ .trimMargin())
+
+ assertFormatted(
+ """
+ |// Comment as first element
+ |package foo
+ |// Adjacent line comments
+ |// Don't separate
+ |"""
+ .trimMargin())
+ }
@Test
fun `properties with line comment above initializer`() =
@@ -504,15 +541,18 @@ class FormatterTest {
|class Foo {
| var x: Int
| get() = field
+ |
| var y: Boolean
| get() = x.equals(123)
| set(value) {
| field = value
| }
+ |
| var z: Boolean
| get() {
| x.equals(123)
| }
+ |
| var zz = false
| private set
|}
@@ -525,7 +565,9 @@ class FormatterTest {
"""
|class Foo {
| var x = false; private set
+ |
| internal val a by lazy { 5 }; internal get
+ |
| var foo: Int; get() = 6; set(x) {};
|}
|"""
@@ -536,8 +578,10 @@ class FormatterTest {
|class Foo {
| var x = false
| private set
+ |
| internal val a by lazy { 5 }
| internal get
+ |
| var foo: Int
| get() = 6
| set(x) {}
@@ -552,7 +596,7 @@ class FormatterTest {
fun `a property with a too long name being broken on multiple lines`() =
assertFormatted(
"""
- |--------------------
+ |////////////////////
|class Foo {
| val thisIsALongName:
| String =
@@ -632,7 +676,7 @@ class FormatterTest {
fun `safe dot operator expression chain in expression function`() =
assertFormatted(
"""
- |--------------------------------------------------
+ |//////////////////////////////////////////////////
|fun f(number: Int) =
| Something.doStuff(number)?.size
|"""
@@ -643,7 +687,7 @@ class FormatterTest {
fun `avoid breaking suspected package names`() =
assertFormatted(
"""
- |-----------------------
+ |///////////////////////
|fun f() {
| com.facebook.Foo
| .format()
@@ -666,9 +710,9 @@ class FormatterTest {
fun `an assortment of tests for emitQualifiedExpression`() =
assertFormatted(
"""
- |-------------------------------------
+ |/////////////////////////////////////
|fun f() {
- | // Regression test: https://github.com/facebookincubator/ktfmt/issues/56
+ | // Regression test: https://github.com/facebook/ktfmt/issues/56
| kjsdfglkjdfgkjdfkgjhkerjghkdfj
| ?.methodName1()
|
@@ -718,7 +762,7 @@ class FormatterTest {
fun `an assortment of tests for emitQualifiedExpression with lambdas`() =
assertFormatted(
"""
- |----------------------------------------------------------------------------
+ |////////////////////////////////////////////////////////////////////////////
|fun f() {
| val items =
| items.toMutableList.apply {
@@ -818,7 +862,7 @@ class FormatterTest {
fun `don't one-line lambdas following argument breaks`() =
assertFormatted(
"""
- |------------------------------------------------------------------------
+ |////////////////////////////////////////////////////////////////////////
|class Foo : Bar() {
| fun doIt() {
| // don't break in lambda, no argument breaks found
@@ -876,7 +920,7 @@ class FormatterTest {
fun `indent parameters after a break when there's a lambda afterwards`() =
assertFormatted(
"""
- |---------------------------
+ |///////////////////////////
|class C {
| fun method() {
| Foo.FooBar(
@@ -895,7 +939,7 @@ class FormatterTest {
fun `no forward propagation of breaks in call expressions (at trailing lambda)`() =
assertFormatted(
"""
- |--------------------------
+ |//////////////////////////
|fun test() {
| foo_bar_baz__zip<A>(b) {
| c
@@ -912,7 +956,7 @@ class FormatterTest {
fun `forward propagation of breaks in call expressions (at value args)`() =
assertFormatted(
"""
- |----------------------
+ |//////////////////////
|fun test() {
| foo_bar_baz__zip<A>(
| b) {
@@ -934,7 +978,7 @@ class FormatterTest {
fun `forward propagation of breaks in call expressions (at type args)`() =
assertFormatted(
"""
- |-------------------
+ |///////////////////
|fun test() {
| foo_bar_baz__zip<
| A>(
@@ -955,9 +999,9 @@ class FormatterTest {
fun `expected indent in methods following single-line strings`() =
assertFormatted(
"""
- |-------------------------
- |"Hello %s".format(
- | someLongExpression)
+ |/////////////////////////
+ |"Hello %s"
+ | .format(expression)
|"""
.trimMargin(),
deduceMaxWidth = true)
@@ -966,7 +1010,7 @@ class FormatterTest {
fun `forced break between multi-line strings and their selectors`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|val STRING =
| $TQ
| |foo
@@ -976,7 +1020,7 @@ class FormatterTest {
|val STRING =
| $TQ
| |foo
- | |----------------------------------$TQ
+ | |//////////////////////////////////$TQ
| .wouldntFit()
|
|val STRING =
@@ -1136,31 +1180,121 @@ class FormatterTest {
}
@Test
- fun `imports from the same package are removed`() {
+ fun `used imports from this package are removed`() {
val code =
"""
- |package com.example
- |
- |import com.example.Sample
- |import com.example.Sample.CONSTANT
- |import com.example.a.foo
- |
- |fun test() {
- | foo(CONSTANT, Sample())
- |}
- |"""
+ |package com.example
+ |
+ |import com.example.Sample
+ |import com.example.Sample.CONSTANT
+ |import com.example.a.foo
+ |
+ |fun test() {
+ | foo(CONSTANT, Sample())
+ |}
+ |"""
.trimMargin()
val expected =
"""
- |package com.example
- |
- |import com.example.Sample.CONSTANT
- |import com.example.a.foo
- |
- |fun test() {
- | foo(CONSTANT, Sample())
- |}
- |"""
+ |package com.example
+ |
+ |import com.example.Sample.CONSTANT
+ |import com.example.a.foo
+ |
+ |fun test() {
+ | foo(CONSTANT, Sample())
+ |}
+ |"""
+ .trimMargin()
+ assertThatFormatting(code).isEqualTo(expected)
+ }
+
+ @Test
+ fun `potentially unused imports from this package are kept if they are overloaded`() {
+ val code =
+ """
+ |package com.example
+ |
+ |import com.example.a
+ |import com.example.b
+ |import com.example.c
+ |import com.notexample.a
+ |import com.notexample.b
+ |import com.notexample.notC as c
+ |
+ |fun test() {
+ | a("hello")
+ | c("hello")
+ |}
+ |"""
+ .trimMargin()
+ val expected =
+ """
+ |package com.example
+ |
+ |import com.example.a
+ |import com.example.c
+ |import com.notexample.a
+ |import com.notexample.notC as c
+ |
+ |fun test() {
+ | a("hello")
+ | c("hello")
+ |}
+ |"""
+ .trimMargin()
+ assertThatFormatting(code).isEqualTo(expected)
+ }
+
+ @Test
+ fun `used imports from this package are kept if they are aliased`() {
+ val code =
+ """
+ |package com.example
+ |
+ |import com.example.b as a
+ |import com.example.c
+ |
+ |fun test() {
+ | a("hello")
+ |}
+ |"""
+ .trimMargin()
+ val expected =
+ """
+ |package com.example
+ |
+ |import com.example.b as a
+ |
+ |fun test() {
+ | a("hello")
+ |}
+ |"""
+ .trimMargin()
+ assertThatFormatting(code).isEqualTo(expected)
+ }
+
+ @Test
+ fun `unused imports are computed using only the alias name if present`() {
+ val code =
+ """
+ |package com.example
+ |
+ |import com.notexample.a as b
+ |
+ |fun test() {
+ | a("hello")
+ |}
+ |"""
+ .trimMargin()
+ val expected =
+ """
+ |package com.example
+ |
+ |fun test() {
+ | a("hello")
+ |}
+ |"""
.trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -1169,66 +1303,66 @@ class FormatterTest {
fun `keep import elements only mentioned in kdoc`() {
val code =
"""
- |package com.example.kdoc
- |
- |import com.example.Bar
- |import com.example.Example
- |import com.example.Foo
- |import com.example.JavaDocLink
- |import com.example.Param
- |import com.example.R
- |import com.example.ReturnedValue
- |import com.example.Sample
- |import com.example.unused
- |import com.example.exception.AnException
- |import com.example.kdoc.Doc
- |
- |/**
- | * [Foo] is something only mentioned here, just like [R.layout.test] and [Doc].
- | *
- | * Old {@link JavaDocLink} that gets removed.
- | *
- | * @throws AnException
- | * @exception Sample.SampleException
- | * @param unused [Param]
- | * @property JavaDocLink [Param]
- | * @return [Unit] as [ReturnedValue]
- | * @sample Example
- | * @see Bar for more info
- | * @throws AnException
- | */
- |class Dummy
- |"""
+ |package com.example.kdoc
+ |
+ |import com.example.Bar
+ |import com.example.Example
+ |import com.example.Foo
+ |import com.example.JavaDocLink
+ |import com.example.Param
+ |import com.example.R
+ |import com.example.ReturnedValue
+ |import com.example.Sample
+ |import com.example.unused
+ |import com.example.exception.AnException
+ |import com.example.kdoc.Doc
+ |
+ |/**
+ | * [Foo] is something only mentioned here, just like [R.layout.test] and [Doc].
+ | *
+ | * Old {@link JavaDocLink} that gets removed.
+ | *
+ | * @throws AnException
+ | * @exception Sample.SampleException
+ | * @param unused [Param]
+ | * @property JavaDocLink [Param]
+ | * @return [Unit] as [ReturnedValue]
+ | * @sample Example
+ | * @see Bar for more info
+ | * @throws AnException
+ | */
+ |class Dummy
+ |"""
.trimMargin()
val expected =
"""
- |package com.example.kdoc
- |
- |import com.example.Bar
- |import com.example.Example
- |import com.example.Foo
- |import com.example.Param
- |import com.example.R
- |import com.example.ReturnedValue
- |import com.example.Sample
- |import com.example.exception.AnException
- |
- |/**
- | * [Foo] is something only mentioned here, just like [R.layout.test] and [Doc].
- | *
- | * Old {@link JavaDocLink} that gets removed.
- | *
- | * @param unused [Param]
- | * @return [Unit] as [ReturnedValue]
- | * @property JavaDocLink [Param]
- | * @throws AnException
- | * @throws AnException
- | * @exception Sample.SampleException
- | * @sample Example
- | * @see Bar for more info
- | */
- |class Dummy
- |"""
+ |package com.example.kdoc
+ |
+ |import com.example.Bar
+ |import com.example.Example
+ |import com.example.Foo
+ |import com.example.Param
+ |import com.example.R
+ |import com.example.ReturnedValue
+ |import com.example.Sample
+ |import com.example.exception.AnException
+ |
+ |/**
+ | * [Foo] is something only mentioned here, just like [R.layout.test] and [Doc].
+ | *
+ | * Old {@link JavaDocLink} that gets removed.
+ | *
+ | * @param unused [Param]
+ | * @return [Unit] as [ReturnedValue]
+ | * @property JavaDocLink [Param]
+ | * @throws AnException
+ | * @throws AnException
+ | * @exception Sample.SampleException
+ | * @sample Example
+ | * @see Bar for more info
+ | */
+ |class Dummy
+ |"""
.trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -1237,15 +1371,15 @@ class FormatterTest {
fun `keep import elements only mentioned in kdoc, single line`() {
assertFormatted(
"""
- |import com.shopping.Bag
- |
- |/**
- | * Some summary.
- | *
- | * @param count you can fit this many in a [Bag]
- | */
- |fun fetchBananas(count: Int)
- |"""
+ |import com.shopping.Bag
+ |
+ |/**
+ | * Some summary.
+ | *
+ | * @param count you can fit this many in a [Bag]
+ | */
+ |fun fetchBananas(count: Int)
+ |"""
.trimMargin())
}
@@ -1253,16 +1387,16 @@ class FormatterTest {
fun `keep import elements only mentioned in kdoc, multiline`() {
assertFormatted(
"""
- |import com.shopping.Bag
- |
- |/**
- | * Some summary.
- | *
- | * @param count this is how many of these wonderful fruit you can fit into the useful object that
- | * you may refer to as a [Bag]
- | */
- |fun fetchBananas(count: Int)
- |"""
+ |import com.shopping.Bag
+ |
+ |/**
+ | * Some summary.
+ | *
+ | * @param count this is how many of these wonderful fruit you can fit into the useful object that
+ | * you may refer to as a [Bag]
+ | */
+ |fun fetchBananas(count: Int)
+ |"""
.trimMargin())
}
@@ -1270,69 +1404,69 @@ class FormatterTest {
fun `keep component imports`() =
assertFormatted(
"""
- |import com.example.component1
- |import com.example.component10
- |import com.example.component120
- |import com.example.component2
- |import com.example.component3
- |import com.example.component4
- |import com.example.component5
- |"""
+ |import com.example.component1
+ |import com.example.component10
+ |import com.example.component120
+ |import com.example.component2
+ |import com.example.component3
+ |import com.example.component4
+ |import com.example.component5
+ |"""
.trimMargin())
@Test
fun `keep operator imports`() =
assertFormatted(
"""
- |import com.example.and
- |import com.example.compareTo
- |import com.example.contains
- |import com.example.dec
- |import com.example.div
- |import com.example.divAssign
- |import com.example.equals
- |import com.example.get
- |import com.example.getValue
- |import com.example.hasNext
- |import com.example.inc
- |import com.example.invoke
- |import com.example.iterator
- |import com.example.minus
- |import com.example.minusAssign
- |import com.example.mod
- |import com.example.modAssign
- |import com.example.next
- |import com.example.not
- |import com.example.or
- |import com.example.plus
- |import com.example.plusAssign
- |import com.example.provideDelegate
- |import com.example.rangeTo
- |import com.example.rem
- |import com.example.remAssign
- |import com.example.set
- |import com.example.setValue
- |import com.example.times
- |import com.example.timesAssign
- |import com.example.unaryMinus
- |import com.example.unaryPlus
- |"""
+ |import com.example.and
+ |import com.example.compareTo
+ |import com.example.contains
+ |import com.example.dec
+ |import com.example.div
+ |import com.example.divAssign
+ |import com.example.equals
+ |import com.example.get
+ |import com.example.getValue
+ |import com.example.hasNext
+ |import com.example.inc
+ |import com.example.invoke
+ |import com.example.iterator
+ |import com.example.minus
+ |import com.example.minusAssign
+ |import com.example.mod
+ |import com.example.modAssign
+ |import com.example.next
+ |import com.example.not
+ |import com.example.or
+ |import com.example.plus
+ |import com.example.plusAssign
+ |import com.example.provideDelegate
+ |import com.example.rangeTo
+ |import com.example.rem
+ |import com.example.remAssign
+ |import com.example.set
+ |import com.example.setValue
+ |import com.example.times
+ |import com.example.timesAssign
+ |import com.example.unaryMinus
+ |import com.example.unaryPlus
+ |"""
.trimMargin())
@Test
fun `keep unused imports when formatting options has feature turned off`() {
val code =
"""
- |import com.unused.FooBarBaz as Baz
- |import com.unused.Sample
- |import com.unused.a as `when`
- |import com.unused.a as wow
- |import com.unused.a.*
- |import com.unused.b as `if`
- |import com.unused.b as we
- |import com.unused.bar // test
- |import com.unused.`class`
- |"""
+ |import com.unused.FooBarBaz as Baz
+ |import com.unused.Sample
+ |import com.unused.a as `when`
+ |import com.unused.a as wow
+ |import com.unused.a.*
+ |import com.unused.b as `if`
+ |import com.unused.b as we
+ |import com.unused.bar // test
+ |import com.unused.`class`
+ |"""
.trimMargin()
assertThatFormatting(code)
@@ -1344,34 +1478,34 @@ class FormatterTest {
fun `comments between imports are moved above import list`() {
val code =
"""
- |package com.facebook.ktfmt
- |
- |/* leading comment */
- |import com.example.abc
- |/* internal comment 1 */
- |import com.example.bcd
- |// internal comment 2
- |import com.example.Sample
- |// trailing comment
- |
- |val x = Sample(abc, bcd)
- |"""
+ |package com.facebook.ktfmt
+ |
+ |/* leading comment */
+ |import com.example.abc
+ |/* internal comment 1 */
+ |import com.example.bcd
+ |// internal comment 2
+ |import com.example.Sample
+ |// trailing comment
+ |
+ |val x = Sample(abc, bcd)
+ |"""
.trimMargin()
val expected =
"""
- |package com.facebook.ktfmt
- |
- |/* leading comment */
- |/* internal comment 1 */
- |// internal comment 2
- |import com.example.Sample
- |import com.example.abc
- |import com.example.bcd
- |
- |// trailing comment
- |
- |val x = Sample(abc, bcd)
- |"""
+ |package com.facebook.ktfmt
+ |
+ |/* leading comment */
+ |/* internal comment 1 */
+ |// internal comment 2
+ |import com.example.Sample
+ |import com.example.abc
+ |import com.example.bcd
+ |
+ |// trailing comment
+ |
+ |val x = Sample(abc, bcd)
+ |"""
.trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -1380,12 +1514,12 @@ class FormatterTest {
fun `no redundant newlines when there are no imports`() =
assertFormatted(
"""
- |package foo123
- |
- |/*
- |bar
- |*/
- |"""
+ |package foo123
+ |
+ |/*
+ |bar
+ |*/
+ |"""
.trimMargin())
@Test
@@ -1451,7 +1585,7 @@ class FormatterTest {
fun `Arguments are blocks`() =
assertFormatted(
"""
- |--------------------------------------------------
+ |//////////////////////////////////////////////////
|override fun visitProperty(property: KtProperty) {
| builder.sync(property)
| builder.block(ZERO) {
@@ -1563,6 +1697,7 @@ class FormatterTest {
| in a..3 -> print()
| in 1..b -> print()
| !in 1..b -> print()
+ | in 1..<b -> print()
| else -> print(3)
| }
|}
@@ -1600,7 +1735,7 @@ class FormatterTest {
fun `when() expression with multiline condition`() =
assertFormatted(
"""
- |--------------------------
+ |//////////////////////////
|fun foo() {
| when (expressions1 +
| expression2 +
@@ -1688,7 +1823,7 @@ class FormatterTest {
fun `multi line function without a block body`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|fun longFunctionNoBlock():
| Int =
| 1234567 + 1234567
@@ -1703,7 +1838,7 @@ class FormatterTest {
fun `return type doesn't fit in one line`() =
assertFormatted(
"""
- |--------------------------------------------------
+ |//////////////////////////////////////////////////
|interface X {
| fun f(
| arg1: Arg1Type,
@@ -1743,7 +1878,7 @@ class FormatterTest {
fun `list of superclasses over multiple lines`() =
assertFormatted(
"""
- |--------------------
+ |////////////////////
|class Derived2 :
| Super1,
| Super2 {}
@@ -2019,7 +2154,7 @@ class FormatterTest {
fun `if expression with break before else`() =
assertFormatted(
"""
- |------------------------------
+ |//////////////////////////////
|fun compute(b: Boolean) {
| val c =
| if (a + b < 20) a + b
@@ -2035,7 +2170,7 @@ class FormatterTest {
fun `if expression with break before expressions`() =
assertFormatted(
"""
- |--------------------------
+ |//////////////////////////
|fun compute(b: Boolean) {
| val c =
| if (a + b < 20)
@@ -2073,7 +2208,7 @@ class FormatterTest {
fun `if expression with multiline condition`() =
assertFormatted(
"""
- |----------------------------
+ |////////////////////////////
|fun foo() {
| if (expressions1 &&
| expression2 &&
@@ -2096,7 +2231,7 @@ class FormatterTest {
fun `assignment expression on multiple lines`() =
assertFormatted(
"""
- |--------------------------------------------------
+ |//////////////////////////////////////////////////
|fun f() {
| var myVariable = 5
| myVariable =
@@ -2118,6 +2253,7 @@ class FormatterTest {
fun `a few variations of constructors`() =
assertFormatted(
"""
+ |//////////////////////////////////////////////////////
|class Foo constructor(number: Int) {}
|
|class Foo2 private constructor(number: Int) {}
@@ -2136,14 +2272,25 @@ class FormatterTest {
| number5: Int,
| number6: Int
|) {}
+ |
+ |class Foo6
+ |@Inject
+ |private constructor(hasSpaceForAnnos: Innnt) {
+ | // @Inject
+ |}
+ |
+ |class FooTooLongForCtorAndSupertypes
+ |@Inject
+ |private constructor(x: Int) : NoooooooSpaceForAnnos {}
|"""
- .trimMargin())
+ .trimMargin(),
+ deduceMaxWidth = true)
@Test
fun `a primary constructor without a class body `() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|data class Foo(
| val number: Int = 0
|)
@@ -2155,7 +2302,7 @@ class FormatterTest {
fun `a secondary constructor without a body`() =
assertFormatted(
"""
- |---------------------------
+ |///////////////////////////
|data class Foo {
| constructor(
| val number: Int = 0
@@ -2169,7 +2316,7 @@ class FormatterTest {
fun `a secondary constructor with a body breaks before closing parenthesis`() =
assertFormatted(
"""
- |---------------------------
+ |///////////////////////////
|data class Foo {
| constructor(
| val number: Int = 0
@@ -2212,7 +2359,7 @@ class FormatterTest {
fun `a constructor with many arguments over multiple lines`() =
assertFormatted(
"""
- |--------------------------------------------------
+ |//////////////////////////////////////////////////
|data class Foo
|constructor(
| val number: Int,
@@ -2231,6 +2378,7 @@ class FormatterTest {
"""
|class Foo private constructor(number: Int) {
| private constructor(n: Float) : this(1)
+ |
| private constructor(n: Double) : this(1) {
| println("built")
| }
@@ -2242,7 +2390,7 @@ class FormatterTest {
fun `a secondary constructor with many arguments over multiple lines`() =
assertFormatted(
"""
- |--------------------------------------------------
+ |//////////////////////////////////////////////////
|data class Foo {
| constructor(
| val number: Int,
@@ -2260,7 +2408,7 @@ class FormatterTest {
fun `a secondary constructor with many arguments passed to delegate`() =
assertFormatted(
"""
- |--------------------------------------------------
+ |//////////////////////////////////////////////////
|data class Foo {
| constructor(
| val number: Int,
@@ -2284,7 +2432,7 @@ class FormatterTest {
fun `a secondary constructor with no arguments passed to delegate`() =
assertFormatted(
"""
- |--------------------------------------------------
+ |//////////////////////////////////////////////////
|data class Foo {
| constructor() :
| this(
@@ -2405,7 +2553,7 @@ class FormatterTest {
fun `keep array indexing grouped with expression is possible`() =
assertFormatted(
"""
- |-----------------------
+ |///////////////////////
|fun f(a: Magic) {
| foo.bar()
| .foobar[1, 2, 3]
@@ -2428,7 +2576,7 @@ class FormatterTest {
fun `mixed chains`() =
assertFormatted(
"""
- |-----------------------
+ |///////////////////////
|fun f(a: Magic) {
| foo.bar()
| .foobar[1, 2, 3]
@@ -2451,7 +2599,7 @@ class FormatterTest {
fun `handle destructuring declaration`() =
assertFormatted(
"""
- |-----------------------------------------------
+ |///////////////////////////////////////////////
|fun f() {
| val (a, b: Int) = listOf(1, 2)
| val (asd, asd, asd, asd, asd, asd, asd) =
@@ -2464,11 +2612,12 @@ class FormatterTest {
|"""
.trimMargin(),
deduceMaxWidth = true)
+
@Test
fun `chains with derferences and array indexing`() =
assertFormatted(
"""
- |-----------------------
+ |///////////////////////
|fun f() {
| foo.bam()
| .uber!![0, 1, 2]
@@ -2486,7 +2635,7 @@ class FormatterTest {
fun `block like syntax after dereferences and indexing with short lines`() =
assertFormatted(
"""
- |-----------------------
+ |///////////////////////
|fun f() {
| foo.bam()
| .uber!![0, 1, 2]
@@ -2502,7 +2651,7 @@ class FormatterTest {
fun `block like syntax after dereferences and indexing with long lines`() =
assertFormatted(
"""
- |----------------------------------
+ |//////////////////////////////////
|fun f() {
| foo.uber!![0, 1, 2].forEach {
| println(it)
@@ -2516,7 +2665,7 @@ class FormatterTest {
fun `try to keep type names together`() =
assertFormatted(
"""
- |-----------------------
+ |///////////////////////
|fun f() {
| com.facebook.foo.Foo(
| 1, 2)
@@ -2543,7 +2692,7 @@ class FormatterTest {
fun `avoid breaking brackets and keep them with array name`() =
assertFormatted(
"""
- |-------------------------------------------------------------------------
+ |/////////////////////////////////////////////////////////////////////////
|fun f() {
| val a =
| invokeIt(context.packageName)
@@ -2576,7 +2725,7 @@ class FormatterTest {
fun `array access in middle of chain and end of it behaves similarly`() =
assertFormatted(
"""
- |--------------------------------------
+ |//////////////////////////////////////
|fun f() {
| if (aaaaa == null ||
| aaaaa.bbbbb[0] == null ||
@@ -2590,14 +2739,24 @@ class FormatterTest {
deduceMaxWidth = true)
@Test
- fun `handle qmark for nullalble types`() =
+ fun `handle qmark for nullable types`() =
assertFormatted(
"""
- |fun doItWithNullReturns(a: String, b: String): Int? {
- | return 5
- |}
+ |var x: Int? = null
+ |var x: (Int)? = null
+ |var x: (Int?) = null
+ |var x: ((Int))? = null
+ |var x: ((Int?)) = null
+ |var x: ((Int)?) = null
|
- |fun doItWithNulls(a: String, b: String?) {}
+ |var x: @Anno Int? = null
+ |var x: @Anno() (Int)? = null
+ |var x: @Anno (Int?) = null
+ |var x: (@Anno Int)? = null
+ |var x: (@Anno Int?) = null
+ |var x: (@Anno() (Int))? = null
+ |var x: (@Anno (Int?)) = null
+ |var x: (@Anno() (Int)?) = null
|"""
.trimMargin())
@@ -2636,8 +2795,14 @@ class FormatterTest {
assertFormatted(
"""
|fun doIt(world: String) {
- | println(${"\"".repeat(3)}Hello
- | world!${"\"".repeat(3)})
+ | println(
+ | ${TQ}Hello
+ | world!${TQ})
+ | println(
+ | ${TQ}Hello
+ | world!${TQ},
+ | ${TQ}Goodbye
+ | world!${TQ})
|}
|"""
.trimMargin())
@@ -2647,14 +2812,18 @@ class FormatterTest {
val code =
listOf(
"fun doIt(world: String) {",
- " println(\"\"\"This line has trailing whitespace ",
- " world!\"\"\")",
- " println(\"\"\"This line has trailing whitespace \$s ",
- " world!\"\"\")",
- " println(\"\"\"This line has trailing whitespace \${s} ",
- " world!\"\"\")",
- " println(\"\"\"This line has trailing whitespace \$ ",
- " world!\"\"\")",
+ " println(",
+ " ${TQ}This line has trailing whitespace ",
+ " world!${TQ})",
+ " println(",
+ " ${TQ}This line has trailing whitespace \$s ",
+ " world!${TQ})",
+ " println(",
+ " ${TQ}This line has trailing whitespace \${s} ",
+ " world!${TQ})",
+ " println(",
+ " ${TQ}This line has trailing whitespace \$ ",
+ " world!${TQ})",
"}",
"")
.joinToString("\n")
@@ -2665,7 +2834,8 @@ class FormatterTest {
fun `Consecutive line breaks in multiline strings are preserved`() =
assertFormatted(
"""
- |val x = $TQ
+ |val x =
+ | $TQ
|
|
|
@@ -2730,7 +2900,7 @@ class FormatterTest {
fun `handle for loops with long dot chains`() =
assertFormatted(
"""
- |-----------------------------------
+ |///////////////////////////////////
|fun f(a: Node<Int>) {
| for (child in node.next.data()) {
| println(child)
@@ -2753,7 +2923,7 @@ class FormatterTest {
fun `when two lambdas following a call, indent the lambda properly`() =
assertFormatted(
"""
- |----------------------------
+ |////////////////////////////
|fun f() {
| doIt()
| .apply {
@@ -2772,7 +2942,7 @@ class FormatterTest {
fun `when two lambdas following a field, indent the lambda properly`() =
assertFormatted(
"""
- |----------------------------
+ |////////////////////////////
|fun f() {
| field
| .apply {
@@ -2806,7 +2976,7 @@ class FormatterTest {
fun `keep last expression in qualified indented`() =
assertFormatted(
"""
- |----------------------------
+ |////////////////////////////
|fun f() {
| Stuff()
| .doIt(
@@ -2824,7 +2994,7 @@ class FormatterTest {
fun `properly place lambda arguments into blocks`() =
assertFormatted(
"""
- |-----------------------
+ |///////////////////////
|fun f() {
| foo {
| red.orange.yellow()
@@ -2842,7 +3012,7 @@ class FormatterTest {
fun `properly handle one statement lambda with comment`() =
assertFormatted(
"""
- |-----------------------
+ |///////////////////////
|fun f() {
| foo {
| // this is a comment
@@ -2873,7 +3043,7 @@ class FormatterTest {
fun `properly handle one statement lambda with comment after body statements`() =
assertFormatted(
"""
- |-----------------------
+ |///////////////////////
|fun f() {
| foo {
| red.orange.yellow()
@@ -2906,7 +3076,7 @@ class FormatterTest {
fun `try to keep expression in the same line until the first lambda`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|fun f() {
| foo.bar.bar?.let {
| a()
@@ -2931,7 +3101,7 @@ class FormatterTest {
fun `different indentation in chained calls`() =
assertFormatted(
"""
- |---------------------------
+ |///////////////////////////
|fun f() {
| fooDdoIt(
| foo1, foo2, foo3)
@@ -2949,7 +3119,7 @@ class FormatterTest {
fun `always add a conditional break for a lambda which is not last`() =
assertFormatted(
"""
- |--------------------
+ |////////////////////
|fun f() {
| foofoo
| .doIt {
@@ -3010,7 +3180,7 @@ class FormatterTest {
fun `handle function references`() =
assertFormatted(
"""
- |--------------------------------
+ |////////////////////////////////
|fun f(a: List<Int>) {
| a.forEach(::println)
| a.map(Int::toString)
@@ -3069,7 +3239,7 @@ class FormatterTest {
fun `no newlines after annotations if entire expr fits in one line`() =
assertFormatted(
"""
- |-----------------------------------------------
+ |///////////////////////////////////////////////
|@Px @Px fun f(): Int = 5
|
|@Px
@@ -3128,7 +3298,7 @@ class FormatterTest {
fun `no newlines after annotations on properties if entire expression fits in one line`() =
assertFormatted(
"""
- |--------------------------------------------
+ |////////////////////////////////////////////
|@Suppress("UnsafeCast")
|val ClassA.methodA
| get() = foo as Bar
@@ -3140,7 +3310,7 @@ class FormatterTest {
fun `when annotations cause line breaks, and constant has no type dont break before value`() =
assertFormatted(
"""
- |----------------------------------------------------------
+ |//////////////////////////////////////////////////////////
|object Foo {
| @LongLongLongLongAnnotation
| @LongLongLongLongLongAnnotation
@@ -3374,7 +3544,7 @@ class FormatterTest {
fun `handle top level constants`() =
assertFormatted(
"""
- |-----------------------------
+ |/////////////////////////////
|val a = 5
|
|const val b = "a"
@@ -3435,7 +3605,7 @@ class FormatterTest {
fun `handle extension methods with very long names`() =
assertFormatted(
"""
- |------------------------------------------
+ |//////////////////////////////////////////
|fun LongReceiverNameThatRequiresBreaking
| .doIt() {}
|
@@ -3465,9 +3635,9 @@ class FormatterTest {
.trimMargin())
@Test
- fun `handle file annotations`() =
- assertFormatted(
- """
+ fun `handle file annotations`() {
+ assertFormatted(
+ """
|@file:JvmName("DifferentName")
|
|package com.somecompany.example
@@ -3478,7 +3648,53 @@ class FormatterTest {
| val a = example2("and 1")
|}
|"""
- .trimMargin())
+ .trimMargin())
+
+ assertFormatted(
+ """
+ |@file:JvmName("DifferentName") // Comment
+ |
+ |package com.somecompany.example
+ |
+ |import com.somecompany.example2
+ |
+ |class Foo {
+ | val a = example2("and 1")
+ |}
+ |"""
+ .trimMargin())
+
+ assertFormatted(
+ """
+ |@file:JvmName("DifferentName")
+ |
+ |// Comment
+ |
+ |package com.somecompany.example
+ |
+ |import com.somecompany.example2
+ |
+ |class Foo {
+ | val a = example2("and 1")
+ |}
+ |"""
+ .trimMargin())
+
+ assertFormatted(
+ """
+ |@file:JvmName("DifferentName")
+ |
+ |// Comment
+ |package com.somecompany.example
+ |
+ |import com.somecompany.example2
+ |
+ |class Foo {
+ | val a = example2("and 1")
+ |}
+ |"""
+ .trimMargin())
+ }
@Test
fun `handle init block`() =
@@ -3512,7 +3728,7 @@ class FormatterTest {
fun `handle property delegation with type and breaks`() =
assertFormatted(
"""
- |---------------------------------
+ |/////////////////////////////////
|val importantValue: Int by lazy {
| 1 + 1
|}
@@ -3602,7 +3818,7 @@ class FormatterTest {
fun `handle casting with breaks`() =
assertFormatted(
"""
- |-----------------------
+ |///////////////////////
|fun castIt(
| something: Any
|) {
@@ -4051,7 +4267,7 @@ class FormatterTest {
fun `handle multi line one statement lambda`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|fun f() {
| a {
| println(foo.bar.boom)
@@ -4078,7 +4294,7 @@ class FormatterTest {
fun `properly break fully qualified nested user types`() =
assertFormatted(
"""
- |-------------------------------------------------------
+ |///////////////////////////////////////////////////////
|val complicated:
| com.example.interesting.SomeType<
| com.example.interesting.SomeType<Int, Nothing>,
@@ -4125,7 +4341,7 @@ class FormatterTest {
fun `handle multi line lambdas with explicit args`() =
assertFormatted(
"""
- |--------------------
+ |////////////////////
|fun f() {
| a { (x, y) ->
| x + y
@@ -4177,7 +4393,7 @@ class FormatterTest {
fun `handle break of lambda args per line with indentation`() =
assertFormatted(
"""
- |-----------
+ |///////////
|fun f() {
| a() {
| arg1,
@@ -4203,7 +4419,7 @@ class FormatterTest {
fun `handle trailing comma in lambda`() =
assertFormatted(
"""
- |-----------
+ |///////////
|fun f() {
| a() {
| arg1,
@@ -4222,7 +4438,7 @@ class FormatterTest {
fun `break before Elvis operator`() =
assertFormatted(
"""
- |--------------------------------------------------
+ |//////////////////////////////////////////////////
|fun f() {
| someObject
| .someMethodReturningCollection()
@@ -4235,10 +4451,56 @@ class FormatterTest {
deduceMaxWidth = true)
@Test
+ fun `chain of Elvis operator`() =
+ assertFormatted(
+ """
+ |///////////////////////////
+ |fun f() {
+ | return option1()
+ | ?: option2()
+ | ?: option3()
+ | ?: option4()
+ | ?: option5()
+ |}
+ |"""
+ .trimMargin(),
+ deduceMaxWidth = true)
+
+ @Test
+ fun `Elvis operator mixed with plus operator breaking on plus`() =
+ assertFormatted(
+ """
+ |////////////////////////
+ |fun f() {
+ | return option1()
+ | ?: option2() +
+ | option3()
+ | ?: option4() +
+ | option5()
+ |}
+ |"""
+ .trimMargin(),
+ deduceMaxWidth = true)
+
+ @Test
+ fun `Elvis operator mixed with plus operator breaking on elvis`() =
+ assertFormatted(
+ """
+ |/////////////////////////////////
+ |fun f() {
+ | return option1()
+ | ?: option2() + option3()
+ | ?: option4() + option5()
+ |}
+ |"""
+ .trimMargin(),
+ deduceMaxWidth = true)
+
+ @Test
fun `handle comments in the middle of calling chain`() =
assertFormatted(
"""
- |---------------------------
+ |///////////////////////////
|fun f() {
| someObject
| .letsDoIt()
@@ -4299,6 +4561,7 @@ class FormatterTest {
| TRUE,
| FALSE,
| FILE_NOT_FOUND;
+ |
| fun isGood(): Boolean {
| return true
| }
@@ -4345,8 +4608,7 @@ class FormatterTest {
fun `handle empty enum`() =
assertFormatted(
"""
- |enum class YTho {
- |}
+ |enum class YTho {}
|"""
.trimMargin())
@@ -4387,6 +4649,37 @@ class FormatterTest {
}
@Test
+ fun `empty enum with semicolons`() {
+ assertThatFormatting(
+ """
+ |enum class Empty {
+ | ;
+ |}
+ |"""
+ .trimMargin())
+ .isEqualTo(
+ """
+ |enum class Empty {}
+ |"""
+ .trimMargin())
+
+ assertThatFormatting(
+ """
+ |enum class Empty {
+ | ;
+ | ;
+ | ;
+ |}
+ |"""
+ .trimMargin())
+ .isEqualTo(
+ """
+ |enum class Empty {}
+ |"""
+ .trimMargin())
+ }
+
+ @Test
fun `semicolon is placed on next line when there's a trailing comma in an enum declaration`() =
assertFormatted(
"""
@@ -4401,6 +4694,68 @@ class FormatterTest {
.trimMargin())
@Test
+ fun `semicolon is removed from empty enum`() {
+ val code =
+ """
+ |enum class SingleSemi {
+ | ;
+ |}
+ |
+ |enum class MultSemi {
+ | // a
+ | ;
+ | // b
+ | ;
+ | // c
+ | ;
+ |}
+ |"""
+ .trimMargin()
+ val expected =
+ """
+ |enum class SingleSemi {}
+ |
+ |enum class MultSemi {
+ | // a
+ |
+ | // b
+ |
+ | // c
+ |
+ |}
+ |"""
+ .trimMargin()
+ assertThatFormatting(code).isEqualTo(expected)
+ }
+
+ @Test
+ fun `semicolon management in enum with no entries but other members`() {
+ val code =
+ """
+ |enum class Empty {
+ | ;
+ |
+ | fun f() {}
+ | ;
+ | fun g() {}
+ |}
+ |"""
+ .trimMargin()
+ val expected =
+ """
+ |enum class Empty {
+ | ;
+ |
+ | fun f() {}
+ |
+ | fun g() {}
+ |}
+ |"""
+ .trimMargin()
+ assertThatFormatting(code).isEqualTo(expected)
+ }
+
+ @Test
fun `handle varargs and spread operator`() =
assertFormatted(
"""
@@ -4415,7 +4770,7 @@ class FormatterTest {
fun `handle typealias`() =
assertFormatted(
"""
- |----------------------------------------------
+ |//////////////////////////////////////////////
|private typealias TextChangedListener =
| (string: String) -> Unit
|
@@ -4553,7 +4908,7 @@ class FormatterTest {
fun `handle annotations more`() =
assertFormatted(
"""
- |-------------------------------------------------
+ |/////////////////////////////////////////////////
|@Anno1
|@Anno2(param = Param1::class)
|@Anno3
@@ -4574,7 +4929,7 @@ class FormatterTest {
fun `annotated expressions`() =
assertFormatted(
"""
- |------------------------------------------------
+ |////////////////////////////////////////////////
|fun f() {
| @Suppress("MagicNumber") add(10) && add(20)
|
@@ -5011,7 +5366,7 @@ class FormatterTest {
fun `handle trailing commas (constructors)`() =
assertFormatted(
"""
- |--------------------
+ |////////////////////
|class Foo(
| a: Int,
|)
@@ -5033,7 +5388,7 @@ class FormatterTest {
fun `handle trailing commas (explicit constructors)`() =
assertFormatted(
"""
- |------------------------
+ |////////////////////////
|class Foo
|constructor(
| a: Int,
@@ -5058,7 +5413,7 @@ class FormatterTest {
fun `handle trailing commas (secondary constructors)`() =
assertFormatted(
"""
- |------------------------
+ |////////////////////////
|class Foo {
| constructor(
| a: Int,
@@ -5086,7 +5441,7 @@ class FormatterTest {
fun `handle trailing commas (function definitions)`() =
assertFormatted(
"""
- |------------------------
+ |////////////////////////
|fun <
| T,
|> foo() {}
@@ -5123,7 +5478,7 @@ class FormatterTest {
fun `handle trailing commas (function calls)`() =
assertFormatted(
"""
- |------------------------
+ |////////////////////////
|fun main() {
| foo(
| 3,
@@ -5164,7 +5519,7 @@ class FormatterTest {
fun `handle trailing commas (proprties)`() =
assertFormatted(
"""
- |--------------------------
+ |//////////////////////////
|val foo: String
| set(
| value,
@@ -5177,7 +5532,7 @@ class FormatterTest {
fun `handle trailing commas (higher-order functions)`() =
assertFormatted(
"""
- |--------------------------
+ |//////////////////////////
|fun foo(
| x:
| (
@@ -5192,7 +5547,7 @@ class FormatterTest {
fun `handle trailing commas (after lambda arg)`() =
assertFormatted(
"""
- |--------------------------
+ |//////////////////////////
|fun foo() {
| foo(
| { it },
@@ -5206,7 +5561,7 @@ class FormatterTest {
fun `handle trailing commas (other)`() =
assertFormatted(
"""
- |--------------------------
+ |//////////////////////////
|fun main() {
| val (
| x: Int,
@@ -5271,7 +5626,7 @@ class FormatterTest {
fun `assignment of a scoping function`() =
assertFormatted(
"""
- |----------------------------
+ |////////////////////////////
|val foo = coroutineScope {
| foo()
| //
@@ -5287,6 +5642,11 @@ class FormatterTest {
| //
|}
|
+ |fun foo() = scope label@{
+ | foo()
+ | //
+ |}
+ |
|fun foo() =
| coroutineScope { x ->
| foo()
@@ -5294,6 +5654,12 @@ class FormatterTest {
| }
|
|fun foo() =
+ | coroutineScope label@{
+ | foo()
+ | //
+ | }
+ |
+ |fun foo() =
| Runnable @Px {
| foo()
| //
@@ -5312,7 +5678,7 @@ class FormatterTest {
fun `top level properties with other types preserve newline spacing`() {
assertFormatted(
"""
- |---------------------------------
+ |/////////////////////////////////
|fun something() {
| println("hi")
|}
@@ -5486,7 +5852,7 @@ class FormatterTest {
assertThatFormatting(code).isEqualTo(code)
}
- // Regression test against https://github.com/facebookincubator/ktfmt/issues/243
+ // Regression test against https://github.com/facebook/ktfmt/issues/243
@Test
fun `regression test against Issue 243`() {
val code =
@@ -5549,10 +5915,217 @@ class FormatterTest {
.trimMargin())
@Test
+ fun `lambda with only comments`() {
+ assertFormatted(
+ """
+ |val a = { /* do nothing */ }
+ |val b = { /* do nothing */ /* also do nothing */ }
+ |val c = { -> /* do nothing */ }
+ |val d = { _ -> /* do nothing */ }
+ |private val e = Runnable {
+ | // do nothing
+ |}
+ |private val f: () -> Unit = {
+ | // no-op
+ |}
+ |private val g: () -> Unit = { /* no-op */ }
+ |"""
+ .trimMargin())
+
+ assertFormatted(
+ """
+ |//////////////////////////////
+ |val a = { /* do nothing */ }
+ |val b =
+ | { /* do nothing */ /* also do nothing */
+ | }
+ |val c = { -> /* do nothing */
+ |}
+ |val d =
+ | { _ -> /* do nothing */
+ | }
+ |private val e = Runnable {
+ | // do nothing
+ |}
+ |private val f: () -> Unit = {
+ | // no-op
+ |}
+ |private val g: () -> Unit =
+ | { /* no-op */
+ | }
+ |"""
+ .trimMargin(),
+ deduceMaxWidth = true)
+ }
+
+ @Test
+ fun `lambda block with single and multiple statements`() =
+ assertFormatted(
+ """
+ |private val a = Runnable {
+ | foo()
+ | TODO("implement me")
+ |}
+ |
+ |private val b = Runnable { TODO("implement me") }
+ |
+ |private val c: () -> Unit = {
+ | foo()
+ | TODO("implement me")
+ |}
+ |
+ |private val d: () -> Unit = { TODO("implement me") }
+ |"""
+ .trimMargin())
+
+ @Test
+ fun `lambda block with comments and statements mix`() =
+ assertFormatted(
+ """
+ |private val a = Runnable {
+ | // no-op
+ | TODO("implement me")
+ |}
+ |
+ |private val b = Runnable {
+ | TODO("implement me")
+ | // no-op
+ |}
+ |
+ |private val c: () -> Unit = {
+ | /* no-op */ TODO("implement me")
+ |}
+ |
+ |private val d: () -> Unit = { ->
+ | /* no-op */ TODO("implement me")
+ |}
+ |
+ |private val e: (String, Int) -> Unit = { _, i -> foo(i) /* do nothing ... */ }
+ |"""
+ .trimMargin())
+
+ @Test
+ fun `lambda block with comments and with statements have same formatting treatment`() =
+ assertFormatted(
+ """
+ |private val a = Runnable { /* no-op */ }
+ |private val A = Runnable { TODO("...") }
+ |
+ |private val b = Runnable {
+ | /* no-op 1 */
+ | /* no-op 2 */
+ |}
+ |private val B = Runnable {
+ | TODO("no-op")
+ | TODO("no-op")
+ |}
+ |
+ |private val c: () -> Unit = {
+ | /* no-op */
+ |}
+ |private val C: () -> Unit = { TODO("...") }
+ |
+ |private val d: () -> Unit = {
+ | /*.*/
+ | /* do nothing ... */
+ |}
+ |private val D: () -> Unit = {
+ | foo()
+ | TODO("implement me")
+ |}
+ |"""
+ .trimMargin())
+
+ @Test
+ fun `last parameter with comment and with statements have same formatting treatment`() {
+ assertFormatted(
+ """
+ |private val a =
+ | call(param) {
+ | // no-op
+ | /* comment */
+ | }
+ |private val A =
+ | call(param) {
+ | a.run()
+ | TODO("implement me")
+ | }
+ |
+ |private val b = call(param) { /* no-op */ }
+ |private val B = call(param) { TODO("implement me") }
+ |
+ |private val c = firstCall().prop.call(param) { /* no-op */ }
+ |private val C = firstCall().prop.call(param) { TODO("implement me") }
+ |"""
+ .trimMargin())
+
+ assertFormatted(
+ """
+ |////////////////////////////////////////
+ |private val a =
+ | firstCall().prop.call(
+ | mySuperInterestingParameter) {
+ | /* no-op */
+ | }
+ |private val A =
+ | firstCall().prop.call(
+ | mySuperInterestingParameter) {
+ | TODO("...")
+ | }
+ |
+ |fun b() {
+ | myProp.funCall(param) { /* 12345 */ }
+ | myProp.funCall(param) { TODO("123") }
+ |
+ | myProp.funCall(param) { /* 123456 */ }
+ | myProp.funCall(param) { TODO("1234") }
+ |
+ | myProp.funCall(param) { /* 1234567 */
+ | }
+ | myProp.funCall(param) {
+ | TODO("12345")
+ | }
+ |
+ | myProp.funCall(param) { /* 12345678 */
+ | }
+ | myProp.funCall(param) {
+ | TODO("123456")
+ | }
+ |
+ | myProp.funCall(
+ | param) { /* 123456789 */
+ | }
+ | myProp.funCall(param) {
+ | TODO("1234567")
+ | }
+ |
+ | myProp.funCall(
+ | param) { /* very_very_long_comment_that_should_go_on_its_own_line */
+ | }
+ | myProp.funCall(param) {
+ | TODO(
+ | "_a_very_long_comment_that_should_go_on_its_own_line")
+ | }
+ |}
+ |
+ |private val c =
+ | firstCall().prop.call(param) {
+ | /* no-op */
+ | }
+ |private val C =
+ | firstCall().prop.call(param) {
+ | TODO("...")
+ | }
+ |"""
+ .trimMargin(),
+ deduceMaxWidth = true)
+ }
+
+ @Test
fun `chaining - many dereferences`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.red.orange.yellow
| .green
| .blue
@@ -5569,7 +6142,7 @@ class FormatterTest {
fun `chaining - many dereferences, fit on one line`() =
assertFormatted(
"""
- |---------------------------------------------------------------------------
+ |///////////////////////////////////////////////////////////////////////////
|rainbow.red.orange.yellow.green.blue.indigo.violet.cyan.magenta.key
|"""
.trimMargin(),
@@ -5579,7 +6152,7 @@ class FormatterTest {
fun `chaining - many dereferences, one invocation at end`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.red.orange.yellow
| .green
| .blue
@@ -5597,7 +6170,7 @@ class FormatterTest {
fun `chaining - many dereferences, one invocation at end, fit on one line`() =
assertFormatted(
"""
- |---------------------------------------------------------------------------
+ |///////////////////////////////////////////////////////////////////////////
|rainbow.red.orange.yellow.green.blue.indigo.violet.cyan.magenta.key.build()
|"""
.trimMargin(),
@@ -5607,7 +6180,7 @@ class FormatterTest {
fun `chaining - many dereferences, two invocations at end`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.red.orange.yellow
| .green
| .blue
@@ -5626,7 +6199,7 @@ class FormatterTest {
fun `chaining - many dereferences, one invocation in the middle`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.red.orange.yellow
| .green
| .blue
@@ -5644,7 +6217,7 @@ class FormatterTest {
fun `chaining - many dereferences, two invocations in the middle`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.red.orange.yellow
| .green
| .blue
@@ -5663,7 +6236,7 @@ class FormatterTest {
fun `chaining - many dereferences, one lambda at end`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.red.orange.yellow
| .green
| .blue
@@ -5681,7 +6254,7 @@ class FormatterTest {
fun `chaining - many dereferences, one short lambda at end`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.red.orange.yellow
| .green
| .blue
@@ -5699,7 +6272,7 @@ class FormatterTest {
fun `chaining - many dereferences, one multiline lambda at end`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.red.orange.yellow
| .green
| .blue
@@ -5720,7 +6293,7 @@ class FormatterTest {
fun `chaining - many dereferences, one short lambda in the middle`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.red.orange.yellow
| .green
| .blue
@@ -5738,7 +6311,7 @@ class FormatterTest {
fun `chaining - many dereferences, one multiline lambda in the middle`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.red.orange.yellow
| .green
| .blue
@@ -5759,7 +6332,7 @@ class FormatterTest {
fun `chaining - many dereferences, one multiline lambda in the middle, remainder could fit on one line`() =
assertFormatted(
"""
- |-----------------------------------------------------------------------------------------
+ |/////////////////////////////////////////////////////////////////////////////////////////
|rainbow.red.orange.yellow.green.blue
| .z {
| it
@@ -5778,7 +6351,7 @@ class FormatterTest {
fun `chaining - many dereferences, one multiline lambda and two invocations in the middle, remainder could fit on one line`() =
assertFormatted(
"""
- |-----------------------------------------------------------------------------------------
+ |/////////////////////////////////////////////////////////////////////////////////////////
|rainbow.red.orange.yellow.green.blue
| .z {
| it
@@ -5799,7 +6372,7 @@ class FormatterTest {
fun `chaining - many dereferences, one lambda and invocation at end`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.red.orange.yellow
| .green
| .blue
@@ -5818,7 +6391,7 @@ class FormatterTest {
fun `chaining - many dereferences, one multiline lambda and invocation at end`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.red.orange.yellow
| .green
| .blue
@@ -5840,7 +6413,7 @@ class FormatterTest {
fun `chaining - many dereferences, one invocation and lambda at end`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.red.orange.yellow
| .green
| .blue
@@ -5859,7 +6432,7 @@ class FormatterTest {
fun `chaining - many dereferences, one short lambda and invocation in the middle`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.red.orange.yellow
| .green
| .blue
@@ -5878,7 +6451,7 @@ class FormatterTest {
fun `chaining - many dereferences, invocation and one short lambda in the middle`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.red.orange.yellow
| .green
| .blue
@@ -5897,7 +6470,7 @@ class FormatterTest {
fun `chaining - many dereferences, starting with this`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|this.red.orange.yellow
| .green
| .blue
@@ -5914,7 +6487,7 @@ class FormatterTest {
fun `chaining - many dereferences, starting with this, one invocation at end`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|this.red.orange.yellow
| .green
| .blue
@@ -5932,7 +6505,7 @@ class FormatterTest {
fun `chaining - many dereferences, starting with super`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|super.red.orange.yellow
| .green
| .blue
@@ -5949,7 +6522,7 @@ class FormatterTest {
fun `chaining - many dereferences, starting with super, one invocation at end`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|super.red.orange.yellow
| .green
| .blue
@@ -5967,7 +6540,7 @@ class FormatterTest {
fun `chaining - many dereferences, starting with short variable`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|z123.red.orange.yellow
| .green
| .blue
@@ -5984,7 +6557,7 @@ class FormatterTest {
fun `chaining - many dereferences, starting with short variable, one invocation at end`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|z123.red.orange.yellow
| .green
| .blue
@@ -6002,7 +6575,7 @@ class FormatterTest {
fun `chaining - many dereferences, starting with short variable and lambda, invocation at end`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|z12.z { it }
| .red
| .orange
@@ -6023,7 +6596,7 @@ class FormatterTest {
fun `chaining - many dereferences, starting with this and lambda, invocation at end`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|this.z { it }
| .red
| .orange
@@ -6044,7 +6617,7 @@ class FormatterTest {
fun `chaining - many invocations`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.a().b().c()
|"""
.trimMargin(),
@@ -6054,7 +6627,7 @@ class FormatterTest {
fun `chaining - many invocations, with multiline lambda at end`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.a().b().c().zz {
| it
| it
@@ -6067,7 +6640,7 @@ class FormatterTest {
fun `chaining - many dereferences, starting type name`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|com.sky.Rainbow.red
| .orange
| .yellow
@@ -6086,7 +6659,7 @@ class FormatterTest {
fun `chaining - many invocations, starting with short variable, lambda at end`() =
assertFormatted(
"""
- |-------------
+ |/////////////
|z12.shine()
| .bright()
| .z { it }
@@ -6098,7 +6671,7 @@ class FormatterTest {
fun `chaining - start with invocation, lambda at end`() =
assertFormatted(
"""
- |---------------------
+ |/////////////////////
|getRainbow(
| aa, bb, cc)
| .z { it }
@@ -6110,7 +6683,7 @@ class FormatterTest {
fun `chaining - many invocations, start with lambda`() =
assertFormatted(
"""
- |---------------------
+ |/////////////////////
|z { it }
| .shine()
| .bright()
@@ -6122,7 +6695,7 @@ class FormatterTest {
fun `chaining - start with type name, end with invocation`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|com.sky.Rainbow
| .colorFactory
| .build()
@@ -6134,7 +6707,7 @@ class FormatterTest {
fun `chaining (indentation) - multiline lambda`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.z {
| it
| it
@@ -6147,7 +6720,7 @@ class FormatterTest {
fun `chaining (indentation) - multiline lambda with trailing dereferences`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow
| .z {
| it
@@ -6162,7 +6735,7 @@ class FormatterTest {
fun `chaining (indentation) - multiline lambda with long name`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow
| .someLongLambdaName {
| it
@@ -6176,7 +6749,7 @@ class FormatterTest {
fun `chaining (indentation) - multiline lambda with long name and trailing dereferences`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow
| .someLongLambdaName {
| it
@@ -6191,7 +6764,7 @@ class FormatterTest {
fun `chaining (indentation) - multiline lambda with prefix`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.red.z {
| it
| it
@@ -6204,7 +6777,7 @@ class FormatterTest {
fun `chaining (indentation) - multiline lambda with prefix, forced to next line`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.red.orange.yellow
| .longLambdaName {
| it
@@ -6218,7 +6791,7 @@ class FormatterTest {
fun `chaining (indentation) - multiline lambda with prefix, forced to next line with another expression`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.red.orange.yellow
| .key
| .longLambdaName {
@@ -6233,7 +6806,7 @@ class FormatterTest {
fun `chaining (indentation) - multiline arguments`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.shine(
| infrared,
| ultraviolet,
@@ -6246,7 +6819,7 @@ class FormatterTest {
fun `chaining (indentation) - multiline arguments with trailing dereferences`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow
| .shine(
| infrared,
@@ -6261,7 +6834,7 @@ class FormatterTest {
fun `chaining (indentation) - multiline arguments, forced to next line`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.red.orange.yellow
| .shine(
| infrared,
@@ -6275,7 +6848,7 @@ class FormatterTest {
fun `chaining (indentation) - multiline arguments, forced to next line with another expression`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.red.orange.yellow
| .key
| .shine(
@@ -6290,7 +6863,7 @@ class FormatterTest {
fun `chaining (indentation) - multiline arguments, forced to next line with another expression, with trailing dereferences`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow.red.orange.yellow
| .key
| .shine(
@@ -6306,7 +6879,7 @@ class FormatterTest {
fun `chaining (indentation) - multiline arguments, with trailing invocation`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow
| .shine(
| infrared,
@@ -6321,7 +6894,7 @@ class FormatterTest {
fun `chaining (indentation) - multiline arguments, with trailing lambda`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|rainbow
| .shine(
| infrared,
@@ -6336,7 +6909,7 @@ class FormatterTest {
fun `chaining (indentation) - multiline arguments, prefixed with super, with trailing invocation`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|super.shine(
| infrared,
| ultraviolet,
@@ -6350,7 +6923,7 @@ class FormatterTest {
fun `chaining (indentation) - multiline arguments, starting with short variable, with trailing invocation`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|z12.shine(
| infrared,
| ultraviolet,
@@ -6364,7 +6937,7 @@ class FormatterTest {
fun `chaining (indentation) - start with multiline arguments`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|getRainbow(
| infrared,
| ultraviolet,
@@ -6377,7 +6950,7 @@ class FormatterTest {
fun `chaining (indentation) - start with multiline arguments, with trailing invocation`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|getRainbow(
| infrared,
| ultraviolet,
@@ -6438,8 +7011,18 @@ class FormatterTest {
fun `function call following long multiline string`() =
assertFormatted(
"""
- |--------------------------------
- |fun f() {
+ |////////////////////////////////
+ |fun stringFitsButNotMethod() {
+ | val str1 =
+ | $TQ Some string $TQ
+ | .trimIndent()
+ |
+ | val str2 =
+ | $TQ Some string $TQ
+ | .trimIndent(someArg)
+ |}
+ |
+ |fun stringTooLong() {
| val str1 =
| $TQ
| Some very long string that might mess things up
@@ -6460,7 +7043,7 @@ class FormatterTest {
fun `array-literal in annotation`() =
assertFormatted(
"""
- |--------------------------------
+ |////////////////////////////////
|@Anno(
| array =
| [
@@ -6495,6 +7078,319 @@ class FormatterTest {
.trimMargin(),
deduceMaxWidth = true)
+ @Test
+ fun `force blank line between class members`() {
+ val code =
+ """
+ |class Foo {
+ | val x = 0
+ | fun foo() {}
+ | class Bar {}
+ | enum class Enum {
+ | A {
+ | val x = 0
+ | fun foo() {}
+ | };
+ | abstract fun foo(): Unit
+ | }
+ |}
+ |"""
+ .trimMargin()
+
+ val expected =
+ """
+ |class Foo {
+ | val x = 0
+ |
+ | fun foo() {}
+ |
+ | class Bar {}
+ |
+ | enum class Enum {
+ | A {
+ | val x = 0
+ |
+ | fun foo() {}
+ | };
+ |
+ | abstract fun foo(): Unit
+ | }
+ |}
+ |"""
+ .trimMargin()
+
+ assertThatFormatting(code).isEqualTo(expected)
+ }
+
+ @Test
+ fun `preserve blank line between class members between properties`() {
+ val code =
+ """
+ |class Foo {
+ | val x = 0
+ | val x = 0
+ |
+ | val x = 0
+ |}
+ |"""
+ .trimMargin()
+
+ assertThatFormatting(code).isEqualTo(code)
+ }
+
+ @Test
+ fun `force blank line between class members preserved between properties with accessors`() {
+ val code =
+ """
+ |class Foo {
+ | val _x = 0
+ | val x = 0
+ | private get
+ | val y = 0
+ |}
+ |
+ |class Foo {
+ | val _x = 0
+ | val x = 0
+ | private set
+ | val y = 0
+ |}
+ |"""
+ .trimMargin()
+
+ val expected =
+ """
+ |class Foo {
+ | val _x = 0
+ | val x = 0
+ | private get
+ |
+ | val y = 0
+ |}
+ |
+ |class Foo {
+ | val _x = 0
+ | val x = 0
+ | private set
+ |
+ | val y = 0
+ |}
+ |"""
+ .trimMargin()
+
+ assertThatFormatting(code).isEqualTo(expected)
+ }
+
+ @Test
+ fun `context receivers`() {
+ val code =
+ """
+ |context(Something)
+ |
+ |class A {
+ | context(
+ | // Test comment.
+ | Logger, Raise<Error>)
+ |
+ | @SomeAnnotation
+ |
+ | fun doNothing() {}
+ |
+ | context(SomethingElse)
+ |
+ | private class NestedClass {}
+ |}
+ |"""
+ .trimMargin()
+
+ val expected =
+ """
+ |context(Something)
+ |class A {
+ | context(
+ | // Test comment.
+ | Logger,
+ | Raise<Error>)
+ | @SomeAnnotation
+ | fun doNothing() {}
+ |
+ | context(SomethingElse)
+ | private class NestedClass {}
+ |}
+ |"""
+ .trimMargin()
+
+ assertThatFormatting(code).isEqualTo(expected)
+ }
+
+ @Test
+ fun `trailing comment after function in class`() =
+ assertFormatted(
+ """
+ |class Host {
+ | fun fooBlock() {
+ | return
+ | } // Trailing after fn
+ | // Hanging after fn
+ |
+ | // End of class
+ |}
+ |
+ |class Host {
+ | fun fooExpr() = 0 // Trailing after fn
+ | // Hanging after fn
+ |
+ | // End of class
+ |}
+ |
+ |class Host {
+ | constructor() {} // Trailing after fn
+ | // Hanging after fn
+ |
+ | // End of class
+ |}
+ |
+ |class Host
+ |// Primary constructor
+ |constructor() // Trailing after fn
+ | // Hanging after fn
+ |{
+ | // End of class
+ |}
+ |
+ |class Host {
+ | fun fooBlock() {
+ | return
+ | }
+ |
+ | // Between elements
+ |
+ | fun fooExpr() = 0
+ |
+ | // Between elements
+ |
+ | fun fooBlock() {
+ | return
+ | }
+ |}
+ |"""
+ .trimMargin())
+
+ @Test
+ fun `trailing comment after function top-level`() {
+ assertFormatted(
+ """
+ |fun fooBlock() {
+ | return
+ |} // Trailing after fn
+ |// Hanging after fn
+ |
+ |// End of file
+ |"""
+ .trimMargin())
+
+ assertFormatted(
+ """
+ |fun fooExpr() = 0 // Trailing after fn
+ |// Hanging after fn
+ |
+ |// End of file
+ |"""
+ .trimMargin())
+
+ assertFormatted(
+ """
+ |fun fooBlock() {
+ | return
+ |}
+ |
+ |// Between elements
+ |
+ |fun fooExpr() = 0
+ |
+ |// Between elements
+ |
+ |fun fooBlock() {
+ | return
+ |}
+ |"""
+ .trimMargin())
+ }
+
+ @Test
+ fun `line break on base class`() =
+ assertFormatted(
+ """
+ |///////////////////////////
+ |class Basket<T>() :
+ | WovenObject {
+ | // some body
+ |}
+ |"""
+ .trimMargin(),
+ deduceMaxWidth = true)
+
+ @Test
+ fun `line break on type specifier`() =
+ assertFormatted(
+ """
+ |///////////////////////////
+ |class Basket<T>() where
+ |T : Fruit {
+ | // some body
+ |}
+ |"""
+ .trimMargin(),
+ deduceMaxWidth = true)
+
+ @Test
+ fun `don't crash on empty enum with semicolons`() {
+ assertFormatted(
+ """
+ |///////////////////////////
+ |enum class Foo {
+ | ;
+ |
+ | fun foo(): Unit
+ |}
+ |"""
+ .trimMargin(),
+ deduceMaxWidth = true)
+
+ assertFormatted(
+ """
+ |///////////////////////////
+ |enum class Foo {
+ | ;
+ |
+ | companion object Bar
+ |}
+ |"""
+ .trimMargin(),
+ deduceMaxWidth = true)
+
+ assertThatFormatting(
+ """
+ |enum class Foo {
+ | ;
+ | ;
+ | ;
+ |
+ | fun foo(): Unit
+ |}
+ |"""
+ .trimMargin())
+ .isEqualTo(
+ """
+ |enum class Foo {
+ | ;
+ |
+ | fun foo(): Unit
+ |}
+ |"""
+ .trimMargin())
+ }
+
companion object {
/** Triple quotes, useful to use within triple-quoted strings. */
private const val TQ = "\"\"\""
diff --git a/core/src/test/java/com/facebook/ktfmt/format/GoogleStyleFormatterKtTest.kt b/core/src/test/java/com/facebook/ktfmt/format/GoogleStyleFormatterKtTest.kt
index b09fb48..f5440bd 100644
--- a/core/src/test/java/com/facebook/ktfmt/format/GoogleStyleFormatterKtTest.kt
+++ b/core/src/test/java/com/facebook/ktfmt/format/GoogleStyleFormatterKtTest.kt
@@ -105,14 +105,14 @@ class GoogleStyleFormatterKtTest {
}
@Test
- fun `class params are placed each in their own line`() =
+ fun `class value params are placed each in their own line`() =
assertFormatted(
"""
- |-----------------------------------------
+ |/////////////////////////////////////////
|class Foo(
| a: Int,
| var b: Double,
- | val c: String
+ | val c: String,
|) {
| //
|}
@@ -120,13 +120,13 @@ class GoogleStyleFormatterKtTest {
|class Foo(
| a: Int,
| var b: Double,
- | val c: String
+ | val c: String,
|)
|
|class Foo(
| a: Int,
| var b: Int,
- | val c: Int
+ | val c: Int,
|) {
| //
|}
@@ -134,7 +134,7 @@ class GoogleStyleFormatterKtTest {
|class Bi(
| a: Int,
| var b: Int,
- | val c: Int
+ | val c: Int,
|) {
| //
|}
@@ -148,14 +148,66 @@ class GoogleStyleFormatterKtTest {
deduceMaxWidth = true)
@Test
+ fun `class type params are placed each in their own line`() =
+ assertFormatted(
+ """
+ |////////////////////////////////////
+ |class Foo<
+ | TypeA : Int,
+ | TypeC : String,
+ |> {
+ | // Class name + type params too long for one line
+ | // Type params could fit on one line but break
+ |}
+ |
+ |class Foo<
+ | TypeA : Int,
+ | TypeB : Double,
+ | TypeC : String,
+ |> {
+ | // Type params can't fit on one line
+ |}
+ |
+ |class Foo<
+ | TypeA : Int,
+ | TypeB : Double,
+ | TypeC : String,
+ |>
+ |
+ |class Foo<
+ | TypeA : Int,
+ | TypeB : Double,
+ | TypeC : String,
+ |>() {
+ | //
+ |}
+ |
+ |class Bi<
+ | TypeA : Int,
+ | TypeB : Double,
+ | TypeC : String,
+ |>(a: Int, var b: Int, val c: Int) {
+ | // TODO: Breaking the type param list
+ | // should propagate to the value param list
+ |}
+ |
+ |class C<A : Int, B : Int, C : Int> {
+ | // Class name + type params fit on one line
+ |}
+ |"""
+ .trimMargin(),
+ formattingOptions = Formatter.GOOGLE_FORMAT,
+ deduceMaxWidth = true)
+
+ @Test
fun `function params are placed each in their own line`() =
assertFormatted(
"""
- |-----------------------------------------
+ |/////////////////////////////////////////
|fun foo12(
| a: Int,
| var b: Double,
- | val c: String
+ | val c: String,
|) {
| //
|}
@@ -163,19 +215,19 @@ class GoogleStyleFormatterKtTest {
|fun foo12(
| a: Int,
| var b: Double,
- | val c: String
+ | val c: String,
|)
|
|fun foo12(
| a: Int,
| var b: Double,
- | val c: String
+ | val c: String,
|) = 5
|
|fun foo12(
| a: Int,
| var b: Int,
- | val c: Int
+ | val c: Int,
|) {
| //
|}
@@ -183,7 +235,7 @@ class GoogleStyleFormatterKtTest {
|fun bi12(
| a: Int,
| var b: Int,
- | val c: Int
+ | val c: Int,
|) {
| //
|}
@@ -200,18 +252,18 @@ class GoogleStyleFormatterKtTest {
fun `return type doesn't fit in one line`() =
assertFormatted(
"""
- |--------------------------------------------------
+ |//////////////////////////////////////////////////
|interface X {
| fun f(
| arg1: Arg1Type,
- | arg2: Arg2Type
+ | arg2: Arg2Type,
| ): Map<String, Map<String, Double>>? {
| //
| }
|
| fun functionWithGenericReturnType(
| arg1: Arg1Type,
- | arg2: Arg2Type
+ | arg2: Arg2Type,
| ): Map<String, Map<String, Double>>? {
| //
| }
@@ -225,12 +277,12 @@ class GoogleStyleFormatterKtTest {
fun `indent parameters after a break when there's a lambda afterwards`() =
assertFormatted(
"""
- |---------------------------
+ |///////////////////////////
|class C {
| fun method() {
| Foo.FooBar(
| param1,
- | param2
+ | param2,
| )
| .apply {
| //
@@ -247,7 +299,7 @@ class GoogleStyleFormatterKtTest {
fun `no forward propagation of breaks in call expressions (at trailing lambda)`() =
assertFormatted(
"""
- |--------------------------
+ |//////////////////////////
|fun test() {
| foo_bar_baz__zip<A>(b) {
| c
@@ -265,7 +317,7 @@ class GoogleStyleFormatterKtTest {
fun `forward propagation of breaks in call expressions (at value args)`() =
assertFormatted(
"""
- |----------------------
+ |//////////////////////
|fun test() {
| foo_bar_baz__zip<A>(
| b
@@ -290,7 +342,7 @@ class GoogleStyleFormatterKtTest {
fun `forward propagation of breaks in call expressions (at type args)`() =
assertFormatted(
"""
- |-------------------
+ |///////////////////
|fun test() {
| foo_bar_baz__zip<
| A
@@ -316,10 +368,9 @@ class GoogleStyleFormatterKtTest {
fun `expected indent in methods following single-line strings`() =
assertFormatted(
"""
- |-------------------------
- |"Hello %s".format(
- | someLongExpression
- |)
+ |/////////////////////////
+ |"Hello %s"
+ | .format(expression)
|"""
.trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
@@ -329,7 +380,7 @@ class GoogleStyleFormatterKtTest {
fun `forced break between multi-line strings and their selectors`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|val STRING =
| $TQ
| |foo
@@ -339,7 +390,7 @@ class GoogleStyleFormatterKtTest {
|val STRING =
| $TQ
| |foo
- | |----------------------------------$TQ
+ | |//////////////////////////////////$TQ
| .wouldntFit()
|
|val STRING =
@@ -357,14 +408,14 @@ class GoogleStyleFormatterKtTest {
fun `properly break fully qualified nested user types`() =
assertFormatted(
"""
- |-------------------------------------------------------
+ |///////////////////////////////////////////////////////
|val complicated:
| com.example.interesting.SomeType<
| com.example.interesting.SomeType<Int, Nothing>,
| com.example.interesting.SomeType<
| com.example.interesting.SomeType<Int, Nothing>,
- | Nothing
- | >
+ | Nothing,
+ | >,
| > =
| DUMMY
|"""
@@ -376,30 +427,30 @@ class GoogleStyleFormatterKtTest {
fun `don't one-line lambdas following argument breaks`() =
assertFormatted(
"""
- |------------------------------------------------------------------------
+ |////////////////////////////////////////////////////////////////////////
|class Foo : Bar() {
| fun doIt() {
| // don't break in lambda, no argument breaks found
| fruit.forEach { eat(it) }
|
- | // break in lambda, without comma
+ | // break in lambda, natural break
| fruit.forEach(
| someVeryLongParameterNameThatWillCauseABreak,
- | evenWithoutATrailingCommaOnTheParameterListSoLetsSeeIt
+ | evenWithoutATrailingCommaOnTheParameterListSoLetsSeeIt,
| ) {
| eat(it)
| }
|
- | // break in the lambda, with comma
+ | // break in the lambda, forced break
| fruit.forEach(
- | fromTheVine = true,
+ | fromTheVine = true //
| ) {
| eat(it)
| }
|
| // don't break in the inner lambda, as nesting doesn't respect outer levels
| fruit.forEach(
- | fromTheVine = true,
+ | fromTheVine = true //
| ) {
| fruit.forEach { eat(it) }
| }
@@ -407,21 +458,21 @@ class GoogleStyleFormatterKtTest {
| // don't break in the lambda, as breaks don't propagate
| fruit
| .onlyBananas(
- | fromTheVine = true,
+ | fromTheVine = true //
| )
| .forEach { eat(it) }
|
| // don't break in the inner lambda, as breaks don't propagate to parameters
| fruit.onlyBananas(
| fromTheVine = true,
- | processThem = { eat(it) },
+ | processThem = { eat(it) }, //
| ) {
| eat(it)
| }
|
| // don't break in the inner lambda, as breaks don't propagate to the body
| fruit.onlyBananas(
- | fromTheVine = true,
+ | fromTheVine = true //
| ) {
| val anon = { eat(it) }
| }
@@ -442,7 +493,7 @@ class GoogleStyleFormatterKtTest {
| foo(
| 123456789012345678901234567890,
| 123456789012345678901234567890,
- | 123456789012345678901234567890
+ | 123456789012345678901234567890,
| )
|}
|"""
@@ -469,7 +520,7 @@ class GoogleStyleFormatterKtTest {
| blablabl,
| blablabl,
| blablabl,
- | blabla
+ | blabla,
| )
| .show()
| }
@@ -507,7 +558,7 @@ class GoogleStyleFormatterKtTest {
| lambdaArgument = {
| step1()
| step2()
- | }
+ | },
| ) {
| it.doIt()
| }
@@ -543,7 +594,7 @@ class GoogleStyleFormatterKtTest {
| foo(
| 123456789012345678901234567890,
| b = 23456789012345678901234567890,
- | c = 3456789012345678901234567890
+ | c = 3456789012345678901234567890,
| )
|}
|"""
@@ -554,7 +605,7 @@ class GoogleStyleFormatterKtTest {
fun `Arguments are blocks`() =
assertFormatted(
"""
- |--------------------------------------------------
+ |//////////////////////////////////////////////////
|override fun visitProperty(property: KtProperty) {
| builder.sync(property)
| builder.block(ZERO) {
@@ -571,7 +622,7 @@ class GoogleStyleFormatterKtTest {
| typeConstraintList =
| property.typeConstraintList,
| delegate = property.delegate,
- | initializer = property.initializer
+ | initializer = property.initializer,
| )
| }
|}
@@ -584,7 +635,7 @@ class GoogleStyleFormatterKtTest {
fun `keep last expression in qualified indented`() =
assertFormatted(
"""
- |-----------------------
+ |///////////////////////
|fun f() {
| Stuff()
| .doIt(
@@ -630,7 +681,7 @@ class GoogleStyleFormatterKtTest {
| // Printing
| print()
| },
- | duration = duration
+ | duration = duration,
| )
|"""
.trimMargin(),
@@ -640,7 +691,7 @@ class GoogleStyleFormatterKtTest {
fun `breaking long binary operations`() =
assertFormatted(
"""
- |--------------------
+ |////////////////////
|fun foo() {
| val finalWidth =
| value1 +
@@ -652,7 +703,7 @@ class GoogleStyleFormatterKtTest {
| (1 + 2) +
| function(
| value7,
- | value8
+ | value8,
| ) +
| value9
|}
@@ -665,7 +716,7 @@ class GoogleStyleFormatterKtTest {
fun `handle casting with breaks`() =
assertFormatted(
"""
- |-------------------
+ |///////////////////
|fun castIt(
| something: Any
|) {
@@ -691,14 +742,16 @@ class GoogleStyleFormatterKtTest {
| something
| is
| PairList<
- | String, Int
+ | String,
+ | Int,
| >
| )
| doIt(
| something
| as
| PairList<
- | String, Int
+ | String,
+ | Int,
| >
| )
| println(
@@ -715,27 +768,27 @@ class GoogleStyleFormatterKtTest {
fun `line breaks in function arguments`() =
assertFormatted(
"""
- |--------------------------------------------------
+ |//////////////////////////////////////////////////
|fun f() {
| computeBreaks(
| javaOutput.commentsHelper,
| maxWidth,
- | Doc.State(+0, 0)
+ | Doc.State(+0, 0),
| )
| computeBreaks(
| output.commentsHelper,
| maxWidth,
- | State(0)
+ | State(0),
| )
| doc.computeBreaks(
| javaOutput.commentsHelper,
| maxWidth,
- | Doc.State(+0, 0)
+ | Doc.State(+0, 0),
| )
| doc.computeBreaks(
| output.commentsHelper,
| maxWidth,
- | State(0)
+ | State(0),
| )
|}
|"""
@@ -748,23 +801,23 @@ class GoogleStyleFormatterKtTest {
fun `different indentation in chained calls`() =
assertFormatted(
"""
- |----------------------
+ |//////////////////////
|fun f() {
| fooDdoIt(
| foo1,
| foo2,
- | foo3
+ | foo3,
| )
| foo.doIt(
| foo1,
| foo2,
- | foo3
+ | foo3,
| )
| foo
| .doIt(
| foo1,
| foo2,
- | foo3
+ | foo3,
| )
| .doThat()
|}
@@ -777,21 +830,21 @@ class GoogleStyleFormatterKtTest {
fun `a secondary constructor with many arguments passed to delegate`() =
assertFormatted(
"""
- |--------------------------------------------------
+ |//////////////////////////////////////////////////
|data class Foo {
| constructor(
| val number: Int,
| val name: String,
| val age: Int,
| val title: String,
- | val offspring: List<Foo>
+ | val offspring: List<Foo>,
| ) : this(
| number,
| name,
| age,
| title,
| offspring,
- | offspring
+ | offspring,
| )
|}
|"""
@@ -803,7 +856,7 @@ class GoogleStyleFormatterKtTest {
fun `a secondary constructor with no arguments passed to delegate`() =
assertFormatted(
"""
- |--------------------------------------------------
+ |//////////////////////////////////////////////////
|data class Foo {
| constructor() :
| this(
@@ -817,42 +870,42 @@ class GoogleStyleFormatterKtTest {
deduceMaxWidth = true)
@Test
- fun `handle trailing commas (function calls)`() =
+ fun `handle forced breaks in function calls`() =
assertFormatted(
"""
- |------------------------
+ |////////////////////////
|fun main() {
| foo(
- | 3,
+ | 3 //
| )
|
| foo<Int>(
- | 3,
+ | 3 //
| )
|
| foo<
- | Int,
+ | Int //
| >(
- | 3,
+ | 3 //
| )
|
| foo<Int>(
| "asdf",
- | "asdf"
+ | "asdf", //
| )
|
| foo<
- | Int,
+ | Int //
| >(
| "asd",
- | "asd",
+ | "asd", //
| )
|
| foo<
| Int,
- | Boolean,
+ | Boolean, //
| >(
- | 3,
+ | 3 //
| )
|}
|"""
@@ -861,12 +914,241 @@ class GoogleStyleFormatterKtTest {
deduceMaxWidth = true)
@Test
+ fun `tailing commas are removed when redundant`() {
+ val code =
+ """
+ |fun main() {
+ | fun <A, B,> foo() {}
+ |
+ | fun foo(a: Int, b: Int = 0,) {}
+ |
+ | foo<Int, Int,>()
+ |
+ | foo(0, 0,)
+ |
+ | @Anno(arr = [0, 0,]) //
+ | fun foo() {}
+ |}
+ |"""
+ .trimMargin()
+ val expected =
+ """
+ |fun main() {
+ | fun <A, B> foo() {}
+ |
+ | fun foo(a: Int, b: Int = 0) {}
+ |
+ | foo<Int, Int>()
+ |
+ | foo(0, 0)
+ |
+ | @Anno(arr = [0, 0]) //
+ | fun foo() {}
+ |}
+ |"""
+ .trimMargin()
+ assertThatFormatting(code).withOptions(Formatter.GOOGLE_FORMAT).isEqualTo(expected)
+ }
+
+ @Test
+ fun `tailing commas are added when missing`() {
+ // Use trailing comments to force the breaks
+ val code =
+ """
+ |fun main() {
+ | fun <
+ | A,
+ | B // Comma before comment
+ | > foo() {}
+ |
+ | fun foo(
+ | a: Int,
+ | b: Int = 0 // Comma before comment
+ | ) {}
+ |
+ | foo<
+ | Int,
+ | Int // Comma before comment
+ | >()
+ |
+ | foo(
+ | 0,
+ | b = 0 // Comma before comment
+ | )
+ |
+ | foo(
+ | 0,
+ | b = {
+ | // Comma outside lambda
+ | }
+ | )
+ |
+ | @Anno(
+ | arr = [
+ | 0,
+ | 0 // Comma before comment
+ | ]
+ | )
+ | fun foo() {}
+ |}
+ |"""
+ .trimMargin()
+ val expected =
+ """
+ |fun main() {
+ | fun <
+ | A,
+ | B, // Comma before comment
+ | > foo() {}
+ |
+ | fun foo(
+ | a: Int,
+ | b: Int = 0, // Comma before comment
+ | ) {}
+ |
+ | foo<
+ | Int,
+ | Int, // Comma before comment
+ | >()
+ |
+ | foo(
+ | 0,
+ | b = 0, // Comma before comment
+ | )
+ |
+ | foo(
+ | 0,
+ | b = {
+ | // Comma outside lambda
+ | },
+ | )
+ |
+ | @Anno(
+ | arr =
+ | [
+ | 0,
+ | 0, // Comma before comment
+ | ]
+ | )
+ | fun foo() {}
+ |}
+ |"""
+ .trimMargin()
+ assertThatFormatting(code).withOptions(Formatter.GOOGLE_FORMAT).isEqualTo(expected)
+ }
+
+ @Test
+ fun `tailing commas that are always removed`() {
+ // Use trailing comments to force the breaks
+ val code =
+ """
+ |fun main() {
+ | foo {
+ | a, //
+ | b, ->
+ | a
+ | }
+ |
+ | when (a) {
+ | is A, //
+ | is B, -> return
+ | }
+ |}
+ |"""
+ .trimMargin()
+ val expected =
+ """
+ |fun main() {
+ | foo {
+ | a, //
+ | b ->
+ | a
+ | }
+ |
+ | when (a) {
+ | is A, //
+ | is B -> return
+ | }
+ |}
+ |"""
+ .trimMargin()
+ assertThatFormatting(code).withOptions(Formatter.GOOGLE_FORMAT).isEqualTo(expected)
+ }
+
+ @Test
+ fun `tailing commas are not added to empty lists`() {
+ // Use trailing comments to force the breaks
+ assertFormatted(
+ """
+ |fun main() {
+ | fun foo(
+ | //
+ | ) {}
+ |
+ | foo(
+ | //
+ | )
+ |
+ | foo {
+ | //
+ | ->
+ | 0
+ | }
+ |
+ | @Anno(
+ | arr =
+ | [
+ | //
+ | ]
+ | )
+ | fun foo() {}
+ |}
+ |"""
+ .trimMargin(),
+ formattingOptions = Formatter.GOOGLE_FORMAT,
+ deduceMaxWidth = false)
+ }
+
+ @Test
+ fun `tailing commas are not added to single-element lists`() {
+ assertFormatted(
+ """
+ |fun main() {
+ | fun foo(
+ | a: Int //
+ | ) {}
+ |
+ | foo(
+ | 0 //
+ | )
+ |
+ | foo {
+ | a //
+ | ->
+ | 0
+ | }
+ |
+ | @Anno(
+ | arr =
+ | [
+ | 0 //
+ | ]
+ | )
+ | fun foo() {}
+ |}
+ |"""
+ .trimMargin(),
+ formattingOptions = Formatter.GOOGLE_FORMAT,
+ deduceMaxWidth = false)
+ }
+
+ @Test
fun `an assortment of tests for emitQualifiedExpression`() =
assertFormatted(
"""
- |--------------------------------------
+ |//////////////////////////////////////
|fun f() {
- | // Regression test: https://github.com/facebookincubator/ktfmt/issues/56
+ | // Regression test: https://github.com/facebook/ktfmt/issues/56
| kjsdfglkjdfgkjdfkgjhkerjghkdfj
| ?.methodName1()
|
@@ -943,15 +1225,15 @@ class GoogleStyleFormatterKtTest {
fun `chained calls that don't fit in one line`() =
assertFormatted(
"""
- |---------------------------
+ |///////////////////////////
|fun f() {
| foo(
| println("a"),
- | println("b")
+ | println("b"),
| )
| .bar(
| println("b"),
- | println("b")
+ | println("b"),
| )
|}
|"""
@@ -981,7 +1263,7 @@ class GoogleStyleFormatterKtTest {
fun `comma separated lists, no automatic trailing break after lambda params`() =
assertFormatted(
"""
- |----------------------------
+ |////////////////////////////
|fun foo() {
| someExpr.let { x -> x }
| someExpr.let { x, y -> x }
@@ -1014,7 +1296,7 @@ class GoogleStyleFormatterKtTest {
fun `comma separated lists, no automatic trailing break after supertype list`() =
assertFormatted(
"""
- |----------------------------
+ |////////////////////////////
|class Foo() :
| ThisList,
| WillBe,
@@ -1032,7 +1314,7 @@ class GoogleStyleFormatterKtTest {
fun `if expression with multiline condition`() =
assertFormatted(
"""
- |----------------------------
+ |////////////////////////////
|fun foo() {
| if (
| expressions1 &&
@@ -1061,7 +1343,7 @@ class GoogleStyleFormatterKtTest {
fun `if expression with condition that exactly fits to line`() =
assertFormatted(
"""
- |-------------------------
+ |/////////////////////////
|fun foo() {
| if (
| e1 && e2 && e3 = e4
@@ -1078,7 +1360,7 @@ class GoogleStyleFormatterKtTest {
fun `when() expression with multiline condition`() =
assertFormatted(
"""
- |-----------------------
+ |///////////////////////
|fun foo() {
| when (
| expressions1 +
@@ -1109,7 +1391,7 @@ class GoogleStyleFormatterKtTest {
fun `when expression with condition that exactly fits to line`() =
assertFormatted(
"""
- |---------------------------
+ |///////////////////////////
|fun foo() {
| when (
| e1 && e2 && e3 = e4
@@ -1127,7 +1409,7 @@ class GoogleStyleFormatterKtTest {
fun `while expression with multiline condition`() =
assertFormatted(
"""
- |----------------------------
+ |////////////////////////////
|fun foo() {
| while (
| expressions1 &&
@@ -1156,7 +1438,7 @@ class GoogleStyleFormatterKtTest {
fun `while expression with condition that exactly fits to line`() =
assertFormatted(
"""
- |----------------------------
+ |////////////////////////////
|fun foo() {
| while (
| e1 && e2 && e3 = e4
@@ -1173,7 +1455,7 @@ class GoogleStyleFormatterKtTest {
fun `handle destructuring declaration`() =
assertFormatted(
"""
- |-------------------------------------------
+ |///////////////////////////////////////////
|fun f() {
| val (a, b: Int) = listOf(1, 2)
| val (asd, asd, asd, asd, asd, asd, asd) =
@@ -1184,7 +1466,7 @@ class GoogleStyleFormatterKtTest {
| foo,
| bar,
| zed,
- | boo
+ | boo,
| )
|}
|"""
@@ -1196,14 +1478,14 @@ class GoogleStyleFormatterKtTest {
fun `trailing break argument list`() =
assertFormatted(
"""
- |-------------------
+ |///////////////////
|fun method() {
| Foo.FooBar(
| longParameter
| )
| Foo.FooBar(
| param1,
- | param2
+ | param2,
| )
|}
|"""
@@ -1215,7 +1497,7 @@ class GoogleStyleFormatterKtTest {
fun `trailing break chains`() =
assertFormatted(
"""
- |-------------
+ |/////////////
|bar(
| FooOpClass
| .doOp(1)
@@ -1230,13 +1512,13 @@ class GoogleStyleFormatterKtTest {
fun `wrapping for long function types`() =
assertFormatted(
"""
- |------------------------
+ |////////////////////////
|var listener:
| (
| a: String,
| b: String,
| c: String,
- | d: String
+ | d: String,
| ) -> Unit
|"""
.trimMargin(),
@@ -1247,7 +1529,7 @@ class GoogleStyleFormatterKtTest {
fun `function call following long multiline string`() =
assertFormatted(
"""
- |--------------------------------
+ |////////////////////////////////
|fun f() {
| val str1 =
| $TQ
@@ -1267,16 +1549,36 @@ class GoogleStyleFormatterKtTest {
deduceMaxWidth = true)
@Test
+ fun `multiline string literals as function params`() =
+ assertFormatted(
+ """
+ |fun doIt(world: String) {
+ | println(
+ | ${TQ}Hello
+ | world!${TQ}
+ | )
+ | println(
+ | ${TQ}Hello
+ | world!${TQ},
+ | ${TQ}Goodbye
+ | world!${TQ},
+ | )
+ |}
+ |"""
+ .trimMargin(),
+ formattingOptions = Formatter.GOOGLE_FORMAT)
+
+ @Test
fun `array-literal in annotation`() =
assertFormatted(
"""
- |--------------------------------
+ |////////////////////////////////
|@Anno(
| array =
| [
| someItem,
| andAnother,
- | noTrailingComma
+ | noTrailingComma,
| ]
|)
|class Host
@@ -1299,7 +1601,7 @@ class GoogleStyleFormatterKtTest {
| // Comment
| andAnother,
| // Comment
- | withTrailingComment
+ | withTrailingComment,
| // Comment
| // Comment
| ]
@@ -1310,6 +1612,226 @@ class GoogleStyleFormatterKtTest {
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
+ @Test
+ fun `leading and trailing comments in block-like lists`() =
+ assertFormatted(
+ """
+ |////////////////////////////////
+ |@Anno(
+ | array =
+ | [
+ | // Comment
+ | someItem
+ | // Comment
+ | ]
+ |)
+ |class Host(
+ | // Comment
+ | val someItem: Int
+ | // Comment
+ |) {
+ | constructor(
+ | // Comment
+ | someItem: Int
+ | // Comment
+ | ) : this(
+ | // Comment
+ | someItem
+ | // Comment
+ | )
+ |
+ | fun foo(
+ | // Comment
+ | someItem: Int
+ | // Comment
+ | ): Int {
+ | foo(
+ | // Comment
+ | someItem
+ | // Comment
+ | )
+ | }
+ |
+ | var x: Int = 0
+ | set(
+ | // Comment
+ | someItem: Int
+ | // Comment
+ | ) = Unit
+ |
+ | fun <
+ | // Comment
+ | someItem : Int
+ | // Comment
+ | > bar(): Int {
+ | bar<
+ | // Comment
+ | someItem
+ | // Comment
+ | >()
+ | }
+ |}
+ |"""
+ .trimMargin(),
+ formattingOptions = Formatter.GOOGLE_FORMAT,
+ deduceMaxWidth = true)
+
+ @Test
+ fun `comments in empty block-like lists`() =
+ assertFormatted(
+ """
+ |////////////////////////////////
+ |@Anno(
+ | array =
+ | [
+ | // Comment
+ | ]
+ |)
+ |class Host(
+ | // Comment
+ |) {
+ | constructor(
+ | // Comment
+ | ) : this(
+ | // Comment
+ | )
+ |
+ | val x: Int
+ | get(
+ | // Comment
+ | ) = 0
+ |
+ | fun foo(
+ | // Comment
+ | ): Int {
+ | foo(
+ | // Comment
+ | )
+ | }
+ |}
+ |"""
+ .trimMargin(),
+ formattingOptions = Formatter.GOOGLE_FORMAT,
+ deduceMaxWidth = true)
+
+ @Test
+ fun `trailing commas on multline enum entries`() =
+ assertFormatted(
+ """
+ |enum class MultilineEntries {
+ | A(
+ | arg = 0, //
+ | arg = 0,
+ | ),
+ | /* Comment */
+ | B,
+ | C {
+ | fun foo() {}
+ | },
+ |}
+ |"""
+ .trimMargin(),
+ formattingOptions = Formatter.GOOGLE_FORMAT)
+
+ @Test
+ fun `trailing commas in enums`() {
+ val code =
+ """
+ |enum class A {}
+ |
+ |enum class B {
+ | Z // Comment
+ |}
+ |
+ |enum class C {
+ | Z, // Comment
+ |}
+ |
+ |enum class D {
+ | Z,
+ | Y // Comment
+ |}
+ |
+ |enum class E {
+ | Z,
+ | Y, // Comment
+ |}
+ |
+ |enum class F {
+ | Z,
+ | Y; // Comment
+ |
+ | val x = 0
+ |}
+ |
+ |enum class G {
+ | Z,
+ | Y,; // Comment
+ |
+ | val x = 0
+ |}
+ |
+ |enum class H {
+ | Z,
+ | Y() {} // Comment
+ |}
+ |
+ |enum class I {
+ | Z,
+ | Y() {}, // Comment
+ |}
+ |"""
+ .trimMargin()
+ val expected =
+ """
+ |enum class A {}
+ |
+ |enum class B {
+ | Z // Comment
+ |}
+ |
+ |enum class C {
+ | Z // Comment
+ |}
+ |
+ |enum class D {
+ | Z,
+ | Y, // Comment
+ |}
+ |
+ |enum class E {
+ | Z,
+ | Y, // Comment
+ |}
+ |
+ |enum class F {
+ | Z,
+ | Y; // Comment
+ |
+ | val x = 0
+ |}
+ |
+ |enum class G {
+ | Z,
+ | Y; // Comment
+ |
+ | val x = 0
+ |}
+ |
+ |enum class H {
+ | Z,
+ | Y() {}, // Comment
+ |}
+ |
+ |enum class I {
+ | Z,
+ | Y() {}, // Comment
+ |}
+ |"""
+ .trimMargin()
+ assertThatFormatting(code).withOptions(Formatter.GOOGLE_FORMAT).isEqualTo(expected)
+ }
+
companion object {
/** Triple quotes, useful to use within triple-quoted strings. */
private const val TQ = "\"\"\""
diff --git a/core/src/test/java/com/facebook/ktfmt/format/TokenizerTest.kt b/core/src/test/java/com/facebook/ktfmt/format/TokenizerTest.kt
index 3f8fe71..e645990 100644
--- a/core/src/test/java/com/facebook/ktfmt/format/TokenizerTest.kt
+++ b/core/src/test/java/com/facebook/ktfmt/format/TokenizerTest.kt
@@ -94,10 +94,13 @@ class TokenizerTest {
@Test
fun `Token index is advanced after a string token`() {
- val code = """
+ val code =
+ """
|val b="a"
|val a=5
- |""".trimMargin().trimMargin()
+ |"""
+ .trimMargin()
+ .trimMargin()
val file = Parser.parse(code)
val tokenizer = Tokenizer(code, file)
@@ -110,4 +113,109 @@ class TokenizerTest {
.containsExactly(0, -1, 1, 2, 3, -1, 4, -1, 5, 6, 7)
.inOrder()
}
+
+ @Test
+ fun `Context receivers are parsed correctly`() {
+ val code =
+ """
+ |context(Something)
+ |class A {
+ | context(
+ | // Test comment.
+ | Logger, Raise<Error>)
+ | fun test() {}
+ |}
+ |"""
+ .trimMargin()
+ .trimMargin()
+
+ val file = Parser.parse(code)
+ val tokenizer = Tokenizer(code, file)
+ file.accept(tokenizer)
+
+ assertThat(tokenizer.toks.map { it.originalText })
+ .containsExactly(
+ "context",
+ "(",
+ "Something",
+ ")",
+ "\n",
+ "class",
+ " ",
+ "A",
+ " ",
+ "{",
+ "\n",
+ " ",
+ "context",
+ "(",
+ "\n",
+ " ",
+ "// Test comment.",
+ "\n",
+ " ",
+ "Logger",
+ ",",
+ " ",
+ "Raise",
+ "<",
+ "Error",
+ ">",
+ ")",
+ "\n",
+ " ",
+ "fun",
+ " ",
+ "test",
+ "(",
+ ")",
+ " ",
+ "{",
+ "}",
+ "\n",
+ "}")
+ .inOrder()
+ assertThat(tokenizer.toks.map { it.index })
+ .containsExactly(
+ 0,
+ 1,
+ 2,
+ 3,
+ -1,
+ 4,
+ -1,
+ 5,
+ -1,
+ 6,
+ -1,
+ -1,
+ 7,
+ 8,
+ -1,
+ -1,
+ 9,
+ -1,
+ -1,
+ 10,
+ 11,
+ -1,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ -1,
+ -1,
+ 17,
+ -1,
+ 18,
+ 19,
+ 20,
+ -1,
+ 21,
+ 22,
+ -1,
+ 23)
+ .inOrder()
+ }
}
diff --git a/core/src/test/java/com/facebook/ktfmt/kdoc/KDocFormatterTest.kt b/core/src/test/java/com/facebook/ktfmt/kdoc/KDocFormatterTest.kt
index af13a25..4e5e0a7 100644
--- a/core/src/test/java/com/facebook/ktfmt/kdoc/KDocFormatterTest.kt
+++ b/core/src/test/java/com/facebook/ktfmt/kdoc/KDocFormatterTest.kt
@@ -2892,9 +2892,9 @@ class KDocFormatterTest {
@Test
fun test193246766() {
val source =
- // Nonsensical text derived from the original using the lorem() method and
- // replacing same-length & same capitalization words from lorem ipsum
- """
+ // Nonsensical text derived from the original using the lorem() method and
+ // replacing same-length & same capitalization words from lorem ipsum
+ """
/**
* * Do do occaecat sunt in culpa:
* * Id id reprehenderit cillum non `adipiscing` enim enim ad occaecat
@@ -2939,7 +2939,7 @@ class KDocFormatterTest {
@Test
fun test203584301() {
- // https://github.com/facebookincubator/ktfmt/issues/310
+ // https://github.com/facebook/ktfmt/issues/310
val source =
"""
/**
@@ -2955,8 +2955,7 @@ class KDocFormatterTest {
/**
* This is my SampleInterface interface.
*
- * @sample
- * com.example.java.sample.library.extra.long.path.MyCustomSampleInterfaceImplementationForTesting
+ * @sample com.example.java.sample.library.extra.long.path.MyCustomSampleInterfaceImplementationForTesting
*/
"""
.trimIndent())
@@ -2966,9 +2965,9 @@ class KDocFormatterTest {
fun test209435082() {
// b/209435082
val source =
- // Nonsensical text derived from the original using the lorem() method and
- // replacing same-length & same capitalization words from lorem ipsum
- """
+ // Nonsensical text derived from the original using the lorem() method and
+ // replacing same-length & same capitalization words from lorem ipsum
+ """
/**
* eiusmod.com
* - - -
@@ -3029,9 +3028,9 @@ class KDocFormatterTest {
@Test
fun test236743270() {
val source =
- // Nonsensical text derived from the original using the lorem() method and
- // replacing same-length & same capitalization words from lorem ipsum
- """
+ // Nonsensical text derived from the original using the lorem() method and
+ // replacing same-length & same capitalization words from lorem ipsum
+ """
/**
* @return Amet do non adipiscing sed consequat duis non Officia ID (amet sed consequat non
* adipiscing sed eiusmod), magna consequat.
@@ -3056,9 +3055,9 @@ class KDocFormatterTest {
@Test
fun test238279769() {
val source =
- // Nonsensical text derived from the original using the lorem() method and
- // replacing same-length & same capitalization words from lorem ipsum
- """
+ // Nonsensical text derived from the original using the lorem() method and
+ // replacing same-length & same capitalization words from lorem ipsum
+ """
/**
* @property dataItemOrderRandomizer sit tempor enim pariatur non culpa id [Pariatur]z in qui anim.
* Anim id-lorem sit magna [Consectetur] pariatur.
@@ -4613,9 +4612,9 @@ class KDocFormatterTest {
@Test
fun testPropertiesWithBrackets() {
val source =
- // From AOSP
- // tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/cxx/prefab/PackageModel.kt
- """
+ // From AOSP
+ // tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/cxx/prefab/PackageModel.kt
+ """
/**
* The Android abi.json schema.
*
diff --git a/core/src/test/java/com/facebook/ktfmt/testutil/KtfmtTruth.kt b/core/src/test/java/com/facebook/ktfmt/testutil/KtfmtTruth.kt
index de67121..1e99237 100644
--- a/core/src/test/java/com/facebook/ktfmt/testutil/KtfmtTruth.kt
+++ b/core/src/test/java/com/facebook/ktfmt/testutil/KtfmtTruth.kt
@@ -23,16 +23,19 @@ import com.facebook.ktfmt.format.Parser
import com.google.common.truth.FailureMetadata
import com.google.common.truth.Subject
import com.google.common.truth.Truth
+import org.intellij.lang.annotations.Language
import org.junit.Assert
/**
* Verifies the given code passes through formatting, and stays the same at the end
*
- * @param code a code string that continas an optional first line made of "---" in the case
- * [deduceMaxWidth] is true. For example:
+ * @param code a code string that contains an optional first line made of at least 8 '-' or '/' in
+ * the case [deduceMaxWidth] is true. For example:
* ```
- * --------------------
- * // exactly 20 `-` above
+ * ////////////////////////
+ * // exactly 24 `/` above
+ * // and that will be the
+ * // size of the line
* fun f()
* ```
*
@@ -40,25 +43,26 @@ import org.junit.Assert
* beginning to indicate the max width to format by
*/
fun assertFormatted(
- code: String,
+ @Language("kts") code: String,
formattingOptions: FormattingOptions = FormattingOptions(),
- deduceMaxWidth: Boolean = false
+ deduceMaxWidth: Boolean = false,
) {
val first = code.lines().first()
var deducedCode = code
var maxWidth = FormattingOptions.DEFAULT_MAX_WIDTH
- val isFirstLineAMaxWidthMarker = first.all { it == '-' }
+ val lineWidthMarkers = setOf('-', '/')
+ val isFirstLineAMaxWidthMarker = first.length >= 8 && first.all { it in lineWidthMarkers }
if (deduceMaxWidth) {
if (!isFirstLineAMaxWidthMarker) {
throw RuntimeException(
- "deduceMaxWidth is false, please remove the first dashes only line from the code (i.e. ---)")
+ "When deduceMaxWidth is true the first line needs to be all dashes only (i.e. ---)")
}
deducedCode = code.substring(code.indexOf('\n') + 1)
maxWidth = first.length
} else {
if (isFirstLineAMaxWidthMarker) {
throw RuntimeException(
- "When deduceMaxWidth is true the first line need to be all dashes only (i.e. ---)")
+ "deduceMaxWidth is false, please remove the first dashes only line from the code (i.e. ---)")
}
}
assertThatFormatting(deducedCode)
@@ -66,9 +70,11 @@ fun assertFormatted(
.isEqualTo(deducedCode)
}
-fun assertThatFormatting(code: String): FormattedCodeSubject {
+fun assertThatFormatting(@Language("kts") code: String): FormattedCodeSubject {
fun codes(): Subject.Factory<FormattedCodeSubject, String> {
- return Subject.Factory { metadata, subject -> FormattedCodeSubject(metadata, subject) }
+ return Subject.Factory { metadata, subject ->
+ FormattedCodeSubject(metadata, checkNotNull(subject))
+ }
}
return Truth.assertAbout(codes()).that(code)
}
@@ -88,7 +94,7 @@ class FormattedCodeSubject(metadata: FailureMetadata, private val code: String)
return this
}
- fun isEqualTo(expectedFormatting: String) {
+ fun isEqualTo(@Language("kts") expectedFormatting: String) {
if (!allowTrailingWhitespace && expectedFormatting.lines().any { it.endsWith(" ") }) {
throw RuntimeException(
"Expected code contains trailing whitespace, which the formatter usually doesn't output:\n" +
@@ -110,7 +116,7 @@ class FormattedCodeSubject(metadata: FailureMetadata, private val code: String)
println(expectedFormatting)
println("#".repeat(20))
println(
- "Need more information about the break operations?" +
+ "Need more information about the break operations? " +
"Run test with assertion with \"FormattingOptions(debuggingPrintOpsAfterFormatting = true)\"")
}
} catch (e: Error) {
diff --git a/docs/editorconfig/.editorconfig-dropbox b/docs/editorconfig/.editorconfig-dropbox
index bc214f6..b76fa75 100644
--- a/docs/editorconfig/.editorconfig-dropbox
+++ b/docs/editorconfig/.editorconfig-dropbox
@@ -63,7 +63,6 @@ ij_kotlin_method_parameters_right_paren_on_new_line = true
ij_kotlin_method_parameters_wrap = on_every_item
ij_kotlin_name_count_to_use_star_import = 9999
ij_kotlin_name_count_to_use_star_import_for_members = 9999
-ij_java_names_count_to_use_import_on_demand = 9999
ij_kotlin_parameter_annotation_wrap = off
ij_kotlin_space_after_comma = true
ij_kotlin_space_after_extend_colon = true
diff --git a/docs/editorconfig/.editorconfig-kotlinlang b/docs/editorconfig/.editorconfig-kotlinlang
index 2f55867..85d6e05 100644
--- a/docs/editorconfig/.editorconfig-kotlinlang
+++ b/docs/editorconfig/.editorconfig-kotlinlang
@@ -42,10 +42,11 @@ ij_kotlin_continuation_indent_in_supertype_lists = false
ij_kotlin_else_on_new_line = false
ij_kotlin_enum_constants_wrap = off
ij_kotlin_extends_list_wrap = normal
-ij_kotlin_field_annotation_wrap = split_into_lines
+ij_kotlin_field_annotation_wrap = off
ij_kotlin_finally_on_new_line = false
ij_kotlin_if_rparen_on_new_line = false
ij_kotlin_import_nested_classes = false
+ij_kotlin_imports_layout = *
ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
ij_kotlin_keep_blank_lines_before_right_brace = 2
ij_kotlin_keep_blank_lines_in_code = 2
diff --git a/ktfmt_idea_plugin/build.gradle.kts b/ktfmt_idea_plugin/build.gradle.kts
index 63c7879..4568378 100644
--- a/ktfmt_idea_plugin/build.gradle.kts
+++ b/ktfmt_idea_plugin/build.gradle.kts
@@ -15,17 +15,18 @@
*/
plugins {
- id("org.jetbrains.intellij") version "0.7.2"
+ id("org.jetbrains.intellij") version "1.17.3"
java
id("com.diffplug.spotless") version "5.10.2"
}
-val ktfmtVersion = rootProject.file("../version.txt").readText().trim()
+val currentKtfmtVersion = rootProject.file("../version.txt").readText().trim()
+val stableKtfmtVersion = rootProject.file("../stable_version.txt").readText().trim()
val pluginVersion = "1.1"
group = "com.facebook"
-version = "$pluginVersion.$ktfmtVersion"
+version = "$pluginVersion.$currentKtfmtVersion"
repositories {
mavenCentral()
@@ -38,25 +39,25 @@ java {
}
dependencies {
- implementation("com.facebook", "ktfmt", ktfmtVersion)
- implementation("com.google.googlejavaformat", "google-java-format", "1.8")
+ implementation("com.facebook", "ktfmt", stableKtfmtVersion)
+ implementation("com.google.googlejavaformat", "google-java-format", "1.22.0")
}
// See https://github.com/JetBrains/gradle-intellij-plugin/
intellij {
// Version with which to build (and run; unless alternativeIdePath is specified)
- version = "2020.3"
+ version.set("2022.1")
// To run on a different IDE, uncomment and specify a path.
- // alternativeIdePath = "/Applications/Android Studio.app"
+ // localPath = "/Applications/Android Studio.app"
}
tasks {
patchPluginXml {
- sinceBuild("201")
- untilBuild("")
+ sinceBuild.set("221")
+ untilBuild.set("")
}
- publishPlugin { token(System.getenv("JETBRAINS_MARKETPLACE_TOKEN")) }
- runPluginVerifier { ideVersions(listOf("211.6432.7")) }
+ publishPlugin { token.set(System.getenv("JETBRAINS_MARKETPLACE_TOKEN")) }
+ runPluginVerifier { ideVersions.set(listOf("221")) }
}
-spotless { java { googleJavaFormat() } }
+spotless { java { googleJavaFormat("1.22.0") } }
diff --git a/ktfmt_idea_plugin/gradle/wrapper/gradle-wrapper.jar b/ktfmt_idea_plugin/gradle/wrapper/gradle-wrapper.jar
index 62d4c05..033e24c 100644
--- a/ktfmt_idea_plugin/gradle/wrapper/gradle-wrapper.jar
+++ b/ktfmt_idea_plugin/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/ktfmt_idea_plugin/gradle/wrapper/gradle-wrapper.properties b/ktfmt_idea_plugin/gradle/wrapper/gradle-wrapper.properties
index 622ab64..ac72c34 100644
--- a/ktfmt_idea_plugin/gradle/wrapper/gradle-wrapper.properties
+++ b/ktfmt_idea_plugin/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/ktfmt_idea_plugin/gradlew b/ktfmt_idea_plugin/gradlew
index fbd7c51..fcb6fca 100755
--- a/ktfmt_idea_plugin/gradlew
+++ b/ktfmt_idea_plugin/gradlew
@@ -1,7 +1,7 @@
-#!/usr/bin/env sh
+#!/bin/sh
#
-# Copyright 2015 the original author or authors.
+# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,67 +17,98 @@
#
##############################################################################
-##
-## Gradle start up script for UN*X
-##
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
##############################################################################
# Attempt to set APP_HOME
+
# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
warn () {
echo "$*"
-}
+} >&2
die () {
echo
echo "$*"
echo
exit 1
-}
+} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
- NONSTOP* )
- nonstop=true
- ;;
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -87,9 +118,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACMD=$JAVA_HOME/jre/sh/java
else
- JAVACMD="$JAVA_HOME/bin/java"
+ JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -98,88 +129,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
fi
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
-if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-
- JAVACMD=`cygpath --unix "$JAVACMD"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
# Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
fi
- i=`expr $i + 1`
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
done
- case $i in
- 0) set -- ;;
- 1) set -- "$args0" ;;
- 2) set -- "$args0" "$args1" ;;
- 3) set -- "$args0" "$args1" "$args2" ;;
- 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
fi
-# Escape application args
-save () {
- for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
- echo " "
-}
-APP_ARGS=`save "$@"`
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
exec "$JAVACMD" "$@"
diff --git a/ktfmt_idea_plugin/gradlew.bat b/ktfmt_idea_plugin/gradlew.bat
index a9f778a..4e5d21e 100644
--- a/ktfmt_idea_plugin/gradlew.bat
+++ b/ktfmt_idea_plugin/gradlew.bat
@@ -1,11 +1,10 @@
-@rem
-@rem Copyright 2015 the original author or authors.
+@rem Copyright (c) Meta Platforms, Inc. and affiliates.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
-@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,7 +13,7 @@
@rem limitations under the License.
@rem
-@if "%DEBUG%" == "" @echo off
+@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +24,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
+if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -54,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-if exist "%JAVA_EXE%" goto init
+if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -64,21 +64,6 @@ echo location of your Java installation.
goto fail
-:init
-@rem Get command-line arguments, handling Windows variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-
:execute
@rem Setup the command line
@@ -86,17 +71,19 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
+if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
diff --git a/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/CodeStyleManagerDecorator.java b/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/CodeStyleManagerDecorator.java
deleted file mode 100644
index 61de704..0000000
--- a/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/CodeStyleManagerDecorator.java
+++ /dev/null
@@ -1,239 +0,0 @@
-/*
- * Copyright 2015 Google Inc. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.facebook.ktfmt.intellij;
-
-import com.intellij.formatting.FormattingMode;
-import com.intellij.lang.ASTNode;
-import com.intellij.openapi.editor.Document;
-import com.intellij.openapi.fileTypes.FileType;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.util.Computable;
-import com.intellij.openapi.util.TextRange;
-import com.intellij.psi.PsiElement;
-import com.intellij.psi.PsiFile;
-import com.intellij.psi.codeStyle.ChangedRangesInfo;
-import com.intellij.psi.codeStyle.CodeStyleManager;
-import com.intellij.psi.codeStyle.DocCommentSettings;
-import com.intellij.psi.codeStyle.FormattingModeAwareIndentAdjuster;
-import com.intellij.psi.codeStyle.Indent;
-import com.intellij.util.IncorrectOperationException;
-import com.intellij.util.ThrowableRunnable;
-import java.util.Collection;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * Decorates the {@link CodeStyleManager} abstract class by delegating to a concrete implementation
- * instance (likely IJ's default instance).
- */
-@SuppressWarnings("deprecation")
-class CodeStyleManagerDecorator extends CodeStyleManager
- implements FormattingModeAwareIndentAdjuster {
-
- private final CodeStyleManager delegate;
-
- CodeStyleManagerDecorator(CodeStyleManager delegate) {
- this.delegate = delegate;
- }
-
- CodeStyleManager getDelegate() {
- return delegate;
- }
-
- @Override
- public Project getProject() {
- return delegate.getProject();
- }
-
- @Override
- public PsiElement reformat(PsiElement element) throws IncorrectOperationException {
- return delegate.reformat(element);
- }
-
- @Override
- public PsiElement reformat(PsiElement element, boolean canChangeWhiteSpacesOnly)
- throws IncorrectOperationException {
- return delegate.reformat(element, canChangeWhiteSpacesOnly);
- }
-
- @Override
- public PsiElement reformatRange(PsiElement element, int startOffset, int endOffset)
- throws IncorrectOperationException {
- return delegate.reformatRange(element, startOffset, endOffset);
- }
-
- @Override
- public PsiElement reformatRange(
- PsiElement element, int startOffset, int endOffset, boolean canChangeWhiteSpacesOnly)
- throws IncorrectOperationException {
- return delegate.reformatRange(element, startOffset, endOffset, canChangeWhiteSpacesOnly);
- }
-
- @Override
- public void reformatText(PsiFile file, int startOffset, int endOffset)
- throws IncorrectOperationException {
- delegate.reformatText(file, startOffset, endOffset);
- }
-
- @Override
- public void reformatText(PsiFile file, Collection<TextRange> ranges)
- throws IncorrectOperationException {
- delegate.reformatText(file, ranges);
- }
-
- @Override
- public void reformatTextWithContext(PsiFile psiFile, ChangedRangesInfo changedRangesInfo)
- throws IncorrectOperationException {
- delegate.reformatTextWithContext(psiFile, changedRangesInfo);
- }
-
- @Override
- public void reformatTextWithContext(PsiFile file, Collection<TextRange> ranges)
- throws IncorrectOperationException {
- delegate.reformatTextWithContext(file, ranges);
- }
-
- @Override
- public void adjustLineIndent(PsiFile file, TextRange rangeToAdjust)
- throws IncorrectOperationException {
- delegate.adjustLineIndent(file, rangeToAdjust);
- }
-
- @Override
- public int adjustLineIndent(PsiFile file, int offset) throws IncorrectOperationException {
- return delegate.adjustLineIndent(file, offset);
- }
-
- @Override
- public int adjustLineIndent(Document document, int offset) {
- return delegate.adjustLineIndent(document, offset);
- }
-
- public void scheduleIndentAdjustment(Document document, int offset) {
- delegate.scheduleIndentAdjustment(document, offset);
- }
-
- @Override
- public boolean isLineToBeIndented(PsiFile file, int offset) {
- return delegate.isLineToBeIndented(file, offset);
- }
-
- @Override
- @Nullable
- public String getLineIndent(PsiFile file, int offset) {
- return delegate.getLineIndent(file, offset);
- }
-
- @Override
- @Nullable
- public String getLineIndent(PsiFile file, int offset, FormattingMode mode) {
- return delegate.getLineIndent(file, offset, mode);
- }
-
- @Override
- @Nullable
- public String getLineIndent(Document document, int offset) {
- return delegate.getLineIndent(document, offset);
- }
-
- @Override
- public Indent getIndent(String text, FileType fileType) {
- return delegate.getIndent(text, fileType);
- }
-
- @Override
- public String fillIndent(Indent indent, FileType fileType) {
- return delegate.fillIndent(indent, fileType);
- }
-
- @Override
- public Indent zeroIndent() {
- return delegate.zeroIndent();
- }
-
- @Override
- public void reformatNewlyAddedElement(ASTNode block, ASTNode addedElement)
- throws IncorrectOperationException {
- delegate.reformatNewlyAddedElement(block, addedElement);
- }
-
- @Override
- public boolean isSequentialProcessingAllowed() {
- return delegate.isSequentialProcessingAllowed();
- }
-
- @Override
- public void performActionWithFormatterDisabled(Runnable r) {
- delegate.performActionWithFormatterDisabled(r);
- }
-
- @Override
- public <T extends Throwable> void performActionWithFormatterDisabled(ThrowableRunnable<T> r)
- throws T {
- delegate.performActionWithFormatterDisabled(r);
- }
-
- @Override
- public <T> T performActionWithFormatterDisabled(Computable<T> r) {
- return delegate.performActionWithFormatterDisabled(r);
- }
-
- @Override
- public int getSpacing(PsiFile file, int offset) {
- return delegate.getSpacing(file, offset);
- }
-
- @Override
- public int getMinLineFeeds(PsiFile file, int offset) {
- return delegate.getMinLineFeeds(file, offset);
- }
-
- @Override
- public void runWithDocCommentFormattingDisabled(PsiFile file, Runnable runnable) {
- delegate.runWithDocCommentFormattingDisabled(file, runnable);
- }
-
- @Override
- public DocCommentSettings getDocCommentSettings(PsiFile file) {
- return delegate.getDocCommentSettings(file);
- }
-
- // From FormattingModeAwareIndentAdjuster
-
- /** Uses same fallback as {@link CodeStyleManager#getCurrentFormattingMode}. */
- @Override
- public FormattingMode getCurrentFormattingMode() {
- if (delegate instanceof FormattingModeAwareIndentAdjuster) {
- return ((FormattingModeAwareIndentAdjuster) delegate).getCurrentFormattingMode();
- }
- return FormattingMode.REFORMAT;
- }
-
- @Override
- public int adjustLineIndent(final Document document, final int offset, FormattingMode mode)
- throws IncorrectOperationException {
- if (delegate instanceof FormattingModeAwareIndentAdjuster) {
- return ((FormattingModeAwareIndentAdjuster) delegate)
- .adjustLineIndent(document, offset, mode);
- }
- return offset;
- }
-
- @Override
- public void scheduleReformatWhenSettingsComputed(PsiFile file) {
- delegate.scheduleReformatWhenSettingsComputed(file);
- }
-}
diff --git a/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/FormatterUtil.java b/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/FormatterUtil.java
deleted file mode 100644
index c67f114..0000000
--- a/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/FormatterUtil.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (c) Meta Platforms, Inc. and affiliates.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.facebook.ktfmt.intellij;
-
-import static com.facebook.ktfmt.format.Formatter.format;
-
-import com.facebook.ktfmt.format.ParseError;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableMap;
-import com.google.googlejavaformat.java.FormatterException;
-import com.intellij.openapi.util.TextRange;
-import java.util.Map;
-
-final class FormatterUtil {
-
- private FormatterUtil() {}
-
- /**
- * Formats 'code' using ktfmt.
- *
- * @return formatted code
- */
- static Map<TextRange, String> getReplacements(UiFormatterStyle uiFormatterStyle, String code) {
- try {
- return ImmutableMap.of(TextRange.allOf(code), formatCode(uiFormatterStyle, code));
- } catch (FormatterException | ParseError e) {
- return ImmutableMap.of();
- }
- }
-
- @VisibleForTesting
- static String formatCode(UiFormatterStyle uiFormatterStyle, String code)
- throws FormatterException {
-
- return format(uiFormatterStyle.getFormattingOptions(), code);
- }
-}
diff --git a/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/InitialConfigurationProjectManagerListener.java b/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/InitialConfigurationStartupActivity.java
index 2bc9775..9b9387a 100644
--- a/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/InitialConfigurationProjectManagerListener.java
+++ b/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/InitialConfigurationStartupActivity.java
@@ -17,21 +17,21 @@
package com.facebook.ktfmt.intellij;
import com.intellij.notification.Notification;
-import com.intellij.notification.NotificationDisplayType;
import com.intellij.notification.NotificationGroup;
+import com.intellij.notification.NotificationGroupManager;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.project.Project;
-import com.intellij.openapi.project.ProjectManagerListener;
+import com.intellij.openapi.startup.StartupActivity;
import org.jetbrains.annotations.NotNull;
-final class InitialConfigurationProjectManagerListener implements ProjectManagerListener {
+final class InitialConfigurationStartupActivity implements StartupActivity.Background {
private static final String NOTIFICATION_TITLE = "Enable ktfmt";
private static final NotificationGroup NOTIFICATION_GROUP =
- new NotificationGroup(NOTIFICATION_TITLE, NotificationDisplayType.STICKY_BALLOON, true);
+ NotificationGroupManager.getInstance().getNotificationGroup(NOTIFICATION_TITLE);
@Override
- public void projectOpened(@NotNull Project project) {
+ public void runActivity(@NotNull Project project) {
KtfmtSettings settings = KtfmtSettings.getInstance(project);
@@ -42,17 +42,17 @@ final class InitialConfigurationProjectManagerListener implements ProjectManager
}
private void displayNewUserNotification(Project project, KtfmtSettings settings) {
- Notification notification =
- new Notification(
+ new Notification(
NOTIFICATION_GROUP.getDisplayId(),
NOTIFICATION_TITLE,
"The ktfmt plugin is disabled by default. "
+ "<a href=\"enable\">Enable for this project</a>.",
- NotificationType.INFORMATION,
+ NotificationType.INFORMATION)
+ .setListener(
(n, e) -> {
settings.setEnabled(true);
n.expire();
- });
- notification.notify(project);
+ })
+ .notify(project);
}
}
diff --git a/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtCodeStyleManager.java b/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtCodeStyleManager.java
deleted file mode 100644
index d2184ea..0000000
--- a/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtCodeStyleManager.java
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Copyright 2015 Google Inc. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.facebook.ktfmt.intellij;
-
-import static java.util.Comparator.comparing;
-
-import com.google.common.collect.ImmutableList;
-import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.openapi.command.WriteCommandAction;
-import com.intellij.openapi.editor.Document;
-import com.intellij.openapi.util.TextRange;
-import com.intellij.psi.PsiDocumentManager;
-import com.intellij.psi.PsiElement;
-import com.intellij.psi.PsiFile;
-import com.intellij.psi.codeStyle.CodeStyleManager;
-import com.intellij.psi.impl.CheckUtil;
-import com.intellij.util.IncorrectOperationException;
-import java.util.Collection;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.TreeMap;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.kotlin.idea.KotlinFileType;
-
-/**
- * A {@link CodeStyleManager} implementation which formats .kt files with ktfmt. Formatting of all
- * other types of files is delegated to IJ's default implementation.
- */
-class KtfmtCodeStyleManager extends CodeStyleManagerDecorator {
-
- public KtfmtCodeStyleManager(@NotNull CodeStyleManager original) {
- super(original);
- }
-
- @Override
- public void reformatText(PsiFile file, int startOffset, int endOffset)
- throws IncorrectOperationException {
- if (overrideFormatterForFile(file)) {
- formatInternal(file, ImmutableList.of(new TextRange(startOffset, endOffset)));
- } else {
- super.reformatText(file, startOffset, endOffset);
- }
- }
-
- @Override
- public void reformatText(PsiFile file, Collection<TextRange> ranges)
- throws IncorrectOperationException {
- if (overrideFormatterForFile(file)) {
- formatInternal(file, ranges);
- } else {
- super.reformatText(file, ranges);
- }
- }
-
- @Override
- public void reformatTextWithContext(PsiFile file, Collection<TextRange> ranges) {
- if (overrideFormatterForFile(file)) {
- formatInternal(file, ranges);
- } else {
- super.reformatTextWithContext(file, ranges);
- }
- }
-
- @Override
- public PsiElement reformatRange(
- PsiElement element, int startOffset, int endOffset, boolean canChangeWhiteSpacesOnly) {
- // Only handle elements that are PsiFile for now -- otherwise we need to search
- // for some
- // element within the file at new locations given the original startOffset and
- // endOffsets
- // to serve as the return value.
- PsiFile file = element instanceof PsiFile ? (PsiFile) element : null;
- if (file != null && canChangeWhiteSpacesOnly && overrideFormatterForFile(file)) {
- formatInternal(file, ImmutableList.of(new TextRange(startOffset, endOffset)));
- return file;
- } else {
- return super.reformatRange(element, startOffset, endOffset, canChangeWhiteSpacesOnly);
- }
- }
-
- /** Return whether or not this formatter can handle formatting the given file. */
- private boolean overrideFormatterForFile(PsiFile file) {
- return KotlinFileType.INSTANCE.getName().equals(file.getFileType().getName())
- && KtfmtSettings.getInstance(getProject()).isEnabled();
- }
-
- private void formatInternal(PsiFile file, Collection<TextRange> ranges) {
- ApplicationManager.getApplication().assertWriteAccessAllowed();
- PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject());
- documentManager.commitAllDocuments();
- CheckUtil.checkWritable(file);
-
- Document document = documentManager.getDocument(file);
-
- if (document == null) {
- return;
- }
- // If there are postponed PSI changes (e.g., during a refactoring), just abort.
- // If we apply them now, then the incoming text ranges may no longer be valid.
- if (documentManager.isDocumentBlockedByPsi(document)) {
- return;
- }
-
- format(document, ranges);
- }
-
- /**
- * Format the ranges of the given document.
- *
- * <p>Overriding methods will need to modify the document with the result of the external
- * formatter (usually using {@link #performReplacements(Document, Map)}.
- */
- private void format(Document document, Collection<TextRange> ranges) {
- UiFormatterStyle uiFormatterStyle =
- KtfmtSettings.getInstance(getProject()).getUiFormatterStyle();
-
- performReplacements(
- document, FormatterUtil.getReplacements(uiFormatterStyle, document.getText()));
- }
-
- private void performReplacements(
- final Document document, final Map<TextRange, String> replacements) {
-
- if (replacements.isEmpty()) {
- return;
- }
-
- TreeMap<TextRange, String> sorted = new TreeMap<>(comparing(TextRange::getStartOffset));
- sorted.putAll(replacements);
- WriteCommandAction.runWriteCommandAction(
- getProject(),
- () -> {
- for (Entry<TextRange, String> entry : sorted.descendingMap().entrySet()) {
- document.replaceString(
- entry.getKey().getStartOffset(), entry.getKey().getEndOffset(), entry.getValue());
- }
- PsiDocumentManager.getInstance(getProject()).commitDocument(document);
- });
- }
-}
diff --git a/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtConfigurable.java b/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtConfigurable.java
index ac82917..9fd0e9a 100644
--- a/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtConfigurable.java
+++ b/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtConfigurable.java
@@ -200,7 +200,9 @@ public class KtfmtConfigurable extends BaseConfigurable implements SearchableCon
false));
}
- /** @noinspection ALL */
+ /**
+ * @noinspection ALL
+ */
public JComponent $$$getRootComponent$$$() {
return panel;
}
diff --git a/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtFormattingService.java b/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtFormattingService.java
new file mode 100644
index 0000000..58aaac7
--- /dev/null
+++ b/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtFormattingService.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2023 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.facebook.ktfmt.intellij;
+
+import static com.facebook.ktfmt.format.Formatter.format;
+
+import com.google.googlejavaformat.java.FormatterException;
+import com.intellij.formatting.service.AsyncDocumentFormattingService;
+import com.intellij.formatting.service.AsyncFormattingRequest;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiFile;
+import java.util.EnumSet;
+import java.util.Set;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.kotlin.idea.KotlinFileType;
+
+/** Uses {@code ktfmt} to reformat code. */
+public class KtfmtFormattingService extends AsyncDocumentFormattingService {
+
+ @Override
+ protected FormattingTask createFormattingTask(AsyncFormattingRequest request) {
+ Project project = request.getContext().getProject();
+
+ UiFormatterStyle style = KtfmtSettings.getInstance(project).getUiFormatterStyle();
+ return new KtfmtFormattingTask(request, style);
+ }
+
+ @Override
+ protected @NotNull String getNotificationGroupId() {
+ return Notifications.PARSING_ERROR_NOTIFICATION_GROUP;
+ }
+
+ @Override
+ protected @NotNull String getName() {
+ return "ktfmt";
+ }
+
+ @Override
+ public @NotNull Set<Feature> getFeatures() {
+ return EnumSet.noneOf(Feature.class);
+ }
+
+ @Override
+ public boolean canFormat(@NotNull PsiFile file) {
+ return KotlinFileType.INSTANCE.getName().equals(file.getFileType().getName())
+ && KtfmtSettings.getInstance(file.getProject()).isEnabled();
+ }
+
+ private static final class KtfmtFormattingTask implements FormattingTask {
+ private final AsyncFormattingRequest request;
+ private final UiFormatterStyle style;
+
+ private KtfmtFormattingTask(AsyncFormattingRequest request, UiFormatterStyle style) {
+ this.request = request;
+ this.style = style;
+ }
+
+ @Override
+ public void run() {
+ try {
+ String formattedText = format(style.getFormattingOptions(), request.getDocumentText());
+ request.onTextReady(formattedText);
+ } catch (FormatterException e) {
+ request.onError(
+ Notifications.PARSING_ERROR_TITLE,
+ Notifications.parsingErrorMessage(request.getContext().getContainingFile().getName()));
+ }
+ }
+
+ @Override
+ public boolean isRunUnderProgress() {
+ return true;
+ }
+
+ @Override
+ public boolean cancel() {
+ return false;
+ }
+ }
+}
diff --git a/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtInstaller.java b/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtInstaller.java
deleted file mode 100644
index ab414e8..0000000
--- a/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtInstaller.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright 2016 Google Inc. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.facebook.ktfmt.intellij;
-
-import static com.google.common.base.Preconditions.checkState;
-
-import com.intellij.ide.plugins.IdeaPluginDescriptor;
-import com.intellij.ide.plugins.PluginManagerCore;
-import com.intellij.openapi.extensions.PluginId;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.project.ProjectManagerListener;
-import com.intellij.psi.codeStyle.CodeStyleManager;
-import com.intellij.serviceContainer.ComponentManagerImpl;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * A component that replaces the default IntelliJ {@link CodeStyleManager} with one that formats via
- * ktfmt.
- */
-final class KtfmtInstaller implements ProjectManagerListener {
-
- @Override
- public void projectOpened(@NotNull Project project) {
- installFormatter(project);
- }
-
- private static void installFormatter(Project project) {
- CodeStyleManager currentManager = CodeStyleManager.getInstance(project);
-
- if (currentManager instanceof KtfmtCodeStyleManager) {
- currentManager = ((KtfmtCodeStyleManager) currentManager).getDelegate();
- }
-
- setManager(project, new KtfmtCodeStyleManager(currentManager));
- }
-
- private static void setManager(Project project, CodeStyleManager newManager) {
- ComponentManagerImpl platformComponentManager = (ComponentManagerImpl) project;
- IdeaPluginDescriptor plugin =
- PluginManagerCore.getPlugin(PluginId.getId("com.facebook.ktfmt_idea_plugin"));
- checkState(plugin != null, "Couldn't locate our own PluginDescriptor.");
- platformComponentManager.registerServiceInstance(CodeStyleManager.class, newManager, plugin);
- }
-}
diff --git a/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtSettings.java b/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtSettings.java
index 076e633..7a22d0a 100644
--- a/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtSettings.java
+++ b/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtSettings.java
@@ -17,10 +17,10 @@
package com.facebook.ktfmt.intellij;
import com.intellij.openapi.components.PersistentStateComponent;
-import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@State(
@@ -31,7 +31,7 @@ class KtfmtSettings implements PersistentStateComponent<KtfmtSettings.State> {
private State state = new State();
static KtfmtSettings getInstance(Project project) {
- return ServiceManager.getService(project, KtfmtSettings.class);
+ return project.getService(KtfmtSettings.class);
}
@Nullable
@@ -41,7 +41,7 @@ class KtfmtSettings implements PersistentStateComponent<KtfmtSettings.State> {
}
@Override
- public void loadState(State state) {
+ public void loadState(@NotNull State state) {
this.state = state;
}
@@ -85,7 +85,7 @@ class KtfmtSettings implements PersistentStateComponent<KtfmtSettings.State> {
public void setEnabled(@Nullable String enabledStr) {
if (enabledStr == null) {
enabled = EnabledState.UNKNOWN;
- } else if (Boolean.valueOf(enabledStr)) {
+ } else if (Boolean.parseBoolean(enabledStr)) {
enabled = EnabledState.ENABLED;
} else {
enabled = EnabledState.DISABLED;
diff --git a/ktfmt_idea_plugin/src/test/java/com/facebook/ktfmt/intellij/FormatterUtilTest.java b/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/Notifications.java
index 8b6b642..b64db56 100644
--- a/ktfmt_idea_plugin/src/test/java/com/facebook/ktfmt/intellij/FormatterUtilTest.java
+++ b/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/Notifications.java
@@ -16,20 +16,12 @@
package com.facebook.ktfmt.intellij;
-import static org.junit.Assert.assertEquals;
+class Notifications {
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+ static final String PARSING_ERROR_NOTIFICATION_GROUP = "ktfmt parsing error";
+ static final String PARSING_ERROR_TITLE = PARSING_ERROR_NOTIFICATION_GROUP;
-@RunWith(JUnit4.class)
-public class FormatterUtilTest {
- @Test
- public void getReplacements() throws Exception {
- String code = "val a = 5";
- String expected = "val a = 5\n";
- String actual = FormatterUtil.formatCode(UiFormatterStyle.DEFAULT, code);
-
- assertEquals(expected, actual);
+ static String parsingErrorMessage(String filename) {
+ return "ktfmt failed. Does " + filename + " have syntax errors?";
}
}
diff --git a/ktfmt_idea_plugin/src/main/resources/META-INF/plugin.xml b/ktfmt_idea_plugin/src/main/resources/META-INF/plugin.xml
index bf371f9..6c7512a 100644
--- a/ktfmt_idea_plugin/src/main/resources/META-INF/plugin.xml
+++ b/ktfmt_idea_plugin/src/main/resources/META-INF/plugin.xml
@@ -1,7 +1,7 @@
-<idea-plugin url="https://github.com/facebookincubator/ktfmt/tree/main/ktfmt_idea_plugin">
+<idea-plugin url="https://github.com/facebook/ktfmt/tree/main/ktfmt_idea_plugin">
<id>com.facebook.ktfmt_idea_plugin</id>
<name>ktfmt</name>
- <vendor url="https://github.com/facebookincubator/ktfmt">Facebook</vendor>
+ <vendor url="https://github.com/facebook/ktfmt">Facebook</vendor>
<description>ktfmt is a program that reformats Kotlin source code to comply with the common community standard for
Kotlin code conventions.
@@ -9,19 +9,17 @@
<depends>com.intellij.modules.platform</depends>
- <applicationListeners>
- <listener class="com.facebook.ktfmt.intellij.InitialConfigurationProjectManagerListener"
- topic="com.intellij.openapi.project.ProjectManagerListener"/>
- <listener class="com.facebook.ktfmt.intellij.KtfmtInstaller"
- topic="com.intellij.openapi.project.ProjectManagerListener"/>
- </applicationListeners>
-
<extensions defaultExtensionNs="com.intellij">
+ <formattingService
+ implementation="com.facebook.ktfmt.intellij.KtfmtFormattingService"/>
+ <postStartupActivity implementation="com.facebook.ktfmt.intellij.InitialConfigurationStartupActivity"/>
<projectConfigurable instance="com.facebook.ktfmt.intellij.KtfmtConfigurable"
id="com.facebook.ktfmt_idea_plugin.settings"
displayName="ktfmt Settings"
parentId="editor"/>
<projectService serviceImplementation="com.facebook.ktfmt.intellij.KtfmtSettings"/>
+ <notificationGroup displayType="STICKY_BALLOON" id="Enable ktfmt"
+ isLogByDefault="false"/>
</extensions>
</idea-plugin>
diff --git a/online_formatter/build.gradle.kts b/online_formatter/build.gradle.kts
index 140c338..0d8f3f8 100644
--- a/online_formatter/build.gradle.kts
+++ b/online_formatter/build.gradle.kts
@@ -14,16 +14,14 @@
* limitations under the License.
*/
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-
-plugins { kotlin("jvm") version "1.5.0" }
+plugins { kotlin("jvm") version "1.8.22" }
repositories {
mavenLocal()
mavenCentral()
}
-val ktfmtVersion = rootProject.file("../version.txt").readText().trim()
+val ktfmtVersion = rootProject.file("../stable_version.txt").readText().trim()
dependencies {
implementation("com.facebook:ktfmt:$ktfmtVersion")
@@ -35,11 +33,11 @@ dependencies {
testImplementation(kotlin("test-junit"))
}
+kotlin { jvmToolchain(11) }
+
tasks {
test { useJUnit() }
- withType<KotlinCompile>() { kotlinOptions.jvmTarget = "11" }
-
val packageFat by
creating(Zip::class) {
from(compileKotlin)
diff --git a/online_formatter/gradle/wrapper/gradle-wrapper.properties b/online_formatter/gradle/wrapper/gradle-wrapper.properties
index be52383..ac72c34 100644
--- a/online_formatter/gradle/wrapper/gradle-wrapper.properties
+++ b/online_formatter/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/pom.xml b/pom.xml
index 264e458..e379170 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,16 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0"
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.facebook</groupId>
<artifactId>ktfmt-parent</artifactId>
- <version>0.43</version>
+ <version>0.49</version>
<packaging>pom</packaging>
<name>Ktfmt Parent</name>
<description>A program that reformats Kotlin source code to comply with the common community standard for Kotlin code conventions.</description>
- <url>https://github.com/facebookincubator/ktfmt</url>
+ <url>https://github.com/facebook/ktfmt</url>
<inceptionYear>2019</inceptionYear>
<licenses>
<license>
@@ -24,9 +24,9 @@
</developer>
</developers>
<scm>
- <connection>scm:git:https://github.com/facebookincubator/ktfmt.git</connection>
- <developerConnection>scm:git:git@github.com:facebookincubator/ktfmt.git</developerConnection>
- <url>https://github.com/facebookincubator/ktfmt.git</url>
+ <connection>scm:git:https://github.com/facebook/ktfmt.git</connection>
+ <developerConnection>scm:git:git@github.com:facebook/ktfmt.git</developerConnection>
+ <url>https://github.com/facebook/ktfmt.git</url>
</scm>
<modules>
diff --git a/stable_version.txt b/stable_version.txt
new file mode 100644
index 0000000..a2ff373
--- /dev/null
+++ b/stable_version.txt
@@ -0,0 +1 @@
+0.49
diff --git a/version.txt b/version.txt
index 68f3790..a2ff373 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-0.43
+0.49
diff --git a/website/index.html b/website/index.html
index 151769b..0539d07 100644
--- a/website/index.html
+++ b/website/index.html
@@ -20,7 +20,7 @@
</head>
<body>
- <a id="gh-link" href="https://github.com/facebookincubator/ktfmt"
+ <a id="gh-link" href="https://github.com/facebook/ktfmt"
><img
loading="lazy"
width="149"
@@ -57,7 +57,7 @@
<li>
<a
class="nav-item"
- href="https://github.com/facebookincubator/ktfmt"
+ href="https://github.com/facebook/ktfmt"
>GitHub</a
>
</li>
diff --git a/website/package-lock.json b/website/package-lock.json
index 43d1698..90b7647 100644
--- a/website/package-lock.json
+++ b/website/package-lock.json
@@ -5000,9 +5000,9 @@
"dev": true
},
"node_modules/word-wrap": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
- "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
+ "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
@@ -9174,9 +9174,9 @@
"dev": true
},
"word-wrap": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
- "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
+ "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true
},
"wrap-ansi": {