diff options
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 @@ -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 } @@ -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 Binary files differindex 62d4c05..033e24c 100644 --- a/ktfmt_idea_plugin/gradle/wrapper/gradle-wrapper.jar +++ b/ktfmt_idea_plugin/gradle/wrapper/gradle-wrapper.jar 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 @@ -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": { |