From 2489ea00f0820339607311b6accdf0c2cfa0e663 Mon Sep 17 00:00:00 2001 From: Azat Abdullin Date: Tue, 18 Aug 2026 12:28:10 +0200 Subject: [PATCH 01/10] Extract CallFormatter --- core/api/ktfmt.api | 47 +- .../ktfmt/format/KotlinInputAstVisitor.kt | 710 +----------------- .../visitor/AbstractFormatterVisitor.kt | 4 +- .../ktfmt/format/visitor/CallFormatter.kt | 599 +++++++++++++++ .../ktfmt/format/visitor/FileFormatter.kt | 22 + .../format/visitor/KotlinAstFormatter.kt | 89 ++- 6 files changed, 750 insertions(+), 721 deletions(-) create mode 100644 core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt diff --git a/core/api/ktfmt.api b/core/api/ktfmt.api index ec9d61ab..c41c496a 100644 --- a/core/api/ktfmt.api +++ b/core/api/ktfmt.api @@ -184,18 +184,26 @@ public final class org/jetbrains/ktfmt/format/KotlinInput : com/google/googlejav } public class org/jetbrains/ktfmt/format/KotlinInputAstVisitor : org/jetbrains/ktfmt/format/visitor/AbstractFormatterVisitor, org/jetbrains/ktfmt/format/visitor/AnnotationFormatter, org/jetbrains/ktfmt/format/visitor/ExpressionFormatter, org/jetbrains/ktfmt/format/visitor/FileFormatter, org/jetbrains/ktfmt/format/visitor/ListFormatter, org/jetbrains/ktfmt/format/visitor/TypeFormatter { +public class org/jetbrains/ktfmt/format/KotlinInputAstVisitor : org/jetbrains/ktfmt/format/visitor/AbstractFormatterVisitor, org/jetbrains/ktfmt/format/visitor/CallFormatter, org/jetbrains/ktfmt/format/visitor/ExpressionFormatter, org/jetbrains/ktfmt/format/visitor/FileFormatter, org/jetbrains/ktfmt/format/visitor/ListFormatter, org/jetbrains/ktfmt/format/visitor/TypeFormatter { public fun (Lorg/jetbrains/ktfmt/format/FormattingOptions;Lcom/google/googlejavaformat/OpsBuilder;)V public fun formatCallElement (Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/psi/KtTypeArgumentList;Lorg/jetbrains/kotlin/psi/KtValueArgumentList;Ljava/util/List;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;)V - public fun formatChainedBlockLikeCall (Lorg/jetbrains/kotlin/psi/KtQualifiedExpression;Z)V - public fun formatChainedScopingFunction (Lorg/jetbrains/kotlin/psi/KtQualifiedExpression;Z)V - public fun formatLambdaOrScopingFunction (Lorg/jetbrains/kotlin/com/intellij/psi/PsiElement;Z)V + public fun getBlockIndent ()Lcom/google/googlejavaformat/Indent$Const; + public fun getBlockPlusExpressionBreakIndent ()Lcom/google/googlejavaformat/Indent$Const; public fun getBuilder ()Lcom/google/googlejavaformat/OpsBuilder; + public fun getDoubleExpressionBreakIndent ()Lcom/google/googlejavaformat/Indent$Const; public fun getExpressionBreakIndent ()Lcom/google/googlejavaformat/Indent$Const; public fun getExpressionBreakNegativeIndent ()Lcom/google/googlejavaformat/Indent$Const; public fun getInExpression ()Ljava/util/ArrayDeque; + public fun getInImport ()Z public fun getOptions ()Lorg/jetbrains/ktfmt/format/FormattingOptions; public fun visitArgument (Lorg/jetbrains/kotlin/psi/KtValueArgument;)V public fun visitArrayAccessExpression (Lorg/jetbrains/kotlin/psi/KtArrayAccessExpression;)V + public fun setInImport (Z)V + public fun visitAnnotatedExpression (Lorg/jetbrains/kotlin/psi/KtAnnotatedExpression;)V + public fun visitAnnotation (Lorg/jetbrains/kotlin/psi/KtAnnotation;)V + public fun visitAnnotationEntry (Lorg/jetbrains/kotlin/psi/KtAnnotationEntry;)V + public synthetic fun visitAnnotationUseSiteTarget (Lorg/jetbrains/kotlin/psi/KtAnnotationUseSiteTarget;Ljava/lang/Object;)Ljava/lang/Object; + public fun visitAnnotationUseSiteTarget (Lorg/jetbrains/kotlin/psi/KtAnnotationUseSiteTarget;Ljava/lang/Void;)Ljava/lang/Void; public fun visitBinaryExpression (Lorg/jetbrains/kotlin/psi/KtBinaryExpression;)V public fun visitBinaryWithTypeRHSExpression (Lorg/jetbrains/kotlin/psi/KtBinaryExpressionWithTypeRHS;)V public fun visitBlockExpression (Lorg/jetbrains/kotlin/psi/KtBlockExpression;)V @@ -223,7 +231,6 @@ public class org/jetbrains/ktfmt/format/KotlinInputAstVisitor : org/jetbrains/kt public fun visitImportDirective (Lorg/jetbrains/kotlin/psi/KtImportDirective;)V public fun visitIsExpression (Lorg/jetbrains/kotlin/psi/KtIsExpression;)V public fun visitLabeledExpression (Lorg/jetbrains/kotlin/psi/KtLabeledExpression;)V - public fun visitLambdaExpression (Lorg/jetbrains/kotlin/psi/KtLambdaExpression;)V public fun visitNamedFunction (Lorg/jetbrains/kotlin/psi/KtNamedFunction;)V public fun visitPackageDirective (Lorg/jetbrains/kotlin/psi/KtPackageDirective;)V public fun visitParameter (Lorg/jetbrains/kotlin/psi/KtParameter;)V @@ -232,7 +239,6 @@ public class org/jetbrains/ktfmt/format/KotlinInputAstVisitor : org/jetbrains/kt public fun visitPrefixExpression (Lorg/jetbrains/kotlin/psi/KtPrefixExpression;)V public fun visitPrimaryConstructor (Lorg/jetbrains/kotlin/psi/KtPrimaryConstructor;)V public fun visitProperty (Lorg/jetbrains/kotlin/psi/KtProperty;)V - public fun visitQualifiedExpression (Lorg/jetbrains/kotlin/psi/KtQualifiedExpression;)V public fun visitReferenceExpression (Lorg/jetbrains/kotlin/psi/KtReferenceExpression;)V public fun visitReturnExpression (Lorg/jetbrains/kotlin/psi/KtReturnExpression;)V public fun visitSecondaryConstructor (Lorg/jetbrains/kotlin/psi/KtSecondaryConstructor;)V @@ -445,6 +451,19 @@ public abstract interface class org/jetbrains/ktfmt/format/visitor/AnnotationFor public fun formatAnnotationUseSiteTarget (Lorg/jetbrains/kotlin/psi/KtAnnotationUseSiteTarget;)V } +public abstract interface class org/jetbrains/ktfmt/format/visitor/CallFormatter : org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter { + public fun formatArgument (Lorg/jetbrains/kotlin/psi/KtValueArgument;ZLcom/google/googlejavaformat/Output$BreakTag;)V + public fun formatArrayAccessExpression (Lorg/jetbrains/kotlin/psi/KtArrayAccessExpression;)V + public fun formatChainedBlockLikeCall (Lorg/jetbrains/kotlin/psi/KtQualifiedExpression;Z)V + public fun formatChainedScopingFunction (Lorg/jetbrains/kotlin/psi/KtQualifiedExpression;Z)V + public fun formatFunctionCall (Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/psi/KtTypeArgumentList;Lorg/jetbrains/kotlin/psi/KtValueArgumentList;Ljava/util/List;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;)V + public fun formatLambdaExpression (Lorg/jetbrains/kotlin/psi/KtLambdaExpression;Lcom/google/googlejavaformat/Output$BreakTag;)V + public fun formatLambdaOrScopingFunction (Lorg/jetbrains/kotlin/com/intellij/psi/PsiElement;Z)V + public fun formatQualifiedExpression (Lorg/jetbrains/kotlin/psi/KtQualifiedExpression;Z)V + public abstract fun getBlockPlusExpressionBreakIndent ()Lcom/google/googlejavaformat/Indent$Const; + public abstract fun getDoubleExpressionBreakIndent ()Lcom/google/googlejavaformat/Indent$Const; +} + public abstract interface class org/jetbrains/ktfmt/format/visitor/ExpressionFormatter : org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter { public fun formatInitializerExpression (Lorg/jetbrains/kotlin/psi/KtExpression;)V } @@ -452,15 +471,19 @@ public abstract interface class org/jetbrains/ktfmt/format/visitor/ExpressionFor public abstract interface class org/jetbrains/ktfmt/format/visitor/FileFormatter : org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter { public fun formatKtFile (Lorg/jetbrains/kotlin/psi/KtFile;)V public fun formatKtScript (Lorg/jetbrains/kotlin/psi/KtScript;)V + public fun formatStatement (Lorg/jetbrains/kotlin/com/intellij/psi/PsiElement;)V + public fun formatStatements ([Lorg/jetbrains/kotlin/com/intellij/psi/PsiElement;)V } public abstract interface class org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter { + public fun fail (Ljava/lang/String;)Ljava/lang/Void; + public static synthetic fun fail$default (Lorg/jetbrains/ktfmt/format/visitor/KotlinAstFormatter;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/Void; public abstract fun format (Lorg/jetbrains/kotlin/com/intellij/psi/PsiElement;)V public abstract fun formatAnnotatedExpression (Lorg/jetbrains/kotlin/psi/KtAnnotatedExpression;)V public abstract fun formatAnnotation (Lorg/jetbrains/kotlin/psi/KtAnnotation;)V public abstract fun formatAnnotationEntry (Lorg/jetbrains/kotlin/psi/KtAnnotationEntry;)V public abstract fun formatAnnotationUseSiteTarget (Lorg/jetbrains/kotlin/psi/KtAnnotationUseSiteTarget;)V - public fun formatArgument (Lorg/jetbrains/kotlin/psi/KtValueArgument;)V + public abstract fun formatArgument (Lorg/jetbrains/kotlin/psi/KtValueArgument;ZLcom/google/googlejavaformat/Output$BreakTag;)V public fun formatArrayAccessExpression (Lorg/jetbrains/kotlin/psi/KtArrayAccessExpression;)V public fun formatBinaryExpression (Lorg/jetbrains/kotlin/psi/KtBinaryExpression;)V public fun formatBinaryWithTypeRHSExpression (Lorg/jetbrains/kotlin/psi/KtBinaryExpressionWithTypeRHS;)V @@ -493,6 +516,8 @@ public abstract interface class org/jetbrains/ktfmt/format/visitor/KotlinAstForm public abstract fun formatFileAnnotationList (Lorg/jetbrains/kotlin/psi/KtFileAnnotationList;)V public fun formatFinallySection (Lorg/jetbrains/kotlin/psi/KtFinallySection;)V public fun formatForExpression (Lorg/jetbrains/kotlin/psi/KtForExpression;)V + public abstract fun formatFunctionCall (Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/psi/KtTypeArgumentList;Lorg/jetbrains/kotlin/psi/KtValueArgumentList;Ljava/util/List;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;)V + public static synthetic fun formatFunctionCall$default (Lorg/jetbrains/ktfmt/format/visitor/KotlinAstFormatter;Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/psi/KtTypeArgumentList;Lorg/jetbrains/kotlin/psi/KtValueArgumentList;Ljava/util/List;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;ILjava/lang/Object;)V public abstract fun formatFunctionType (Lorg/jetbrains/kotlin/psi/KtFunctionType;)V public fun formatIfExpression (Lorg/jetbrains/kotlin/psi/KtIfExpression;)V public fun formatImportDirective (Lorg/jetbrains/kotlin/psi/KtImportDirective;)V @@ -503,7 +528,7 @@ public abstract interface class org/jetbrains/ktfmt/format/visitor/KotlinAstForm public abstract fun formatKtFile (Lorg/jetbrains/kotlin/psi/KtFile;)V public abstract fun formatKtScript (Lorg/jetbrains/kotlin/psi/KtScript;)V public fun formatLabeledExpression (Lorg/jetbrains/kotlin/psi/KtLabeledExpression;)V - public fun formatLambdaExpression (Lorg/jetbrains/kotlin/psi/KtLambdaExpression;)V + public abstract fun formatLambdaExpression (Lorg/jetbrains/kotlin/psi/KtLambdaExpression;Lcom/google/googlejavaformat/Output$BreakTag;)V public abstract fun formatLambdaOrScopingFunction (Lorg/jetbrains/kotlin/com/intellij/psi/PsiElement;Z)V public static synthetic fun formatLambdaOrScopingFunction$default (Lorg/jetbrains/ktfmt/format/visitor/KotlinAstFormatter;Lorg/jetbrains/kotlin/com/intellij/psi/PsiElement;ZILjava/lang/Object;)V public abstract fun formatModifierList (Lorg/jetbrains/kotlin/psi/KtModifierList;)V @@ -517,11 +542,14 @@ public abstract interface class org/jetbrains/ktfmt/format/visitor/KotlinAstForm public fun formatPrefixExpression (Lorg/jetbrains/kotlin/psi/KtPrefixExpression;)V public fun formatPrimaryConstructor (Lorg/jetbrains/kotlin/psi/KtPrimaryConstructor;)V public fun formatProperty (Lorg/jetbrains/kotlin/psi/KtProperty;)V - public fun formatQualifiedExpression (Lorg/jetbrains/kotlin/psi/KtQualifiedExpression;)V + public fun formatQualifiedExpression (Lorg/jetbrains/kotlin/psi/KtQualifiedExpression;Z)V + public static synthetic fun formatQualifiedExpression$default (Lorg/jetbrains/ktfmt/format/visitor/KotlinAstFormatter;Lorg/jetbrains/kotlin/psi/KtQualifiedExpression;ZILjava/lang/Object;)V public fun formatReferenceExpression (Lorg/jetbrains/kotlin/psi/KtReferenceExpression;)V public fun formatReturnExpression (Lorg/jetbrains/kotlin/psi/KtReturnExpression;)V public fun formatSecondaryConstructor (Lorg/jetbrains/kotlin/psi/KtSecondaryConstructor;)V public fun formatSimpleNameExpression (Lorg/jetbrains/kotlin/psi/KtSimpleNameExpression;)V + public abstract fun formatStatement (Lorg/jetbrains/kotlin/com/intellij/psi/PsiElement;)V + public abstract fun formatStatements ([Lorg/jetbrains/kotlin/com/intellij/psi/PsiElement;)V public fun formatStringTemplateExpression (Lorg/jetbrains/kotlin/psi/KtStringTemplateExpression;)V public fun formatSuperExpression (Lorg/jetbrains/kotlin/psi/KtSuperExpression;)V public fun formatSuperTypeCallEntry (Lorg/jetbrains/kotlin/psi/KtSuperTypeCallEntry;)V @@ -544,12 +572,15 @@ public abstract interface class org/jetbrains/ktfmt/format/visitor/KotlinAstForm public fun formatWhenConditionWithExpression (Lorg/jetbrains/kotlin/psi/KtWhenConditionWithExpression;)V public fun formatWhenExpression (Lorg/jetbrains/kotlin/psi/KtWhenExpression;)V public fun formatWhileExpression (Lorg/jetbrains/kotlin/psi/KtWhileExpression;)V + public abstract fun getBlockIndent ()Lcom/google/googlejavaformat/Indent$Const; public abstract fun getBuilder ()Lcom/google/googlejavaformat/OpsBuilder; public abstract fun getExpressionBreakIndent ()Lcom/google/googlejavaformat/Indent$Const; public abstract fun getExpressionBreakNegativeIndent ()Lcom/google/googlejavaformat/Indent$Const; public abstract fun getInExpression ()Ljava/util/ArrayDeque; + public abstract fun getInImport ()Z public abstract fun getOptions ()Lorg/jetbrains/ktfmt/format/FormattingOptions; public fun markForPartialFormat ()V + public abstract fun setInImport (Z)V } public abstract interface class org/jetbrains/ktfmt/format/visitor/ListFormatter : org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter { diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/KotlinInputAstVisitor.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/KotlinInputAstVisitor.kt index 4f734abe..85d3dc78 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/KotlinInputAstVisitor.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/KotlinInputAstVisitor.kt @@ -29,11 +29,13 @@ import com.google.googlejavaformat.Output.BreakTag import java.util.ArrayDeque import java.util.Optional import kotlin.jvm.optionals.getOrNull -import org.jetbrains.kotlin.com.intellij.psi.PsiComment import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.com.intellij.psi.stubs.PsiFileStubImpl import org.jetbrains.kotlin.lexer.KtTokens -import org.jetbrains.kotlin.psi.KtArrayAccessExpression +import org.jetbrains.kotlin.psi.KtAnnotatedExpression +import org.jetbrains.kotlin.psi.KtAnnotation +import org.jetbrains.kotlin.psi.KtAnnotationEntry +import org.jetbrains.kotlin.psi.KtAnnotationUseSiteTarget import org.jetbrains.kotlin.psi.KtBackingField import org.jetbrains.kotlin.psi.KtBinaryExpression import org.jetbrains.kotlin.psi.KtBinaryExpressionWithTypeRHS @@ -57,7 +59,6 @@ import org.jetbrains.kotlin.psi.KtDelegatedSuperTypeEntry import org.jetbrains.kotlin.psi.KtDestructuringDeclaration import org.jetbrains.kotlin.psi.KtDestructuringDeclarationEntry import org.jetbrains.kotlin.psi.KtDoWhileExpression -import org.jetbrains.kotlin.psi.KtDotQualifiedExpression import org.jetbrains.kotlin.psi.KtElement import org.jetbrains.kotlin.psi.KtEnumEntry import org.jetbrains.kotlin.psi.KtExpression @@ -68,7 +69,6 @@ import org.jetbrains.kotlin.psi.KtImportDirective import org.jetbrains.kotlin.psi.KtIsExpression import org.jetbrains.kotlin.psi.KtLabelReferenceExpression import org.jetbrains.kotlin.psi.KtLabeledExpression -import org.jetbrains.kotlin.psi.KtLambdaArgument import org.jetbrains.kotlin.psi.KtLambdaExpression import org.jetbrains.kotlin.psi.KtModifierList import org.jetbrains.kotlin.psi.KtNamedFunction @@ -94,43 +94,33 @@ import org.jetbrains.kotlin.psi.KtThisExpression import org.jetbrains.kotlin.psi.KtThrowExpression import org.jetbrains.kotlin.psi.KtTryExpression import org.jetbrains.kotlin.psi.KtTypeAlias -import org.jetbrains.kotlin.psi.KtTypeArgumentList import org.jetbrains.kotlin.psi.KtTypeConstraintList import org.jetbrains.kotlin.psi.KtTypeParameterList import org.jetbrains.kotlin.psi.KtTypeReference import org.jetbrains.kotlin.psi.KtValueArgument -import org.jetbrains.kotlin.psi.KtValueArgumentList import org.jetbrains.kotlin.psi.KtWhenConditionInRange import org.jetbrains.kotlin.psi.KtWhenConditionIsPattern import org.jetbrains.kotlin.psi.KtWhenConditionWithExpression import org.jetbrains.kotlin.psi.KtWhenExpression import org.jetbrains.kotlin.psi.KtWhileExpression -import org.jetbrains.kotlin.psi.psiUtil.children 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 import org.jetbrains.ktfmt.format.visitor.AbstractFormatterVisitor import org.jetbrains.ktfmt.format.visitor.AnnotationFormatter +import org.jetbrains.ktfmt.format.visitor.CallFormatter import org.jetbrains.ktfmt.format.visitor.ExpressionFormatter import org.jetbrains.ktfmt.format.visitor.FileFormatter import org.jetbrains.ktfmt.format.visitor.ListFormatter import org.jetbrains.ktfmt.format.visitor.TypeFormatter -import org.jetbrains.ktfmt.format.visitor.asIndent import org.jetbrains.ktfmt.format.visitor.block -import org.jetbrains.ktfmt.format.visitor.chainParts -import org.jetbrains.ktfmt.format.visitor.chainRoot -import org.jetbrains.ktfmt.format.visitor.chainedSelectorsHaveValueArguments import org.jetbrains.ktfmt.format.visitor.fenceComments import org.jetbrains.ktfmt.format.visitor.hasEmptyParenthesis import org.jetbrains.ktfmt.format.visitor.hasLineBreakingCommentBefore -import org.jetbrains.ktfmt.format.visitor.hasSourceNewlineInLambdaBody import org.jetbrains.ktfmt.format.visitor.isBlockLikeCall import org.jetbrains.ktfmt.format.visitor.isChainedBlockLikeCall import org.jetbrains.ktfmt.format.visitor.isChainedScopingFunction -import org.jetbrains.ktfmt.format.visitor.isLambda import org.jetbrains.ktfmt.format.visitor.isLambdaOrScopingFunction -import org.jetbrains.ktfmt.format.visitor.isMultilineScopingFunction import org.jetbrains.ktfmt.format.visitor.sync import org.jetbrains.ktfmt.format.visitor.token import org.jetbrains.ktfmt.util.CONTEXT_PARAMETER_LIST @@ -143,13 +133,14 @@ open class KotlinInputAstVisitor( ) : AbstractFormatterVisitor(), AnnotationFormatter, + CallFormatter, ExpressionFormatter, FileFormatter, ListFormatter, TypeFormatter { /** Standard indentation for a block */ - private val blockIndent: Indent.Const = options.blockIndent.asIndent + override val blockIndent: Indent.Const = options.blockIndent.asIndent /** * Standard indentation for a long expression or function call, it is different than block @@ -157,10 +148,10 @@ open class KotlinInputAstVisitor( */ override val expressionBreakIndent: Indent.Const = options.continuationIndent.asIndent - private val blockPlusExpressionBreakIndent: Indent.Const = + override val blockPlusExpressionBreakIndent: Indent.Const = (options.blockIndent + options.continuationIndent).asIndent - private val doubleExpressionBreakIndent: Indent.Const = (options.continuationIndent * 2).asIndent + override val doubleExpressionBreakIndent: Indent.Const = (options.continuationIndent * 2).asIndent override val expressionBreakNegativeIndent: Indent.Const = (-options.continuationIndent).asIndent @@ -168,7 +159,7 @@ open class KotlinInputAstVisitor( override val inExpression = ArrayDeque(ImmutableList.of(false)) /** Tracks whether we are handling an import directive */ - private var inImport = false + override var inImport = false /** Example: `fun foo(n: Int) { println(n) }` */ override fun visitNamedFunction(function: KtNamedFunction) { @@ -319,26 +310,6 @@ open class KotlinInputAstVisitor( builder.token("}", blockIndent) } - private fun visitStatement(statement: PsiElement) { - builder.block(ZERO) { visit(statement) } - builder.guessToken(";") - } - - private fun visitStatements(statements: Array) { - var first = true - builder.guessToken(";") - for (statement in statements) { - builder.forcedBreak() - if (!first) { - builder.blankLineWanted(OpsBuilder.BlankLineWanted.PRESERVE) - } - first = false - markForPartialFormat() - visitStatement(statement) - markForPartialFormat() - } - } - override fun visitProperty(property: KtProperty) { builder.sync(property) builder.block(ZERO) { @@ -363,275 +334,10 @@ open class KotlinInputAstVisitor( } } - /** - * Example: "com.facebook.bla.bla" in imports or "a.b.c.d" in expressions. - * - * There's a few cases that are different. We deal with imports by keeping them on the same line. - * For regular chained expressions we go the left most descendant so we can start indentation only - * before the first break (a `.` or `?.`), and keep the seem indentation for this chain of calls. - */ - override fun visitQualifiedExpression(expression: KtQualifiedExpression) { - builder.sync(expression) - val receiver = expression.receiverExpression - when { - inImport -> { - visit(receiver) - val selectorExpression = expression.selectorExpression - if (selectorExpression != null) { - builder.token(".") - visit(selectorExpression) - } - } - receiver is KtStringTemplateExpression -> { - builder.block(expressionBreakIndent) { - visit(receiver) - builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO) - builder.token(expression.operationSign.value) - visit(expression.selectorExpression) - } - } - receiver is KtWhenExpression -> { - builder.block(ZERO) { - visit(receiver) - builder.token(expression.operationSign.value) - visit(expression.selectorExpression) - } - } - expression.isChainedScopingFunction && - expression.chainRoot.isMultilineScopingFunction && - !chainedSelectorsHaveValueArguments(expression) -> { - visitChainedScopingFunction(expression, emitLeadingBreak = false) - } - expression.isChainedBlockLikeCall -> { - visitChainedBlockLikeCall(expression, emitLeadingBreak = false) - } - else -> { - emitQualifiedExpression(expression) - } - } - } - - /** Extra data to help [emitQualifiedExpression] know when to open and close a group */ - private class GroupingInfo { - var groupOpenCount = 0 - var shouldCloseGroup = false - } - - /** - * Handles a chain of qualified expressions, i.e. `a[5].b!!.c()[4].f()` - * - * This is by far the most complicated part of this formatter. We start by breaking the expression - * to the steps it is executed in (which are in the opposite order of how the syntax tree is - * built). - * - * We then calculate information to know which parts need to be groups, and finally go part by - * part, emitting it to the [builder] while closing and opening groups. - */ - private fun emitQualifiedExpression(expression: KtExpression) { - val parts = expression.chainParts - // whether we want to make a lambda look like a block, this make Kotlin DSLs look as expected - val useBlockLikeLambdaStyle = parts.last().isLambda && parts.count { it.isLambda } == 1 - val groupingInfos = computeGroupingInfo(parts, useBlockLikeLambdaStyle) - builder.block(expressionBreakIndent) { - val nameTag = genSym() // allows adjusting arguments indentation if a break will be made - for ((index, ktExpression) in parts.withIndex()) { - if (ktExpression is KtQualifiedExpression) { - builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO, Optional.of(nameTag)) - } - repeat(groupingInfos[index].groupOpenCount) { builder.open(ZERO) } - when (ktExpression) { - is KtQualifiedExpression -> { - builder.token(ktExpression.operationSign.value) - val selectorExpression = ktExpression.selectorExpression - if (selectorExpression !is KtCallExpression) { - // selector is a simple field access - visit(selectorExpression) - if (groupingInfos[index].shouldCloseGroup) { - builder.close() - } - } else { - // selector is a function call, we may close a group after its name - // emit `doIt` from `doIt(1, 2) { it }` - visit(selectorExpression.calleeExpression) - // close groups according to instructions - if (groupingInfos[index].shouldCloseGroup) { - builder.close() - } - // close group due to last lambda to allow block-like style in `as.forEach { ... }` - val isTrailingLambda = useBlockLikeLambdaStyle && index == parts.size - 1 - if (isTrailingLambda) { - builder.close() - } - // A block-like (exploded) selector call is laid out like the last part: its - // arguments are indented once relative to the call itself, and its closing paren - // returns to the call's indent, even when chained selectors follow it. This only - // applies when trailing commas are preserved (the block-like style); when ktfmt - // manages trailing commas, exploded chained calls keep the regular extra indent. - val isLastPartOrBlockLikeCall = - index == parts.size - 1 || - !options.manageTrailingCommas && selectorExpression.isBlockLikeCall - val argsIndentElse = if (isLastPartOrBlockLikeCall) ZERO else expressionBreakIndent - val lambdaIndentElse = if (isTrailingLambda) expressionBreakNegativeIndent else ZERO - val negativeLambdaIndentElse = if (isTrailingLambda) expressionBreakIndent else ZERO - - // emit `(1, 2) { it }` from `doIt(1, 2) { it }` - visitCallElement( - null, - selectorExpression.typeArgumentList, - selectorExpression.valueArgumentList, - selectorExpression.lambdaArguments, - argumentsIndent = Indent.If.make(nameTag, expressionBreakIndent, argsIndentElse), - lambdaIndent = Indent.If.make(nameTag, ZERO, lambdaIndentElse), - negativeLambdaIndent = Indent.If.make(nameTag, ZERO, negativeLambdaIndentElse), - ) - } - } - is KtArrayAccessExpression -> { - visitArrayAccessBrackets(ktExpression) - builder.close() - } - is KtPostfixExpression -> { - builder.token(ktExpression.operationReference.text) - builder.close() - } - else -> { - check(index == 0) - visit(ktExpression) - } - } - } - } - } - - /** - * Generates the [GroupingInfo] array to go with an array of [KtQualifiedExpression] parts - * - * For example, the expression `a.b[2].c.d()` is made of four expressions: - * 1. [KtQualifiedExpression] `a.b[2].c . d()` (this will be `parts[4]`) - * 1. [KtQualifiedExpression] `a.b[2] . c` (this will be `parts[3]`) - * 2. [KtArrayAccessExpression] `a.b [2]` (this will be `parts[2]`) - * 3. [KtQualifiedExpression] `a . b` (this will be `parts[1]`) - * 4. [KtSimpleNameExpression] `a` (this will be `parts[0]`) - * - * Once in parts, these are in the reverse order. To render the array correct we need to make sure - * `b` and [2] are in a group so we avoid splitting them. To do so we need to open a group for `b` - * (that will be done in part 2), and always close a group for an array. - * - * Here is the same expression, with justified braces marking the groupings it will get: - * ``` - * a . b [2] . c . d () - * {a . b} --> Grouping `a.b` because it can be a package name or simple field access so we add 1 - * to the number of groups to open at groupingInfos[0], and mark to close a group at - * groupingInfos[1] - * {a . b [2]} --> Grouping `a.b` with `[2]`, since otherwise we may break inside the brackets - * instead of preferring breaks before dots. So we open a group at [0], but since - * we always close a group after brackets, we don't store that information. - * {c . d} --> another group to attach the first function name to the fields before it - * this time we don't start the group in the beginning, and use - * lastIndexToOpen to track the spot after the last time we stopped - * grouping. - * ``` - * - * The final expression with groupings: - * ``` - * {{a.b}[2]}.{c.d}() - * ``` - */ - private fun computeGroupingInfo( - parts: List, - useBlockLikeLambdaStyle: Boolean, - ): List { - val groupingInfos = List(parts.size) { GroupingInfo() } - var lastIndexToOpen = 0 - for ((index, part) in parts.withIndex()) { - when (part) { - is KtQualifiedExpression -> { - val receiverExpression = part.receiverExpression - val previous = - (receiverExpression as? KtQualifiedExpression)?.selectorExpression - ?: receiverExpression - val current = checkNotNull(part.selectorExpression) - if ( - lastIndexToOpen == 0 && - shouldGroupPartWithPrevious(parts, part, index, previous, current) - ) { - // this and the previous items should be grouped for better style - // we add another group to open in index 0 - groupingInfos[0].groupOpenCount++ - // we don't always close a group when emitting this node, so we need this flag to - // mark if we need to close a group - groupingInfos[index].shouldCloseGroup = true - } else { - // use this index in to open future groups for arrays and postfixes - // we will also stop grouping field access to the beginning of the expression - lastIndexToOpen = index - } - } - is KtArrayAccessExpression, - is KtPostfixExpression -> { - // we group these with the last item with a name, and we always close them - groupingInfos[lastIndexToOpen].groupOpenCount++ - } - } - } - if (useBlockLikeLambdaStyle) { - // a trailing lambda adds a group that we stop before emitting the lambda - groupingInfos[0].groupOpenCount++ - } - return groupingInfos - } - - /** Decide whether a [KtQualifiedExpression] part should be grouped with the previous part */ - private fun shouldGroupPartWithPrevious( - parts: List, - part: KtExpression, - index: Int, - previous: KtExpression, - current: KtExpression, - ): Boolean { - // this is the second, and the first is short, avoid `.` "hanging in air" - if (index == 1 && previous.text.length < options.continuationIndent) { - return true - } - // the previous part is `this` or `super` - if (previous is KtSuperExpression || previous is KtThisExpression) { - return true - } - // this and the previous part are a package name, type name, or property - if ( - previous is KtSimpleNameExpression && - current is KtSimpleNameExpression && - part is KtDotQualifiedExpression - ) { - return true - } - // this is `Foo` in `com.facebook.Foo`, so everything before it is a package name - if ( - current.text.first().isUpperCase() && - current is KtSimpleNameExpression && - part is KtDotQualifiedExpression - ) { - return true - } - // this is the `foo()` in `com.facebook.Foo.foo()` or in `Foo.foo()` - if ( - current is KtCallExpression && - (previous !is KtCallExpression) && - previous.text?.firstOrNull()?.isUpperCase() == true - ) { - return true - } - // this is an invocation and the last item, and the previous it not, i.e. `a.b.c()` - // keeping it grouped and splitting the arguments makes `a.b(...)` feel like `aab()` - return current is KtCallExpression && - previous !is KtCallExpression && - index == parts.indices.last - } - override fun visitCallExpression(callExpression: KtCallExpression) { builder.sync(callExpression) with(callExpression) { - visitCallElement( + formatFunctionCall( calleeExpression, typeArgumentList, valueArgumentList, @@ -660,180 +366,6 @@ open class KotlinInputAstVisitor( ) } - /** - * Examples `foo(a, b)`, `foo(a)`, `boo()`, `super(a)` - * - * @param lambdaIndent how to indent [lambdaArguments], if present - * @param negativeLambdaIndent the negative indentation of [lambdaIndent] - */ - private fun visitCallElement( - callee: KtExpression?, - typeArgumentList: KtTypeArgumentList?, - argumentList: KtValueArgumentList?, - lambdaArguments: List, - argumentsIndent: Indent = expressionBreakIndent, - lambdaIndent: Indent = ZERO, - negativeLambdaIndent: Indent = ZERO, - ) { - // Apply the lambda indent to the callee, type args, value args, and the lambda. - // This is undone for the first three by the negative lambda indent. - // This way they're in one block, and breaks in the argument list cause a break in the lambda. - builder.block(lambdaIndent) { - - // Used to keep track of whether or not we need to indent the lambda - // This is based on if there is a break in the argument list - var brokeBeforeBrace: BreakTag? = null - - builder.block(negativeLambdaIndent) { - visit(callee) - builder.block(argumentsIndent) { - builder.block(ZERO) { visit(typeArgumentList) } - if (argumentList != null) { - brokeBeforeBrace = formatValueArgumentList(argumentList) - } - } - } - 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]) - } - } - } - - /** Example `{ 1 + 1 }` (as lambda) or `{ (x, y) -> x + y }` */ - override fun visitLambdaExpression(lambdaExpression: KtLambdaExpression) { - visitLambdaExpressionInternal(lambdaExpression, brokeBeforeBrace = null) - } - - /** - * The internal version of [visitLambdaExpression]. - * - * @param brokeBeforeBrace used for tracking if a break was taken right before the lambda - * expression. Useful for scoping functions where we want good looking indentation. For example, - * here we have correct indentation before `bar()` and `car()` because we can detect the break - * after the equals: - * ``` - * fun foo() = - * coroutineScope { x -> - * bar() - * car() - * } - * ``` - */ - private fun visitLambdaExpressionInternal( - lambdaExpression: KtLambdaExpression, - brokeBeforeBrace: BreakTag?, - ) { - builder.sync(lambdaExpression) - - val valueParams = lambdaExpression.valueParameters - val hasParams = valueParams.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 { - if (brokeBeforeBrace == null) return onFalse - return Indent.If.make(brokeBeforeBrace, onTrue, onFalse) - } - - /** - * Enable correct formatting of the `fun foo() = scope {` syntax. - * - * We can't denote the lambda (+ scope function) as a block, since (for multiline lambdas) the - * rectangle rule would force the entire lambda onto a lower line. Instead, we conditionally - * indent all the interior levels of the lambda based on whether we had to break before the - * opening brace (or scope function). This mimics the look of a block when the break is taken. - * - * These conditional indents should not be used inside interior blocks, since that would apply - * the condition twice. - */ - val bracePlusBlockIndent = ifBrokeBeforeBrace(blockPlusExpressionBreakIndent, blockIndent) - val bracePlusExpressionIndent = - ifBrokeBeforeBrace(doubleExpressionBreakIndent, expressionBreakIndent) - val bracePlusZeroIndent = ifBrokeBeforeBrace(expressionBreakIndent, ZERO) - - builder.token("{") - - if (hasParams || hasArrow) { - builder.space() - builder.block(bracePlusExpressionIndent) { formatCommaSeparatedList(valueParams) } - builder.block(bracePlusBlockIndent) { - if (lambdaExpression.functionLiteral.valueParameterList?.trailingComma != null) { - builder.token(",") - builder.forcedBreak() - } else if (hasParams) { - builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO) - } - builder.token("->") - } - } - - if (hasParams || hasArrow || hasStatements || hasComments) { - builder.breakOp(Doc.FillMode.UNIFIED, " ", bracePlusZeroIndent) - } - - if (hasStatements) { - builder.breakOp(Doc.FillMode.UNIFIED, "", bracePlusBlockIndent) - builder.block(bracePlusBlockIndent) { - builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO) - - val shouldForceMultiline = - options.preserveLambdaBreaks && lambdaExpression.hasSourceNewlineInLambdaBody - - if ( - !shouldForceMultiline && - expressionStatements.size == 1 && - expressionStatements.first() !is KtReturnExpression && - !bodyExpression.startsWithComment() - ) { - visitStatement(expressionStatements[0]) - } else { - visitStatements(expressionStatements) - } - builder.breakOp(Doc.FillMode.UNIFIED, " ", bracePlusZeroIndent) - } - } else if (hasComments) { - val blockComments = - bodyExpression.children().filter { it is PsiComment && it.text.startsWith("/*") }.toList() - builder.breakOp(Doc.FillMode.UNIFIED, "", bracePlusBlockIndent) - builder.block(bracePlusBlockIndent) { - builder.fenceComments() - builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO) - if (blockComments.size == 1) { - builder.token(blockComments[0].text) - } else { - for ((i, comment) in blockComments.withIndex()) { - if (i > 0) { - builder.forcedBreak() - } - builder.token(comment.text) - } - } - builder.breakOp(Doc.FillMode.UNIFIED, " ", bracePlusZeroIndent) - } - } - - if (hasParams || hasArrow || hasStatements || hasComments) { - // If we had to break in the body, ensure there is a break before the closing brace - builder.breakOp(Doc.FillMode.UNIFIED, "", bracePlusZeroIndent) - } - builder.block(bracePlusZeroIndent) { - builder.fenceComments() - builder.token("}", blockIndent) - } - } - /** Example `this` or `this@Foo` */ override fun visitThisExpression(expression: KtThisExpression) { builder.sync(expression) @@ -862,55 +394,6 @@ open class KotlinInputAstVisitor( } } - /** Example `a` in `foo(a)`, or `*a`, or `limit = 50` */ - override fun visitArgument(argument: KtValueArgument) { - visitArgumentInternal( - argument, - wrapInBlock = true, - brokeBeforeBrace = null, - ) - } - - /** - * The internal version of [visitArgument]. - * - * @param wrapInBlock if true places the argument expression in a block. - */ - private fun visitArgumentInternal( - argument: KtValueArgument, - wrapInBlock: Boolean, - brokeBeforeBrace: BreakTag?, - ) { - builder.sync(argument) - val hasArgName = argument.getArgumentName() != null - val isLambda = argument.getArgumentExpression() is KtLambdaExpression - if (hasArgName) { - visit(argument.getArgumentName()) - builder.space() - builder.token("=") - if (isLambda) { - builder.space() - } - } - val indent = if (hasArgName && !isLambda) expressionBreakIndent else ZERO - builder.block(indent, isEnabled = wrapInBlock) { - if (hasArgName && !isLambda) { - builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO) - } - if (argument.isSpread) { - builder.token("*") - } - if (isLambda) { - visitLambdaExpressionInternal( - argument.getArgumentExpression() as KtLambdaExpression, - brokeBeforeBrace = brokeBeforeBrace, - ) - } else { - visit(argument.getArgumentExpression()) - } - } - } - override fun visitReferenceExpression(expression: KtReferenceExpression) { builder.sync(expression) builder.token(expression.text) @@ -944,7 +427,7 @@ open class KotlinInputAstVisitor( visit(expression.left) builder.space() builder.token(expression.operationReference.text) - visitLambdaOrScopingFunction(expression.right) + formatLambdaOrScopingFunction(expression.right) return } @@ -1124,12 +607,12 @@ open class KotlinInputAstVisitor( builder.space() visit(delegate) } else if (delegateExpr != null && delegateExpr.isChainedScopingFunction) { - visitChainedScopingFunction(delegateExpr, emitLeadingBreak = true) + formatChainedScopingFunction(delegateExpr, emitLeadingBreak = true) } else if (delegateExpr.isBlockLikeCall) { builder.space() visit(delegate) } else if (delegateExpr != null && delegateExpr.isChainedBlockLikeCall) { - visitChainedBlockLikeCall(delegateExpr, emitLeadingBreak = true) + formatChainedBlockLikeCall(delegateExpr, emitLeadingBreak = true) } else { builder.breakOp(Doc.FillMode.UNIFIED, " ", expressionBreakIndent) builder.block(expressionBreakIndent) { @@ -1243,125 +726,6 @@ open class KotlinInputAstVisitor( } } - override fun formatChainedBlockLikeCall( - expression: KtQualifiedExpression, - emitLeadingBreak: Boolean, - ) { - visitChainedBlockLikeCall(expression, emitLeadingBreak) - } - - private fun visitChainedBlockLikeCall( - expression: KtQualifiedExpression, - emitLeadingBreak: Boolean, - ) { - val parts = expression.chainParts - if (emitLeadingBreak) { - builder.space() - } - visit(parts[0]) - - builder.block(expressionBreakIndent) { - for (i in 1 until parts.size) { - val part = parts[i] as KtQualifiedExpression - builder.forcedBreak() - builder.token(part.operationSign.value) - val selectorExpression = part.selectorExpression - if (selectorExpression is KtCallExpression) { - visit(selectorExpression.calleeExpression) - visitCallElement( - null, - selectorExpression.typeArgumentList, - selectorExpression.valueArgumentList, - selectorExpression.lambdaArguments, - ) - } else { - visit(selectorExpression) - } - } - } - } - - override fun formatChainedScopingFunction( - expression: KtQualifiedExpression, - emitLeadingBreak: Boolean, - ) { - visitChainedScopingFunction(expression, emitLeadingBreak) - } - - private fun visitChainedScopingFunction( - expression: KtQualifiedExpression, - emitLeadingBreak: Boolean, - ) { - val parts = expression.chainParts - val root = parts[0] - val forceBreakBeforeChain = root.isMultilineScopingFunction - - visitLambdaOrScopingFunction(root, emitLeadingBreak = emitLeadingBreak) - - // The break before each selector must stay outside the block below, at the same level as - // the lambda, so that it is taken exactly when the lambda breaks. Inside the block it - // would fire only when the selector itself is too long, so a lambda broken by max width - // would keep its selector on the closing brace's line — and the next format pass, seeing - // a multiline lambda in the source, would force the selector onto its own line (#640). - val fillMode = if (forceBreakBeforeChain) Doc.FillMode.FORCED else Doc.FillMode.UNIFIED - for (i in 1 until parts.size) { - val part = parts[i] as KtQualifiedExpression - builder.breakOp(fillMode, "", expressionBreakIndent) - builder.block(expressionBreakIndent) { - builder.token(part.operationSign.value) - val selectorExpression = part.selectorExpression - if (selectorExpression is KtCallExpression) { - visit(selectorExpression.calleeExpression) - visitCallElement( - null, - selectorExpression.typeArgumentList, - selectorExpression.valueArgumentList, - selectorExpression.lambdaArguments, - ) - } else { - visit(selectorExpression) - } - } - } - } - - override fun formatLambdaOrScopingFunction(expr: PsiElement?, emitLeadingBreak: Boolean) { - visitLambdaOrScopingFunction(expr, emitLeadingBreak) - } - - private fun visitLambdaOrScopingFunction(expr: PsiElement?, emitLeadingBreak: Boolean = true) { - val breakToExpr = genSym() - val breakSpace = if (emitLeadingBreak) " " else "" - builder.breakOp( - Doc.FillMode.INDEPENDENT, - breakSpace, - expressionBreakIndent, - Optional.of(breakToExpr), - ) - - var carry = expr - if (carry is KtQualifiedExpression && carry.receiverExpression is KtSimpleNameExpression) { - visit(carry.receiverExpression) - builder.token(carry.operationSign.value) - carry = carry.selectorExpression - } - 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 - } - - throw AssertionError(carry) - } - override fun visitClassOrObject(classOrObject: KtClassOrObject) { builder.sync(classOrObject) val contextReceiverList = @@ -1459,7 +823,7 @@ open class KotlinInputAstVisitor( // visitReferenceExpression, but calls visitElement instead. builder.block(ZERO) { builder.token(if (call.isCallToThis) "this" else "super") - visitCallElement( + formatFunctionCall( null, call.typeArgumentList, call.valueArgumentList, @@ -1542,7 +906,7 @@ open class KotlinInputAstVisitor( override fun visitSuperTypeCallEntry(call: KtSuperTypeCallEntry) { builder.sync(call) - visitCallElement(call.calleeExpression, null, call.valueArgumentList, call.lambdaArguments) + formatFunctionCall(call.calleeExpression, null, call.valueArgumentList, call.lambdaArguments) } /** @@ -1674,7 +1038,7 @@ open class KotlinInputAstVisitor( override fun visitBlockExpression(expression: KtBlockExpression) { builder.sync(expression) - emitBracedBlock(expression) { children -> visitStatements(children) } + emitBracedBlock(expression) { children -> formatStatements(children) } } override fun visitWhenConditionWithExpression(condition: KtWhenConditionWithExpression) { @@ -1737,34 +1101,6 @@ open class KotlinInputAstVisitor( } } - /** Example `a[3]`, `b["a", 5]` or `a.b.c[4]` */ - override fun visitArrayAccessExpression(expression: KtArrayAccessExpression) { - builder.sync(expression) - if (expression.arrayExpression is KtQualifiedExpression) { - emitQualifiedExpression(expression) - } else { - visit(expression.arrayExpression) - visitArrayAccessBrackets(expression) - } - } - - /** - * Example `[3]` in `a[3]` or `a[3].b` Separated since it needs to be used from a top level array - * expression (`a[3]`) and from within a qualified chain (`a[3].b) - */ - private fun visitArrayAccessBrackets(expression: KtArrayAccessExpression) { - builder.block(expressionBreakIndent) { - formatCommaSeparatedList( - expression.indexExpressions, - forceMultiline = expression.trailingComma != null, - wrapInBlock = true, - prefix = "[", - postfix = "]", - breakBeforePostfix = false, - ) - } - } - /** Example `val (a, b: Int) = Pair(1, 2)` or `val [a, b] = Pair(1, 2)` */ override fun visitDestructuringDeclaration(destructuringDeclaration: KtDestructuringDeclaration) { builder.sync(destructuringDeclaration) @@ -1938,7 +1274,7 @@ open class KotlinInputAstVisitor( builder.sync(expression) val receiverExpression = expression.receiverExpression if (receiverExpression is KtCallExpression) { - visitCallElement( + formatFunctionCall( receiverExpression.calleeExpression, receiverExpression.typeArgumentList, receiverExpression.valueArgumentList, @@ -2110,16 +1446,6 @@ open class KotlinInputAstVisitor( builder.checkClosed(previous) } - /** - * Throws a formatting error - * - * This is used as `expr ?: fail()` to avoid using the !! operator and provide better error - * messages. - */ - private fun fail(message: String = "Unexpected"): Nothing { - throw FormattingError(builder.diagnostic(message)) - } - /** Helper function to improve readability */ private fun visit(element: PsiElement?) { element?.accept(this) diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/AbstractFormatterVisitor.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/AbstractFormatterVisitor.kt index b0e28ae7..333d1501 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/AbstractFormatterVisitor.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/AbstractFormatterVisitor.kt @@ -173,7 +173,7 @@ abstract class AbstractFormatterVisitor : KtTreeVisitorVoid(), KotlinAstFormatte } override fun visitArgument(argument: KtValueArgument) { - formatArgument(argument) + formatArgument(argument, wrapInBlock = true, brokeBeforeBrace = null) } override fun visitSuperTypeList(list: KtSuperTypeList) { @@ -221,7 +221,7 @@ abstract class AbstractFormatterVisitor : KtTreeVisitorVoid(), KotlinAstFormatte } override fun visitLambdaExpression(lambdaExpression: KtLambdaExpression) { - formatLambdaExpression(lambdaExpression) + formatLambdaExpression(lambdaExpression, brokeBeforeBrace = null) } override fun visitThisExpression(expression: KtThisExpression) { diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt new file mode 100644 index 00000000..1bbdeaf6 --- /dev/null +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt @@ -0,0 +1,599 @@ +package org.jetbrains.ktfmt.format.visitor + +import com.google.googlejavaformat.Doc +import com.google.googlejavaformat.Indent +import com.google.googlejavaformat.Indent.Const.ZERO +import com.google.googlejavaformat.OpsBuilder +import com.google.googlejavaformat.Output.BreakTag +import java.util.Optional +import org.jetbrains.kotlin.com.intellij.psi.PsiComment +import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.psi.KtArrayAccessExpression +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtDotQualifiedExpression +import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.psi.KtLabeledExpression +import org.jetbrains.kotlin.psi.KtLambdaArgument +import org.jetbrains.kotlin.psi.KtLambdaExpression +import org.jetbrains.kotlin.psi.KtPostfixExpression +import org.jetbrains.kotlin.psi.KtQualifiedExpression +import org.jetbrains.kotlin.psi.KtReturnExpression +import org.jetbrains.kotlin.psi.KtSimpleNameExpression +import org.jetbrains.kotlin.psi.KtStringTemplateExpression +import org.jetbrains.kotlin.psi.KtSuperExpression +import org.jetbrains.kotlin.psi.KtThisExpression +import org.jetbrains.kotlin.psi.KtTypeArgumentList +import org.jetbrains.kotlin.psi.KtValueArgument +import org.jetbrains.kotlin.psi.KtValueArgumentList +import org.jetbrains.kotlin.psi.KtWhenExpression +import org.jetbrains.kotlin.psi.psiUtil.children +import org.jetbrains.kotlin.psi.psiUtil.startsWithComment +import org.jetbrains.ktfmt.format.ParseError + +interface CallFormatter : KotlinAstFormatter { + val doubleExpressionBreakIndent: Indent.Const + val blockPlusExpressionBreakIndent: Indent.Const + + override fun formatArgument( + argument: KtValueArgument, + wrapInBlock: Boolean, + brokeBeforeBrace: BreakTag?, + ) { + builder.sync(argument) + val hasArgName = argument.getArgumentName() != null + val isLambda = argument.getArgumentExpression() is KtLambdaExpression + if (hasArgName) { + format(argument.getArgumentName()) + builder.space() + builder.token("=") + if (isLambda) { + builder.space() + } + } + val indent = if (hasArgName && !isLambda) expressionBreakIndent else ZERO + builder.block(indent, isEnabled = wrapInBlock) { + if (hasArgName && !isLambda) { + builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO) + } + if (argument.isSpread) { + builder.token("*") + } + if (isLambda) { + formatLambdaExpression( + argument.getArgumentExpression() as KtLambdaExpression, + brokeBeforeBrace = brokeBeforeBrace, + ) + } else { + format(argument.getArgumentExpression()) + } + } + } + + override fun formatFunctionCall( + callee: KtExpression?, + typeArgumentList: KtTypeArgumentList?, + argumentList: KtValueArgumentList?, + lambdaArguments: List, + argumentsIndent: Indent, + lambdaIndent: Indent, + negativeLambdaIndent: Indent, + ) { + // Apply the lambda indent to the callee, type args, value args, and the lambda. + // This is undone for the first three by the negative lambda indent. + // This way they're in one block, and breaks in the argument list cause a break in the lambda. + builder.block(lambdaIndent) { + + // Used to keep track of whether or not we need to indent the lambda + // This is based on if there is a break in the argument list + var brokeBeforeBrace: BreakTag? = null + + builder.block(negativeLambdaIndent) { + format(callee) + builder.block(argumentsIndent) { + builder.block(ZERO) { format(typeArgumentList) } + if (argumentList != null) { + brokeBeforeBrace = formatValueArgumentList(argumentList) + } + } + } + when (lambdaArguments.size) { + 0 -> {} + 1 -> { + builder.space() + formatArgument( + lambdaArguments.single(), + wrapInBlock = false, + brokeBeforeBrace = brokeBeforeBrace, + ) + } + else -> throw ParseError("Maximum one trailing lambda is allowed", lambdaArguments[1]) + } + } + } + + override fun formatLambdaExpression( + lambdaExpression: KtLambdaExpression, + brokeBeforeBrace: BreakTag?, + ) { + builder.sync(lambdaExpression) + + val valueParams = lambdaExpression.valueParameters + val hasParams = valueParams.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 { + if (brokeBeforeBrace == null) return onFalse + return Indent.If.make(brokeBeforeBrace, onTrue, onFalse) + } + + /** + * Enable correct formatting of the `fun foo() = scope {` syntax. + * + * We can't denote the lambda (+ scope function) as a block, since (for multiline lambdas) the + * rectangle rule would force the entire lambda onto a lower line. Instead, we conditionally + * indent all the interior levels of the lambda based on whether we had to break before the + * opening brace (or scope function). This mimics the look of a block when the break is taken. + * + * These conditional indents should not be used inside interior blocks, since that would apply + * the condition twice. + */ + val bracePlusBlockIndent = ifBrokeBeforeBrace(blockPlusExpressionBreakIndent, blockIndent) + val bracePlusExpressionIndent = + ifBrokeBeforeBrace(doubleExpressionBreakIndent, expressionBreakIndent) + val bracePlusZeroIndent = ifBrokeBeforeBrace(expressionBreakIndent, ZERO) + + builder.token("{") + + if (hasParams || hasArrow) { + builder.space() + builder.block(bracePlusExpressionIndent) { formatCommaSeparatedList(valueParams) } + builder.block(bracePlusBlockIndent) { + if (lambdaExpression.functionLiteral.valueParameterList?.trailingComma != null) { + builder.token(",") + builder.forcedBreak() + } else if (hasParams) { + builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO) + } + builder.token("->") + } + } + + if (hasParams || hasArrow || hasStatements || hasComments) { + builder.breakOp(Doc.FillMode.UNIFIED, " ", bracePlusZeroIndent) + } + + if (hasStatements) { + builder.breakOp(Doc.FillMode.UNIFIED, "", bracePlusBlockIndent) + builder.block(bracePlusBlockIndent) { + builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO) + + val shouldForceMultiline = + options.preserveLambdaBreaks && lambdaExpression.hasSourceNewlineInLambdaBody + + if ( + !shouldForceMultiline && + expressionStatements.size == 1 && + expressionStatements.first() !is KtReturnExpression && + !bodyExpression.startsWithComment() + ) { + formatStatement(expressionStatements[0]) + } else { + formatStatements(expressionStatements) + } + builder.breakOp(Doc.FillMode.UNIFIED, " ", bracePlusZeroIndent) + } + } else if (hasComments) { + val blockComments = + bodyExpression.children().filter { it is PsiComment && it.text.startsWith("/*") }.toList() + builder.breakOp(Doc.FillMode.UNIFIED, "", bracePlusBlockIndent) + builder.block(bracePlusBlockIndent) { + builder.fenceComments() + builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO) + if (blockComments.size == 1) { + builder.token(blockComments[0].text) + } else { + for ((i, comment) in blockComments.withIndex()) { + if (i > 0) { + builder.forcedBreak() + } + builder.token(comment.text) + } + } + builder.breakOp(Doc.FillMode.UNIFIED, " ", bracePlusZeroIndent) + } + } + + if (hasParams || hasArrow || hasStatements || hasComments) { + // If we had to break in the body, ensure there is a break before the closing brace + builder.breakOp(Doc.FillMode.UNIFIED, "", bracePlusZeroIndent) + } + builder.block(bracePlusZeroIndent) { + builder.fenceComments() + builder.token("}", blockIndent) + } + } + + override fun formatChainedBlockLikeCall( + expression: KtQualifiedExpression, + emitLeadingBreak: Boolean, + ) { + val parts = expression.chainParts + if (emitLeadingBreak) { + builder.space() + } + format(parts[0]) + + builder.block(expressionBreakIndent) { + for (i in 1 until parts.size) { + val part = parts[i] as KtQualifiedExpression + builder.forcedBreak() + builder.token(part.operationSign.value) + val selectorExpression = part.selectorExpression + if (selectorExpression is KtCallExpression) { + format(selectorExpression.calleeExpression) + formatFunctionCall( + null, + selectorExpression.typeArgumentList, + selectorExpression.valueArgumentList, + selectorExpression.lambdaArguments, + ) + } else { + format(selectorExpression) + } + } + } + } + + override fun formatChainedScopingFunction( + expression: KtQualifiedExpression, + emitLeadingBreak: Boolean, + ) { + val parts = expression.chainParts + val root = parts[0] + val forceBreakBeforeChain = root.isMultilineScopingFunction + + formatLambdaOrScopingFunction(root, emitLeadingBreak = emitLeadingBreak) + + // The break before each selector must stay outside the block below, at the same level as + // the lambda, so that it is taken exactly when the lambda breaks. Inside the block it + // would fire only when the selector itself is too long, so a lambda broken by max width + // would keep its selector on the closing brace's line — and the next format pass, seeing + // a multiline lambda in the source, would force the selector onto its own line (#640). + val fillMode = if (forceBreakBeforeChain) Doc.FillMode.FORCED else Doc.FillMode.UNIFIED + for (i in 1 until parts.size) { + val part = parts[i] as KtQualifiedExpression + builder.breakOp(fillMode, "", expressionBreakIndent) + builder.block(expressionBreakIndent) { + builder.token(part.operationSign.value) + val selectorExpression = part.selectorExpression + if (selectorExpression is KtCallExpression) { + format(selectorExpression.calleeExpression) + formatFunctionCall( + null, + selectorExpression.typeArgumentList, + selectorExpression.valueArgumentList, + selectorExpression.lambdaArguments, + ) + } else { + format(selectorExpression) + } + } + } + } + + override fun formatLambdaOrScopingFunction(expr: PsiElement?, emitLeadingBreak: Boolean) { + val breakToExpr = BreakTag() + val breakSpace = if (emitLeadingBreak) " " else "" + builder.breakOp( + Doc.FillMode.INDEPENDENT, + breakSpace, + expressionBreakIndent, + Optional.of(breakToExpr), + ) + + var carry = expr + if (carry is KtQualifiedExpression && carry.receiverExpression is KtSimpleNameExpression) { + format(carry.receiverExpression) + builder.token(carry.operationSign.value) + carry = carry.selectorExpression + } + if (carry is KtCallExpression) { + format(carry.calleeExpression) + builder.space() + carry = carry.lambdaArguments[0].getArgumentExpression() + } + if (carry is KtLabeledExpression) { + format(carry.labelQualifier) + carry = carry.baseExpression ?: fail() + } + if (carry is KtLambdaExpression) { + formatLambdaExpression(carry, brokeBeforeBrace = breakToExpr) + return + } + + throw AssertionError(carry) + } + + override fun formatQualifiedExpression(expression: KtQualifiedExpression, extraRules: Boolean) { + + builder.sync(expression) + val receiver = expression.receiverExpression + when { + extraRules && inImport -> { + format(receiver) + val selectorExpression = expression.selectorExpression + if (selectorExpression != null) { + builder.token(".") + format(selectorExpression) + } + } + extraRules && receiver is KtStringTemplateExpression -> { + builder.block(expressionBreakIndent) { + format(receiver) + builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO) + builder.token(expression.operationSign.value) + format(expression.selectorExpression) + } + } + extraRules && receiver is KtWhenExpression -> { + builder.block(ZERO) { + format(receiver) + builder.token(expression.operationSign.value) + format(expression.selectorExpression) + } + } + extraRules && + expression.isChainedScopingFunction && + expression.chainRoot.isMultilineScopingFunction && + !chainedSelectorsHaveValueArguments(expression) -> { + formatChainedScopingFunction(expression, emitLeadingBreak = false) + } + extraRules && expression.isChainedBlockLikeCall -> { + formatChainedBlockLikeCall(expression, emitLeadingBreak = false) + } + else -> { + emitQualifiedExpression(expression) + } + } + } + + private fun emitQualifiedExpression(expression: KtExpression) { + + val parts = expression.chainParts + // whether we want to make a lambda look like a block, this make Kotlin DSLs look as expected + val useBlockLikeLambdaStyle = parts.last().isLambda && parts.count { it.isLambda } == 1 + val groupingInfos = computeGroupingInfo(parts, useBlockLikeLambdaStyle) + builder.block(expressionBreakIndent) { + val nameTag = BreakTag() // allows adjusting arguments indentation if a break will be made + for ((index, ktExpression) in parts.withIndex()) { + if (ktExpression is KtQualifiedExpression) { + builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO, Optional.of(nameTag)) + } + repeat(groupingInfos[index].groupOpenCount) { builder.open(ZERO) } + when (ktExpression) { + is KtQualifiedExpression -> { + builder.token(ktExpression.operationSign.value) + val selectorExpression = ktExpression.selectorExpression + if (selectorExpression !is KtCallExpression) { + // selector is a simple field access + format(selectorExpression) + if (groupingInfos[index].shouldCloseGroup) { + builder.close() + } + } else { + // selector is a function call, we may close a group after its name + // emit `doIt` from `doIt(1, 2) { it }` + format(selectorExpression.calleeExpression) + // close groups according to instructions + if (groupingInfos[index].shouldCloseGroup) { + builder.close() + } + // close group due to last lambda to allow block-like style in `as.forEach { ... }` + val isTrailingLambda = useBlockLikeLambdaStyle && index == parts.size - 1 + if (isTrailingLambda) { + builder.close() + } + // A block-like (exploded) selector call is laid out like the last part: its + // arguments are indented once relative to the call itself, and its closing paren + // returns to the call's indent, even when chained selectors follow it. This only + // applies when trailing commas are preserved (the block-like style); when ktfmt + // manages trailing commas, exploded chained calls keep the regular extra indent. + val isLastPartOrBlockLikeCall = + index == parts.size - 1 || + !options.manageTrailingCommas && selectorExpression.isBlockLikeCall + val argsIndentElse = if (isLastPartOrBlockLikeCall) ZERO else expressionBreakIndent + val lambdaIndentElse = if (isTrailingLambda) expressionBreakNegativeIndent else ZERO + val negativeLambdaIndentElse = if (isTrailingLambda) expressionBreakIndent else ZERO + + // emit `(1, 2) { it }` from `doIt(1, 2) { it }` + formatFunctionCall( + null, + selectorExpression.typeArgumentList, + selectorExpression.valueArgumentList, + selectorExpression.lambdaArguments, + argumentsIndent = Indent.If.make(nameTag, expressionBreakIndent, argsIndentElse), + lambdaIndent = Indent.If.make(nameTag, ZERO, lambdaIndentElse), + negativeLambdaIndent = Indent.If.make(nameTag, ZERO, negativeLambdaIndentElse), + ) + } + } + is KtArrayAccessExpression -> { + builder.block(expressionBreakIndent) { + formatCommaSeparatedList( + ktExpression.indexExpressions, + forceMultiline = ktExpression.trailingComma != null, + wrapInBlock = true, + prefix = "[", + postfix = "]", + breakBeforePostfix = false, + ) + } + builder.close() + } + is KtPostfixExpression -> { + builder.token(ktExpression.operationReference.text) + builder.close() + } + else -> { + check(index == 0) + format(ktExpression) + } + } + } + } + } + + /** Extra data to help [formatQualifiedExpression] know when to open and close a group */ + private class GroupingInfo { + var groupOpenCount = 0 + var shouldCloseGroup = false + } + + /** + * Generates the [GroupingInfo] array to go with an array of [KtQualifiedExpression] parts + * + * For example, the expression `a.b[2].c.d()` is made of four expressions: + * 1. [KtQualifiedExpression] `a.b[2].c . d()` (this will be `parts[4]`) + * 1. [KtQualifiedExpression] `a.b[2] . c` (this will be `parts[3]`) + * 2. [KtArrayAccessExpression] `a.b [2]` (this will be `parts[2]`) + * 3. [KtQualifiedExpression] `a . b` (this will be `parts[1]`) + * 4. [KtSimpleNameExpression] `a` (this will be `parts[0]`) + * + * Once in parts, these are in the reverse order. To render the array correct we need to make sure + * `b` and [2] are in a group so we avoid splitting them. To do so we need to open a group for `b` + * (that will be done in part 2), and always close a group for an array. + * + * Here is the same expression, with justified braces marking the groupings it will get: + * ``` + * a . b [2] . c . d () + * {a . b} --> Grouping `a.b` because it can be a package name or simple field access so we add 1 + * to the number of groups to open at groupingInfos[0], and mark to close a group at + * groupingInfos[1] + * {a . b [2]} --> Grouping `a.b` with `[2]`, since otherwise we may break inside the brackets + * instead of preferring breaks before dots. So we open a group at [0], but since + * we always close a group after brackets, we don't store that information. + * {c . d} --> another group to attach the first function name to the fields before it + * this time we don't start the group in the beginning, and use + * lastIndexToOpen to track the spot after the last time we stopped + * grouping. + * ``` + * + * The final expression with groupings: + * ``` + * {{a.b}[2]}.{c.d}() + * ``` + */ + private fun computeGroupingInfo( + parts: List, + useBlockLikeLambdaStyle: Boolean, + ): List { + val groupingInfos = List(parts.size) { GroupingInfo() } + var lastIndexToOpen = 0 + for ((index, part) in parts.withIndex()) { + when (part) { + is KtQualifiedExpression -> { + val receiverExpression = part.receiverExpression + val previous = + (receiverExpression as? KtQualifiedExpression)?.selectorExpression + ?: receiverExpression + val current = checkNotNull(part.selectorExpression) + if ( + lastIndexToOpen == 0 && + shouldGroupPartWithPrevious(parts, part, index, previous, current) + ) { + // this and the previous items should be grouped for better style + // we add another group to open in index 0 + groupingInfos[0].groupOpenCount++ + // we don't always close a group when emitting this node, so we need this flag to + // mark if we need to close a group + groupingInfos[index].shouldCloseGroup = true + } else { + // use this index in to open future groups for arrays and postfixes + // we will also stop grouping field access to the beginning of the expression + lastIndexToOpen = index + } + } + is KtArrayAccessExpression, + is KtPostfixExpression -> { + // we group these with the last item with a name, and we always close them + groupingInfos[lastIndexToOpen].groupOpenCount++ + } + } + } + if (useBlockLikeLambdaStyle) { + // a trailing lambda adds a group that we stop before emitting the lambda + groupingInfos[0].groupOpenCount++ + } + return groupingInfos + } + + /** Decide whether a [KtQualifiedExpression] part should be grouped with the previous part */ + private fun shouldGroupPartWithPrevious( + parts: List, + part: KtExpression, + index: Int, + previous: KtExpression, + current: KtExpression, + ): Boolean { + // this is the second, and the first is short, avoid `.` "hanging in air" + if (index == 1 && previous.text.length < options.continuationIndent) { + return true + } + // the previous part is `this` or `super` + if (previous is KtSuperExpression || previous is KtThisExpression) { + return true + } + // this and the previous part are a package name, type name, or property + if ( + previous is KtSimpleNameExpression && + current is KtSimpleNameExpression && + part is KtDotQualifiedExpression + ) { + return true + } + // this is `Foo` in `com.facebook.Foo`, so everything before it is a package name + if ( + current.text.first().isUpperCase() && + current is KtSimpleNameExpression && + part is KtDotQualifiedExpression + ) { + return true + } + // this is the `foo()` in `com.facebook.Foo.foo()` or in `Foo.foo()` + if ( + current is KtCallExpression && + (previous !is KtCallExpression) && + previous.text?.firstOrNull()?.isUpperCase() == true + ) { + return true + } + // this is an invocation and the last item, and the previous it not, i.e. `a.b.c()` + // keeping it grouped and splitting the arguments makes `a.b(...)` feel like `aab()` + return current is KtCallExpression && + previous !is KtCallExpression && + index == parts.indices.last + } + + override fun formatArrayAccessExpression(expression: KtArrayAccessExpression) { + builder.sync(expression) + if (expression.arrayExpression is KtQualifiedExpression) { + emitQualifiedExpression(expression) + } else { + format(expression.arrayExpression) + builder.block(expressionBreakIndent) { + formatCommaSeparatedList( + expression.indexExpressions, + forceMultiline = expression.trailingComma != null, + wrapInBlock = true, + prefix = "[", + postfix = "]", + breakBeforePostfix = false, + ) + } + } + } +} diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/FileFormatter.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/FileFormatter.kt index f08e179a..a0f2af6c 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/FileFormatter.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/FileFormatter.kt @@ -1,7 +1,9 @@ package org.jetbrains.ktfmt.format.visitor +import com.google.googlejavaformat.Indent.Const.ZERO import com.google.googlejavaformat.OpsBuilder import org.jetbrains.kotlin.com.intellij.psi.PsiComment +import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.KtScript @@ -68,4 +70,24 @@ interface FileFormatter : KotlinAstFormatter { } markForPartialFormat() } + + override fun formatStatement(statement: PsiElement) { + builder.block(ZERO) { format(statement) } + builder.guessToken(";") + } + + override fun formatStatements(statements: Array) { + var first = true + builder.guessToken(";") + for (statement in statements) { + builder.forcedBreak() + if (!first) { + builder.blankLineWanted(OpsBuilder.BlankLineWanted.PRESERVE) + } + first = false + markForPartialFormat() + formatStatement(statement) + markForPartialFormat() + } + } } diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter.kt index 3bc559dc..ddba5cc4 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter.kt @@ -1,5 +1,6 @@ package org.jetbrains.ktfmt.format.visitor +import com.google.googlejavaformat.FormattingError import com.google.googlejavaformat.Indent import com.google.googlejavaformat.Indent.Const.ZERO import com.google.googlejavaformat.OpsBuilder @@ -128,18 +129,25 @@ interface KotlinAstFormatter { val options: FormattingOptions val builder: OpsBuilder + val blockIndent: Indent.Const val expressionBreakIndent: Indent.Const val expressionBreakNegativeIndent: Indent.Const /** A record of whether we have visited into an expression. */ val inExpression: ArrayDeque + var inImport: Boolean + fun format(element: PsiElement?) fun formatKtFile(file: KtFile) fun formatKtScript(script: KtScript) + fun formatStatement(statement: PsiElement) + + fun formatStatements(statements: Array) + fun formatNamedFunction(function: KtNamedFunction) { TODO("Unreachable code path") } @@ -192,9 +200,12 @@ interface KotlinAstFormatter { TODO("Unreachable code path") } - fun formatArgument(argument: KtValueArgument) { - TODO("Unreachable code path") - } + /** @param wrapInBlock if true places the argument expression in a block. */ + fun formatArgument( + argument: KtValueArgument, + wrapInBlock: Boolean, + brokeBeforeBrace: BreakTag?, + ) fun formatSuperTypeList(list: KtSuperTypeList) @@ -222,7 +233,7 @@ interface KotlinAstFormatter { TODO("Unreachable code path") } - fun formatQualifiedExpression(expression: KtQualifiedExpression) { + fun formatQualifiedExpression(expression: KtQualifiedExpression, extraRules: Boolean = true) { TODO("Unreachable code path") } @@ -230,9 +241,23 @@ interface KotlinAstFormatter { TODO("Unreachable code path") } - fun formatLambdaExpression(lambdaExpression: KtLambdaExpression) { - TODO("Unreachable code path") - } + /** + * @param brokeBeforeBrace used for tracking if a break was taken right before the lambda + * expression. Useful for scoping functions where we want good looking indentation. For example, + * here we have correct indentation before `bar()` and `car()` because we can detect the break + * after the equals: + * ``` + * fun foo() = + * coroutineScope { x -> + * bar() + * car() + * } + * ``` + */ + fun formatLambdaExpression( + lambdaExpression: KtLambdaExpression, + brokeBeforeBrace: BreakTag?, + ) fun formatThisExpression(expression: KtThisExpression) { TODO("Unreachable code path") @@ -499,6 +524,22 @@ interface KotlinAstFormatter { /** See [isLambdaOrScopingFunction] for examples. */ fun formatLambdaOrScopingFunction(expr: PsiElement?, emitLeadingBreak: Boolean = true) + /** + * Examples `foo(a, b)`, `foo(a)`, `boo()`, `super(a)` + * + * @param lambdaIndent how to indent [lambdaArguments], if present + * @param negativeLambdaIndent the negative indentation of [lambdaIndent] + */ + fun formatFunctionCall( + callee: KtExpression?, + typeArgumentList: KtTypeArgumentList?, + argumentList: KtValueArgumentList?, + lambdaArguments: List, + argumentsIndent: Indent = expressionBreakIndent, + lambdaIndent: Indent = ZERO, + negativeLambdaIndent: Indent = ZERO, + ) + /** * Emit a `foo(\n ...,\n).bar().baz()` style chain whose innermost receiver is a block-like * multiline call: render the receiver call normally (so its closing paren sits at the surrounding @@ -522,6 +563,22 @@ interface KotlinAstFormatter { emitLeadingBreak: Boolean, ) + /** + * Examples `foo(a, b)`, `foo(a)`, `boo()`, `super(a)` + * + * @param lambdaIndent how to indent [lambdaArguments], if present + * @param negativeLambdaIndent the negative indentation of [lambdaIndent] + */ + fun formatCallElement( + callee: KtExpression?, + typeArgumentList: KtTypeArgumentList?, + argumentList: KtValueArgumentList?, + lambdaArguments: List, + argumentsIndent: Indent = expressionBreakIndent, + lambdaIndent: Indent = ZERO, + negativeLambdaIndent: Indent = ZERO, + ) + /** * markForPartialFormat is used to delineate the smallest areas of code that must be formatted * together. @@ -536,18 +593,12 @@ interface KotlinAstFormatter { } /** - * Examples `foo(a, b)`, `foo(a)`, `boo()`, `super(a)` + * Throws a formatting error * - * @param lambdaIndent how to indent [lambdaArguments], if present - * @param negativeLambdaIndent the negative indentation of [lambdaIndent] + * This is used as `expr ?: fail()` to avoid using the !! operator and provide better error + * messages. */ - fun formatCallElement( - callee: KtExpression?, - typeArgumentList: KtTypeArgumentList?, - argumentList: KtValueArgumentList?, - lambdaArguments: List, - argumentsIndent: Indent = expressionBreakIndent, - lambdaIndent: Indent = ZERO, - negativeLambdaIndent: Indent = ZERO, - ) + fun fail(message: String = "Unexpected"): Nothing { + throw FormattingError(builder.diagnostic(message)) + } } From 217b2ec6d394a84c47d549374eb2e3be5c9555c8 Mon Sep 17 00:00:00 2001 From: Azat Abdullin Date: Tue, 18 Aug 2026 14:47:59 +0200 Subject: [PATCH 02/10] Introduce custom Indentation as a wrapper for GJF's `Indent` to simplify some logic --- .../ktfmt/format/KotlinInputAstVisitor.kt | 37 ++- .../ktfmt/format/visitor/CallFormatter.kt | 260 ++++-------------- .../ktfmt/format/visitor/FileFormatter.kt | 3 +- .../ktfmt/format/visitor/GroupInfo.kt | 127 +++++++++ .../ktfmt/format/visitor/Indentation.kt | 39 +++ .../format/visitor/KotlinAstFormatter.kt | 21 +- .../ktfmt/format/visitor/ListFormatter.kt | 8 +- .../ktfmt/format/visitor/OpsUtils.kt | 31 ++- .../ktfmt/format/visitor/PsiUtils.kt | 38 ++- 9 files changed, 300 insertions(+), 264 deletions(-) create mode 100644 core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/GroupInfo.kt create mode 100644 core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/Indentation.kt diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/KotlinInputAstVisitor.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/KotlinInputAstVisitor.kt index 85d3dc78..d282df64 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/KotlinInputAstVisitor.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/KotlinInputAstVisitor.kt @@ -22,8 +22,6 @@ import com.google.common.base.Throwables import com.google.common.collect.ImmutableList import com.google.googlejavaformat.Doc 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.BreakTag import java.util.ArrayDeque @@ -111,16 +109,21 @@ import org.jetbrains.ktfmt.format.visitor.AnnotationFormatter import org.jetbrains.ktfmt.format.visitor.CallFormatter import org.jetbrains.ktfmt.format.visitor.ExpressionFormatter import org.jetbrains.ktfmt.format.visitor.FileFormatter +import org.jetbrains.ktfmt.format.visitor.Indentation +import org.jetbrains.ktfmt.format.visitor.Indentation.Companion.ZERO import org.jetbrains.ktfmt.format.visitor.ListFormatter import org.jetbrains.ktfmt.format.visitor.TypeFormatter import org.jetbrains.ktfmt.format.visitor.block +import org.jetbrains.ktfmt.format.visitor.breakOp import org.jetbrains.ktfmt.format.visitor.fenceComments +import org.jetbrains.ktfmt.format.visitor.forcedBreak import org.jetbrains.ktfmt.format.visitor.hasEmptyParenthesis import org.jetbrains.ktfmt.format.visitor.hasLineBreakingCommentBefore import org.jetbrains.ktfmt.format.visitor.isBlockLikeCall import org.jetbrains.ktfmt.format.visitor.isChainedBlockLikeCall import org.jetbrains.ktfmt.format.visitor.isChainedScopingFunction import org.jetbrains.ktfmt.format.visitor.isLambdaOrScopingFunction +import org.jetbrains.ktfmt.format.visitor.open import org.jetbrains.ktfmt.format.visitor.sync import org.jetbrains.ktfmt.format.visitor.token import org.jetbrains.ktfmt.util.CONTEXT_PARAMETER_LIST @@ -140,20 +143,14 @@ open class KotlinInputAstVisitor( TypeFormatter { /** Standard indentation for a block */ - override val blockIndent: Indent.Const = options.blockIndent.asIndent + override val blockIndent: Indentation.Const = Indentation.Const(options.blockIndent) /** * Standard indentation for a long expression or function call, it is different than block * indentation on purpose */ - override val expressionBreakIndent: Indent.Const = options.continuationIndent.asIndent - - override val blockPlusExpressionBreakIndent: Indent.Const = - (options.blockIndent + options.continuationIndent).asIndent - - override val doubleExpressionBreakIndent: Indent.Const = (options.continuationIndent * 2).asIndent - - override val expressionBreakNegativeIndent: Indent.Const = (-options.continuationIndent).asIndent + override val expressionBreakIndent: Indentation.Const = + Indentation.Const(options.continuationIndent) /** A record of whether we have visited into an expression. */ override val inExpression = ArrayDeque(ImmutableList.of(false)) @@ -264,7 +261,7 @@ open class KotlinInputAstVisitor( } emitTypeOrDelegationCall { builder.space() - builder.block(expressionBreakNegativeIndent) { visit(typeOrDelegationCall) } + builder.block(-expressionBreakIndent) { visit(typeOrDelegationCall) } } } } @@ -296,7 +293,12 @@ open class KotlinInputAstVisitor( bodyBlockExpression: PsiElement, emitChildren: (Array) -> Unit, ) { - builder.token("{", Doc.Token.RealOrImaginary.REAL, blockIndent, Optional.of(blockIndent)) + builder.token( + "{", + Doc.Token.RealOrImaginary.REAL, + blockIndent.indent, + Optional.of(blockIndent.indent), + ) val statements = bodyBlockExpression.children if (statements.isNotEmpty()) { builder.block(blockIndent) { @@ -927,7 +929,12 @@ open class KotlinInputAstVisitor( emitKeywordWithCondition("when", expression.subjectExpression) builder.space() - builder.token("{", Doc.Token.RealOrImaginary.REAL, blockIndent, Optional.of(blockIndent)) + builder.token( + "{", + Doc.Token.RealOrImaginary.REAL, + blockIndent.indent, + Optional.of(blockIndent.indent), + ) expression.entries.forEachIndexed { index, whenEntry -> builder.block(blockIndent) { @@ -1477,7 +1484,7 @@ open class KotlinInputAstVisitor( builder.block(expressionBreakIndent) { builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO) visit(condition) - builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakNegativeIndent) + builder.breakOp(Doc.FillMode.UNIFIED, "", -expressionBreakIndent) } } else { builder.block(ZERO) { visit(condition) } diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt index 1bbdeaf6..5d2b2631 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt @@ -1,8 +1,6 @@ package org.jetbrains.ktfmt.format.visitor import com.google.googlejavaformat.Doc -import com.google.googlejavaformat.Indent -import com.google.googlejavaformat.Indent.Const.ZERO import com.google.googlejavaformat.OpsBuilder import com.google.googlejavaformat.Output.BreakTag import java.util.Optional @@ -10,18 +8,13 @@ import org.jetbrains.kotlin.com.intellij.psi.PsiComment import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.psi.KtArrayAccessExpression import org.jetbrains.kotlin.psi.KtCallExpression -import org.jetbrains.kotlin.psi.KtDotQualifiedExpression import org.jetbrains.kotlin.psi.KtExpression -import org.jetbrains.kotlin.psi.KtLabeledExpression import org.jetbrains.kotlin.psi.KtLambdaArgument import org.jetbrains.kotlin.psi.KtLambdaExpression import org.jetbrains.kotlin.psi.KtPostfixExpression import org.jetbrains.kotlin.psi.KtQualifiedExpression import org.jetbrains.kotlin.psi.KtReturnExpression -import org.jetbrains.kotlin.psi.KtSimpleNameExpression import org.jetbrains.kotlin.psi.KtStringTemplateExpression -import org.jetbrains.kotlin.psi.KtSuperExpression -import org.jetbrains.kotlin.psi.KtThisExpression import org.jetbrains.kotlin.psi.KtTypeArgumentList import org.jetbrains.kotlin.psi.KtValueArgument import org.jetbrains.kotlin.psi.KtValueArgumentList @@ -29,11 +22,9 @@ import org.jetbrains.kotlin.psi.KtWhenExpression import org.jetbrains.kotlin.psi.psiUtil.children import org.jetbrains.kotlin.psi.psiUtil.startsWithComment import org.jetbrains.ktfmt.format.ParseError +import org.jetbrains.ktfmt.format.visitor.Indentation.Companion.ZERO interface CallFormatter : KotlinAstFormatter { - val doubleExpressionBreakIndent: Indent.Const - val blockPlusExpressionBreakIndent: Indent.Const - override fun formatArgument( argument: KtValueArgument, wrapInBlock: Boolean, @@ -74,9 +65,8 @@ interface CallFormatter : KotlinAstFormatter { typeArgumentList: KtTypeArgumentList?, argumentList: KtValueArgumentList?, lambdaArguments: List, - argumentsIndent: Indent, - lambdaIndent: Indent, - negativeLambdaIndent: Indent, + argumentsIndent: Indentation, + lambdaIndent: Indentation, ) { // Apply the lambda indent to the callee, type args, value args, and the lambda. // This is undone for the first three by the negative lambda indent. @@ -87,7 +77,7 @@ interface CallFormatter : KotlinAstFormatter { // This is based on if there is a break in the argument list var brokeBeforeBrace: BreakTag? = null - builder.block(negativeLambdaIndent) { + builder.block(-lambdaIndent) { format(callee) builder.block(argumentsIndent) { builder.block(ZERO) { format(typeArgumentList) } @@ -125,9 +115,9 @@ interface CallFormatter : KotlinAstFormatter { val hasComments = bodyExpression.children().any { it is PsiComment } val hasArrow = lambdaExpression.functionLiteral.arrow != null - fun ifBrokeBeforeBrace(onTrue: Indent, onFalse: Indent): Indent { + fun ifBrokeBeforeBrace(onTrue: Indentation, onFalse: Indentation): Indentation { if (brokeBeforeBrace == null) return onFalse - return Indent.If.make(brokeBeforeBrace, onTrue, onFalse) + return Indentation.Conditional(brokeBeforeBrace, onTrue, onFalse) } /** @@ -141,9 +131,9 @@ interface CallFormatter : KotlinAstFormatter { * These conditional indents should not be used inside interior blocks, since that would apply * the condition twice. */ - val bracePlusBlockIndent = ifBrokeBeforeBrace(blockPlusExpressionBreakIndent, blockIndent) + val bracePlusBlockIndent = ifBrokeBeforeBrace(blockIndent + expressionBreakIndent, blockIndent) val bracePlusExpressionIndent = - ifBrokeBeforeBrace(doubleExpressionBreakIndent, expressionBreakIndent) + ifBrokeBeforeBrace(expressionBreakIndent * 2, expressionBreakIndent) val bracePlusZeroIndent = ifBrokeBeforeBrace(expressionBreakIndent, ZERO) builder.token("{") @@ -295,35 +285,26 @@ interface CallFormatter : KotlinAstFormatter { Optional.of(breakToExpr), ) - var carry = expr - if (carry is KtQualifiedExpression && carry.receiverExpression is KtSimpleNameExpression) { - format(carry.receiverExpression) - builder.token(carry.operationSign.value) - carry = carry.selectorExpression + val scopingLambda = expr.scopingLambda ?: throw AssertionError(expr) + scopingLambda.receiverExpression?.let { + format(it) + builder.token(scopingLambda.operation!!.value) } - if (carry is KtCallExpression) { - format(carry.calleeExpression) + scopingLambda.calleeExpression?.let { + format(it) builder.space() - carry = carry.lambdaArguments[0].getArgumentExpression() - } - if (carry is KtLabeledExpression) { - format(carry.labelQualifier) - carry = carry.baseExpression ?: fail() } - if (carry is KtLambdaExpression) { - formatLambdaExpression(carry, brokeBeforeBrace = breakToExpr) - return + scopingLambda.labeledExpression?.let { + format(it.labelQualifier) } - - throw AssertionError(carry) + formatLambdaExpression(scopingLambda.lambdaExpression, breakToExpr) } - override fun formatQualifiedExpression(expression: KtQualifiedExpression, extraRules: Boolean) { - + override fun formatQualifiedExpression(expression: KtQualifiedExpression) { builder.sync(expression) val receiver = expression.receiverExpression when { - extraRules && inImport -> { + inImport -> { format(receiver) val selectorExpression = expression.selectorExpression if (selectorExpression != null) { @@ -331,7 +312,7 @@ interface CallFormatter : KotlinAstFormatter { format(selectorExpression) } } - extraRules && receiver is KtStringTemplateExpression -> { + receiver is KtStringTemplateExpression -> { builder.block(expressionBreakIndent) { format(receiver) builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO) @@ -339,20 +320,19 @@ interface CallFormatter : KotlinAstFormatter { format(expression.selectorExpression) } } - extraRules && receiver is KtWhenExpression -> { + receiver is KtWhenExpression -> { builder.block(ZERO) { format(receiver) builder.token(expression.operationSign.value) format(expression.selectorExpression) } } - extraRules && - expression.isChainedScopingFunction && + expression.isChainedScopingFunction && expression.chainRoot.isMultilineScopingFunction && !chainedSelectorsHaveValueArguments(expression) -> { formatChainedScopingFunction(expression, emitLeadingBreak = false) } - extraRules && expression.isChainedBlockLikeCall -> { + expression.isChainedBlockLikeCall -> { formatChainedBlockLikeCall(expression, emitLeadingBreak = false) } else -> { @@ -362,18 +342,17 @@ interface CallFormatter : KotlinAstFormatter { } private fun emitQualifiedExpression(expression: KtExpression) { - val parts = expression.chainParts // whether we want to make a lambda look like a block, this make Kotlin DSLs look as expected - val useBlockLikeLambdaStyle = parts.last().isLambda && parts.count { it.isLambda } == 1 - val groupingInfos = computeGroupingInfo(parts, useBlockLikeLambdaStyle) + val hasTrailingLambda = parts.last().isLambda && parts.count { it.isLambda } == 1 + val groupingInfos = computeGroupingInfo(parts, hasTrailingLambda, expressionBreakIndent) builder.block(expressionBreakIndent) { val nameTag = BreakTag() // allows adjusting arguments indentation if a break will be made - for ((index, ktExpression) in parts.withIndex()) { + for ((ktExpression, groupOpenCount, shouldCloseGroup, isLast) in groupingInfos) { if (ktExpression is KtQualifiedExpression) { builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO, Optional.of(nameTag)) } - repeat(groupingInfos[index].groupOpenCount) { builder.open(ZERO) } + repeat(groupOpenCount) { builder.open(ZERO) } when (ktExpression) { is KtQualifiedExpression -> { builder.token(ktExpression.operationSign.value) @@ -381,7 +360,7 @@ interface CallFormatter : KotlinAstFormatter { if (selectorExpression !is KtCallExpression) { // selector is a simple field access format(selectorExpression) - if (groupingInfos[index].shouldCloseGroup) { + if (shouldCloseGroup) { builder.close() } } else { @@ -389,11 +368,11 @@ interface CallFormatter : KotlinAstFormatter { // emit `doIt` from `doIt(1, 2) { it }` format(selectorExpression.calleeExpression) // close groups according to instructions - if (groupingInfos[index].shouldCloseGroup) { + if (shouldCloseGroup) { builder.close() } // close group due to last lambda to allow block-like style in `as.forEach { ... }` - val isTrailingLambda = useBlockLikeLambdaStyle && index == parts.size - 1 + val isTrailingLambda = hasTrailingLambda && isLast if (isTrailingLambda) { builder.close() } @@ -403,11 +382,9 @@ interface CallFormatter : KotlinAstFormatter { // applies when trailing commas are preserved (the block-like style); when ktfmt // manages trailing commas, exploded chained calls keep the regular extra indent. val isLastPartOrBlockLikeCall = - index == parts.size - 1 || - !options.manageTrailingCommas && selectorExpression.isBlockLikeCall + isLast || !options.manageTrailingCommas && selectorExpression.isBlockLikeCall val argsIndentElse = if (isLastPartOrBlockLikeCall) ZERO else expressionBreakIndent - val lambdaIndentElse = if (isTrailingLambda) expressionBreakNegativeIndent else ZERO - val negativeLambdaIndentElse = if (isTrailingLambda) expressionBreakIndent else ZERO + val lambdaIndentElse = if (isTrailingLambda) -expressionBreakIndent else ZERO // emit `(1, 2) { it }` from `doIt(1, 2) { it }` formatFunctionCall( @@ -415,23 +392,14 @@ interface CallFormatter : KotlinAstFormatter { selectorExpression.typeArgumentList, selectorExpression.valueArgumentList, selectorExpression.lambdaArguments, - argumentsIndent = Indent.If.make(nameTag, expressionBreakIndent, argsIndentElse), - lambdaIndent = Indent.If.make(nameTag, ZERO, lambdaIndentElse), - negativeLambdaIndent = Indent.If.make(nameTag, ZERO, negativeLambdaIndentElse), + argumentsIndent = + Indentation.Conditional(nameTag, expressionBreakIndent, argsIndentElse), + lambdaIndent = Indentation.Conditional(nameTag, ZERO, lambdaIndentElse), ) } } is KtArrayAccessExpression -> { - builder.block(expressionBreakIndent) { - formatCommaSeparatedList( - ktExpression.indexExpressions, - forceMultiline = ktExpression.trailingComma != null, - wrapInBlock = true, - prefix = "[", - postfix = "]", - breakBeforePostfix = false, - ) - } + formatArrayAccessBrackets(ktExpression) builder.close() } is KtPostfixExpression -> { @@ -439,7 +407,6 @@ interface CallFormatter : KotlinAstFormatter { builder.close() } else -> { - check(index == 0) format(ktExpression) } } @@ -447,153 +414,26 @@ interface CallFormatter : KotlinAstFormatter { } } - /** Extra data to help [formatQualifiedExpression] know when to open and close a group */ - private class GroupingInfo { - var groupOpenCount = 0 - var shouldCloseGroup = false - } - - /** - * Generates the [GroupingInfo] array to go with an array of [KtQualifiedExpression] parts - * - * For example, the expression `a.b[2].c.d()` is made of four expressions: - * 1. [KtQualifiedExpression] `a.b[2].c . d()` (this will be `parts[4]`) - * 1. [KtQualifiedExpression] `a.b[2] . c` (this will be `parts[3]`) - * 2. [KtArrayAccessExpression] `a.b [2]` (this will be `parts[2]`) - * 3. [KtQualifiedExpression] `a . b` (this will be `parts[1]`) - * 4. [KtSimpleNameExpression] `a` (this will be `parts[0]`) - * - * Once in parts, these are in the reverse order. To render the array correct we need to make sure - * `b` and [2] are in a group so we avoid splitting them. To do so we need to open a group for `b` - * (that will be done in part 2), and always close a group for an array. - * - * Here is the same expression, with justified braces marking the groupings it will get: - * ``` - * a . b [2] . c . d () - * {a . b} --> Grouping `a.b` because it can be a package name or simple field access so we add 1 - * to the number of groups to open at groupingInfos[0], and mark to close a group at - * groupingInfos[1] - * {a . b [2]} --> Grouping `a.b` with `[2]`, since otherwise we may break inside the brackets - * instead of preferring breaks before dots. So we open a group at [0], but since - * we always close a group after brackets, we don't store that information. - * {c . d} --> another group to attach the first function name to the fields before it - * this time we don't start the group in the beginning, and use - * lastIndexToOpen to track the spot after the last time we stopped - * grouping. - * ``` - * - * The final expression with groupings: - * ``` - * {{a.b}[2]}.{c.d}() - * ``` - */ - private fun computeGroupingInfo( - parts: List, - useBlockLikeLambdaStyle: Boolean, - ): List { - val groupingInfos = List(parts.size) { GroupingInfo() } - var lastIndexToOpen = 0 - for ((index, part) in parts.withIndex()) { - when (part) { - is KtQualifiedExpression -> { - val receiverExpression = part.receiverExpression - val previous = - (receiverExpression as? KtQualifiedExpression)?.selectorExpression - ?: receiverExpression - val current = checkNotNull(part.selectorExpression) - if ( - lastIndexToOpen == 0 && - shouldGroupPartWithPrevious(parts, part, index, previous, current) - ) { - // this and the previous items should be grouped for better style - // we add another group to open in index 0 - groupingInfos[0].groupOpenCount++ - // we don't always close a group when emitting this node, so we need this flag to - // mark if we need to close a group - groupingInfos[index].shouldCloseGroup = true - } else { - // use this index in to open future groups for arrays and postfixes - // we will also stop grouping field access to the beginning of the expression - lastIndexToOpen = index - } - } - is KtArrayAccessExpression, - is KtPostfixExpression -> { - // we group these with the last item with a name, and we always close them - groupingInfos[lastIndexToOpen].groupOpenCount++ - } - } - } - if (useBlockLikeLambdaStyle) { - // a trailing lambda adds a group that we stop before emitting the lambda - groupingInfos[0].groupOpenCount++ - } - return groupingInfos - } - - /** Decide whether a [KtQualifiedExpression] part should be grouped with the previous part */ - private fun shouldGroupPartWithPrevious( - parts: List, - part: KtExpression, - index: Int, - previous: KtExpression, - current: KtExpression, - ): Boolean { - // this is the second, and the first is short, avoid `.` "hanging in air" - if (index == 1 && previous.text.length < options.continuationIndent) { - return true - } - // the previous part is `this` or `super` - if (previous is KtSuperExpression || previous is KtThisExpression) { - return true - } - // this and the previous part are a package name, type name, or property - if ( - previous is KtSimpleNameExpression && - current is KtSimpleNameExpression && - part is KtDotQualifiedExpression - ) { - return true - } - // this is `Foo` in `com.facebook.Foo`, so everything before it is a package name - if ( - current.text.first().isUpperCase() && - current is KtSimpleNameExpression && - part is KtDotQualifiedExpression - ) { - return true - } - // this is the `foo()` in `com.facebook.Foo.foo()` or in `Foo.foo()` - if ( - current is KtCallExpression && - (previous !is KtCallExpression) && - previous.text?.firstOrNull()?.isUpperCase() == true - ) { - return true - } - // this is an invocation and the last item, and the previous it not, i.e. `a.b.c()` - // keeping it grouped and splitting the arguments makes `a.b(...)` feel like `aab()` - return current is KtCallExpression && - previous !is KtCallExpression && - index == parts.indices.last - } - override fun formatArrayAccessExpression(expression: KtArrayAccessExpression) { builder.sync(expression) if (expression.arrayExpression is KtQualifiedExpression) { emitQualifiedExpression(expression) } else { format(expression.arrayExpression) - builder.block(expressionBreakIndent) { - formatCommaSeparatedList( - expression.indexExpressions, - forceMultiline = expression.trailingComma != null, - wrapInBlock = true, - prefix = "[", - postfix = "]", - breakBeforePostfix = false, - ) - } + formatArrayAccessBrackets(expression) + } + } + + private fun formatArrayAccessBrackets(expression: KtArrayAccessExpression) { + builder.block(expressionBreakIndent) { + formatCommaSeparatedList( + expression.indexExpressions, + forceMultiline = expression.trailingComma != null, + wrapInBlock = true, + prefix = "[", + postfix = "]", + breakBeforePostfix = false, + ) } } } diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/FileFormatter.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/FileFormatter.kt index a0f2af6c..70a73047 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/FileFormatter.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/FileFormatter.kt @@ -1,6 +1,5 @@ package org.jetbrains.ktfmt.format.visitor -import com.google.googlejavaformat.Indent.Const.ZERO import com.google.googlejavaformat.OpsBuilder import org.jetbrains.kotlin.com.intellij.psi.PsiComment import org.jetbrains.kotlin.com.intellij.psi.PsiElement @@ -72,7 +71,7 @@ interface FileFormatter : KotlinAstFormatter { } override fun formatStatement(statement: PsiElement) { - builder.block(ZERO) { format(statement) } + builder.block { format(statement) } builder.guessToken(";") } diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/GroupInfo.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/GroupInfo.kt new file mode 100644 index 00000000..0846aa75 --- /dev/null +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/GroupInfo.kt @@ -0,0 +1,127 @@ +package org.jetbrains.ktfmt.format.visitor + +import org.jetbrains.kotlin.psi.KtArrayAccessExpression +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtDotQualifiedExpression +import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.psi.KtPostfixExpression +import org.jetbrains.kotlin.psi.KtQualifiedExpression +import org.jetbrains.kotlin.psi.KtSimpleNameExpression +import org.jetbrains.kotlin.psi.KtSuperExpression +import org.jetbrains.kotlin.psi.KtThisExpression + +internal data class GroupInfo( + val expression: KtExpression, +) { + var groupOpenCount: Int = 0 + var shouldCloseGroup: Boolean = false + var isLast: Boolean = false + + operator fun component2() = groupOpenCount + + operator fun component3() = shouldCloseGroup + + operator fun component4() = isLast +} + +/** + * Generates the [GroupInfo] array to go with an array of [KtQualifiedExpression] parts + * + * For example, the expression `a.b[2].c.d()` is made of four expressions: + * 1. [KtQualifiedExpression] `a.b[2].c . d()` (this will be `parts[4]`) + * 1. [KtQualifiedExpression] `a.b[2] . c` (this will be `parts[3]`) + * 2. [KtArrayAccessExpression] `a.b [2]` (this will be `parts[2]`) + * 3. [KtQualifiedExpression] `a . b` (this will be `parts[1]`) + * 4. [KtSimpleNameExpression] `a` (this will be `parts[0]`) + * + * Once in parts, these are in the reverse order. To render the array correct we need to make sure + * `b` and [2] are in a group so we avoid splitting them. To do so we need to open a group for `b` + * (that will be done in part 2), and always close a group for an array. + * + * Here is the same expression, with justified braces marking the groupings it will get: + * ``` + * a . b [2] . c . d () + * {a . b} --> Grouping `a.b` because it can be a package name or simple field access so we add 1 + * to the number of groups to open at groupingInfos[0], and mark to close a group at + * groupingInfos[1] + * {a . b [2]} --> Grouping `a.b` with `[2]`, since otherwise we may break inside the brackets + * instead of preferring breaks before dots. So we open a group at [0], but since + * we always close a group after brackets, we don't store that information. + * {c . d} --> another group to attach the first function name to the fields before it + * this time we don't start the group in the beginning, and use + * lastIndexToOpen to track the spot after the last time we stopped + * grouping. + * ``` + * + * The final expression with groupings: + * ``` + * {{a.b}[2]}.{c.d}() + * ``` + */ +internal fun computeGroupingInfo( + parts: List, + hasTrailingLambda: Boolean, + continuationIndent: Indentation.Const, +): List { + val groupingInfos = List(parts.size) { GroupInfo(parts[it]) } + groupingInfos.lastOrNull()?.let { it.isLast = true } + + var inPrefix = true + var lastIndexToOpen = 0 + for ((index, part) in parts.withIndex()) { + when (part) { + is KtQualifiedExpression -> { + if ( + lastIndexToOpen == 0 && + part.shouldGroupWithPrevious(index, parts.lastIndex, continuationIndent) + ) { + // this and the previous items should be grouped for better style + // we add another group to open in index 0 + groupingInfos[0].groupOpenCount++ + // we don't always close a group when emitting this node, so we need this flag to + // mark if we need to close a group + groupingInfos[index].shouldCloseGroup = true + } else { + // use this index in to open future groups for arrays and postfixes + // we will also stop grouping field access to the beginning of the expression + lastIndexToOpen = index + } + } + is KtArrayAccessExpression, + is KtPostfixExpression -> { + // we group these with the last item with a name, and we always close them + groupingInfos[lastIndexToOpen].groupOpenCount++ + } + } + } + if (hasTrailingLambda) { + // a trailing lambda adds a group that we stop before emitting the lambda + groupingInfos[0].groupOpenCount++ + } + return groupingInfos +} + +/** Decide whether a [KtQualifiedExpression] part should be grouped with the previous part */ +internal fun KtQualifiedExpression.shouldGroupWithPrevious( + currentIndex: Int, + lastIndex: Int, + continuationIndent: Indentation.Const, +): Boolean { + val previous = + (receiverExpression as? KtQualifiedExpression)?.selectorExpression ?: receiverExpression + val current = checkNotNull(selectorExpression) + + return when { + // this is the second, and the first is short, avoid hanging `.` + currentIndex == 1 && previous.text.length < continuationIndent.value -> true + // the previous part is `this` or `super` + previous is KtSuperExpression || previous is KtThisExpression -> true + // this is `b` or `C` in `a.b.C`, so everything before it is a package name + current is KtSimpleNameExpression && this is KtDotQualifiedExpression -> + previous is KtSimpleNameExpression || current.startsWithUpperCase() + // this is an invocation that either comes directly after type name OR is last in chain + current is KtCallExpression && previous !is KtCallExpression -> + previous.startsWithUpperCase() || currentIndex == lastIndex + else -> false + } +} diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/Indentation.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/Indentation.kt new file mode 100644 index 00000000..bc5f7eab --- /dev/null +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/Indentation.kt @@ -0,0 +1,39 @@ +package org.jetbrains.ktfmt.format.visitor + +import com.google.googlejavaformat.Indent +import com.google.googlejavaformat.Output + +sealed class Indentation { + internal abstract val indent: Indent + + data class Const(val value: Int) : Indentation() { + override val indent: Indent = Indent.Const.make(value, 1) + + operator fun plus(other: Const): Const = Const(value + other.value) + + operator fun minus(other: Const): Const = Const(value - other.value) + + operator fun unaryPlus(): Const = Const(+value) + + override operator fun unaryMinus(): Const = Const(-value) + + operator fun times(other: Int): Const = Const(value * other) + } + + data class Conditional( + val condition: Output.BreakTag, + val trueIndent: Indentation, + val falseIndent: Indentation, + ) : Indentation() { + override val indent: Indent = Indent.If.make(condition, trueIndent.indent, falseIndent.indent) + + override operator fun unaryMinus(): Conditional = + Conditional(condition, -trueIndent, -falseIndent) + } + + abstract operator fun unaryMinus(): Indentation + + companion object { + val ZERO = Const(0) + } +} diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter.kt index ddba5cc4..a5eeef78 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter.kt @@ -1,7 +1,6 @@ package org.jetbrains.ktfmt.format.visitor 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.BreakTag @@ -129,9 +128,8 @@ interface KotlinAstFormatter { val options: FormattingOptions val builder: OpsBuilder - val blockIndent: Indent.Const - val expressionBreakIndent: Indent.Const - val expressionBreakNegativeIndent: Indent.Const + val blockIndent: Indentation.Const + val expressionBreakIndent: Indentation.Const /** A record of whether we have visited into an expression. */ val inExpression: ArrayDeque @@ -233,9 +231,7 @@ interface KotlinAstFormatter { TODO("Unreachable code path") } - fun formatQualifiedExpression(expression: KtQualifiedExpression, extraRules: Boolean = true) { - TODO("Unreachable code path") - } + fun formatQualifiedExpression(expression: KtQualifiedExpression) fun formatCallExpression(callExpression: KtCallExpression) { TODO("Unreachable code path") @@ -524,20 +520,13 @@ interface KotlinAstFormatter { /** See [isLambdaOrScopingFunction] for examples. */ fun formatLambdaOrScopingFunction(expr: PsiElement?, emitLeadingBreak: Boolean = true) - /** - * Examples `foo(a, b)`, `foo(a)`, `boo()`, `super(a)` - * - * @param lambdaIndent how to indent [lambdaArguments], if present - * @param negativeLambdaIndent the negative indentation of [lambdaIndent] - */ fun formatFunctionCall( callee: KtExpression?, typeArgumentList: KtTypeArgumentList?, argumentList: KtValueArgumentList?, lambdaArguments: List, - argumentsIndent: Indent = expressionBreakIndent, - lambdaIndent: Indent = ZERO, - negativeLambdaIndent: Indent = ZERO, + argumentsIndent: Indentation = expressionBreakIndent, + lambdaIndent: Indentation = Indentation.ZERO, ) /** diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/ListFormatter.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/ListFormatter.kt index 50decd7e..9077f07f 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/ListFormatter.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/ListFormatter.kt @@ -1,7 +1,6 @@ package org.jetbrains.ktfmt.format.visitor import com.google.googlejavaformat.Doc -import com.google.googlejavaformat.Indent.Const.ZERO import com.google.googlejavaformat.Output.BreakTag import java.util.Optional import org.jetbrains.kotlin.com.intellij.psi.PsiElement @@ -18,6 +17,7 @@ import org.jetbrains.kotlin.psi.KtTypeConstraintList import org.jetbrains.kotlin.psi.KtTypeParameterList import org.jetbrains.kotlin.psi.KtValueArgumentList import org.jetbrains.kotlin.psi.psiUtil.children +import org.jetbrains.ktfmt.format.visitor.Indentation.Companion.ZERO import org.jetbrains.ktfmt.util.listToVisit interface ListFormatter : KotlinAstFormatter { @@ -206,7 +206,7 @@ interface ListFormatter : KotlinAstFormatter { builder.breakOp(breakType, " ", ZERO) } - val indent = if (emitLeadingBreak) ZERO else expressionBreakNegativeIndent + val indent = if (emitLeadingBreak) ZERO else -expressionBreakIndent builder.block(indent, isEnabled = wrapInBlock) { if (emitLeadingBreak) { builder.breakOp(breakType, "", ZERO) @@ -225,12 +225,12 @@ interface ListFormatter : KotlinAstFormatter { if (breakAfterLastElement) { // a negative closing indent places the postfix to the left of the elements // see examples 2 and 4 in the docstring - builder.breakOp(breakType, "", expressionBreakNegativeIndent) + builder.breakOp(breakType, "", -expressionBreakIndent) } if (postfix != null) { if (breakAfterLastElement) { - builder.block(expressionBreakNegativeIndent) { + builder.block(-expressionBreakIndent) { builder.fenceComments() builder.token(postfix, expressionBreakIndent) } diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/OpsUtils.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/OpsUtils.kt index 39003dcc..249969a5 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/OpsUtils.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/OpsUtils.kt @@ -2,22 +2,21 @@ package org.jetbrains.ktfmt.format.visitor import com.google.googlejavaformat.Doc import com.google.googlejavaformat.Doc.Level -import com.google.googlejavaformat.Doc.Token -import com.google.googlejavaformat.Indent -import com.google.googlejavaformat.Indent.Const.ZERO import com.google.googlejavaformat.OpsBuilder +import com.google.googlejavaformat.Output import java.util.Optional import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.psi.psiUtil.startOffset import org.jetbrains.ktfmt.format.FenceCommentsOp +import org.jetbrains.ktfmt.format.visitor.Indentation.Companion.ZERO /** Helper method to sync the current offset to match any element in the AST */ -fun OpsBuilder.sync(psiElement: PsiElement) { +internal fun OpsBuilder.sync(psiElement: PsiElement) { sync(psiElement.startOffset) } /** Prevent subsequent comments from being moved ahead of this point, into parent [Level]s. */ -fun OpsBuilder.fenceComments() { +internal fun OpsBuilder.fenceComments() { addAll(FenceCommentsOp.AS_LIST) } @@ -27,11 +26,11 @@ fun OpsBuilder.fenceComments() { * @param token the [String] to wrap in a [Doc.Token] * @param plusIndentCommentsBefore extra block for comments before this token */ -fun OpsBuilder.token(token: String, plusIndentCommentsBefore: Indent = ZERO) { +internal fun OpsBuilder.token(token: String, plusIndentCommentsBefore: Indentation = ZERO) { token( token, Doc.Token.RealOrImaginary.REAL, - plusIndentCommentsBefore, + plusIndentCommentsBefore.indent, /* breakAndIndentTrailingComment */ Optional.empty(), ) } @@ -45,13 +44,13 @@ fun OpsBuilder.token(token: String, plusIndentCommentsBefore: Indent = ZERO) { * @param plusIndent the block level to pass to the block * @param block a code block to be run in this block level */ -fun OpsBuilder.block( - plusIndent: Indent = ZERO, +internal fun OpsBuilder.block( + plusIndent: Indentation = ZERO, isEnabled: Boolean = true, block: () -> Unit, ) { if (isEnabled) { - open(plusIndent) + open(plusIndent.indent) } block() if (isEnabled) { @@ -59,5 +58,13 @@ fun OpsBuilder.block( } } -val Int.asIndent: Indent.Const - get() = Indent.Const.make(this, 1) +internal fun OpsBuilder.breakOp( + fillMode: Doc.FillMode = Doc.FillMode.UNIFIED, + flat: String = " ", + plusIndent: Indentation = ZERO, + optionalTag: Optional = Optional.empty(), +) = breakOp(fillMode, flat, plusIndent.indent, optionalTag) + +internal fun OpsBuilder.open(plusIndent: Indentation) = open(plusIndent.indent) + +internal fun OpsBuilder.forcedBreak(plusIndent: Indentation) = forcedBreak(plusIndent.indent) diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/PsiUtils.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/PsiUtils.kt index e2cfa241..66261083 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/PsiUtils.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/PsiUtils.kt @@ -21,6 +21,7 @@ import kotlin.contracts.contract 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.lexer.KtSingleValueToken import org.jetbrains.kotlin.psi.KtArrayAccessExpression import org.jetbrains.kotlin.psi.KtBinaryExpression import org.jetbrains.kotlin.psi.KtBinaryExpressionWithTypeRHS @@ -38,7 +39,6 @@ import org.jetbrains.kotlin.psi.psiUtil.children import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespace import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespace import org.jetbrains.ktfmt.format.FormattingOptions -import org.jetbrains.ktfmt.format.KotlinInputAstVisitor /** Returns true if the expression represents an invocation that is also a lambda */ val KtExpression.isLambda: Boolean @@ -164,7 +164,7 @@ inline fun PsiElement?.getPrevSiblingIgnoringWhiteSpace } /** - * Returns an unwrapped lambda expression or scoping function of an expression + * An unwrapped lambda expression or scoping function of an expression * * Examples: * 1. '... = { ... }' is a lambda expression @@ -176,14 +176,30 @@ inline fun PsiElement?.getPrevSiblingIgnoringWhiteSpace * 1. '... = foo() { ... }' due to the empty parenthesis * 2. '... = Runnable @Annotation { ... }' due to the annotation */ -val KtExpression?.scopingLambda: KtLambdaExpression? +internal data class ScopingLambda( + val receiverExpression: KtExpression?, + val operation: KtSingleValueToken?, + val calleeExpression: KtExpression?, + val labeledExpression: KtLabeledExpression?, + val lambdaExpression: KtLambdaExpression, +) + +internal val PsiElement?.scopingLambda: ScopingLambda? get() { if (this == null) return null + var receiverExpression: KtExpression? = null + var operation: KtSingleValueToken? = null + var calleeExpression: KtExpression? = null + var labeledExpression: KtLabeledExpression? = null + val lambdaExpression: KtLambdaExpression var carry = this if (carry is KtQualifiedExpression && carry.receiverExpression is KtSimpleNameExpression) { + receiverExpression = carry.receiverExpression + operation = carry.operationSign carry = carry.selectorExpression } if (carry is KtCallExpression) { + calleeExpression = carry.calleeExpression if ( carry.valueArgumentList?.leftParenthesis == null && carry.lambdaArguments.isNotEmpty() && @@ -195,9 +211,17 @@ val KtExpression?.scopingLambda: KtLambdaExpression? } } if (carry is KtLabeledExpression) { + labeledExpression = carry carry = carry.baseExpression } - return carry as? KtLambdaExpression + lambdaExpression = carry as? KtLambdaExpression ?: return null + return ScopingLambda( + receiverExpression, + operation, + calleeExpression, + labeledExpression, + lambdaExpression, + ) } /** @@ -251,7 +275,7 @@ fun chainedSelectorsHaveValueArguments(expression: KtExpression): Boolean { * closing brace must break onto a new line. */ val KtExpression.isMultilineScopingFunction: Boolean - get() = scopingLambda?.hasSourceNewlineInLambdaBody ?: false + get() = scopingLambda?.lambdaExpression?.hasSourceNewlineInLambdaBody ?: false /** * Returns true if the source code contains a newline anywhere inside the body of @@ -268,5 +292,9 @@ val KtLambdaExpression.hasSourceNewlineInLambdaBody: Boolean return false } +internal fun KtExpression?.startsWithUpperCase(): Boolean { + return this?.text?.firstOrNull()?.isUpperCase() ?: false +} + internal val KtExpression?.isBinaryExpression: Boolean get() = this is KtBinaryExpression || this is KtBinaryExpressionWithTypeRHS From 8df5b17f95b74d4f57f859c20da1cddded368305 Mon Sep 17 00:00:00 2001 From: Azat Abdullin Date: Tue, 18 Aug 2026 17:39:42 +0200 Subject: [PATCH 03/10] Refactor `emitQualifiedExpression` --- .../ktfmt/format/visitor/CallFormatter.kt | 123 ++++++++---------- .../ktfmt/format/visitor/GroupInfo.kt | 68 +++++----- .../format/visitor/KotlinAstFormatter.kt | 1 - .../ktfmt/format/visitor/PsiUtils.kt | 2 +- 4 files changed, 85 insertions(+), 109 deletions(-) diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt index 5d2b2631..7aa32ddd 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt @@ -164,12 +164,12 @@ interface CallFormatter : KotlinAstFormatter { val shouldForceMultiline = options.preserveLambdaBreaks && lambdaExpression.hasSourceNewlineInLambdaBody - if ( - !shouldForceMultiline && - expressionStatements.size == 1 && + val singleLineStatement = + expressionStatements.size == 1 && expressionStatements.first() !is KtReturnExpression && !bodyExpression.startsWithComment() - ) { + + if (!shouldForceMultiline && singleLineStatement) { formatStatement(expressionStatements[0]) } else { formatStatements(expressionStatements) @@ -183,15 +183,11 @@ interface CallFormatter : KotlinAstFormatter { builder.block(bracePlusBlockIndent) { builder.fenceComments() builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO) - if (blockComments.size == 1) { - builder.token(blockComments[0].text) - } else { - for ((i, comment) in blockComments.withIndex()) { - if (i > 0) { - builder.forcedBreak() - } - builder.token(comment.text) + for ((i, comment) in blockComments.withIndex()) { + if (i > 0) { + builder.forcedBreak() } + builder.token(comment.text) } builder.breakOp(Doc.FillMode.UNIFIED, " ", bracePlusZeroIndent) } @@ -342,73 +338,58 @@ interface CallFormatter : KotlinAstFormatter { } private fun emitQualifiedExpression(expression: KtExpression) { - val parts = expression.chainParts - // whether we want to make a lambda look like a block, this make Kotlin DSLs look as expected - val hasTrailingLambda = parts.last().isLambda && parts.count { it.isLambda } == 1 - val groupingInfos = computeGroupingInfo(parts, hasTrailingLambda, expressionBreakIndent) + val groupingInfos = expression.computeGroups(expressionBreakIndent) builder.block(expressionBreakIndent) { - val nameTag = BreakTag() // allows adjusting arguments indentation if a break will be made - for ((ktExpression, groupOpenCount, shouldCloseGroup, isLast) in groupingInfos) { + // allows adjusting arguments indentation if a break will be made + val nameTag = BreakTag() + for ((ktExpression, openingGroups, closingGroups, isTrailingLambda, isLast) in + groupingInfos) { if (ktExpression is KtQualifiedExpression) { builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO, Optional.of(nameTag)) } - repeat(groupOpenCount) { builder.open(ZERO) } + + var postfix: Triple? = null + repeat(openingGroups) { builder.open(ZERO) } when (ktExpression) { - is KtQualifiedExpression -> { + is KtQualifiedExpression if ktExpression.selectorExpression is KtCallExpression -> { builder.token(ktExpression.operationSign.value) - val selectorExpression = ktExpression.selectorExpression - if (selectorExpression !is KtCallExpression) { - // selector is a simple field access - format(selectorExpression) - if (shouldCloseGroup) { - builder.close() - } - } else { - // selector is a function call, we may close a group after its name - // emit `doIt` from `doIt(1, 2) { it }` - format(selectorExpression.calleeExpression) - // close groups according to instructions - if (shouldCloseGroup) { - builder.close() - } - // close group due to last lambda to allow block-like style in `as.forEach { ... }` - val isTrailingLambda = hasTrailingLambda && isLast - if (isTrailingLambda) { - builder.close() - } - // A block-like (exploded) selector call is laid out like the last part: its - // arguments are indented once relative to the call itself, and its closing paren - // returns to the call's indent, even when chained selectors follow it. This only - // applies when trailing commas are preserved (the block-like style); when ktfmt - // manages trailing commas, exploded chained calls keep the regular extra indent. - val isLastPartOrBlockLikeCall = - isLast || !options.manageTrailingCommas && selectorExpression.isBlockLikeCall - val argsIndentElse = if (isLastPartOrBlockLikeCall) ZERO else expressionBreakIndent - val lambdaIndentElse = if (isTrailingLambda) -expressionBreakIndent else ZERO - - // emit `(1, 2) { it }` from `doIt(1, 2) { it }` - formatFunctionCall( - null, - selectorExpression.typeArgumentList, - selectorExpression.valueArgumentList, - selectorExpression.lambdaArguments, - argumentsIndent = - Indentation.Conditional(nameTag, expressionBreakIndent, argsIndentElse), - lambdaIndent = Indentation.Conditional(nameTag, ZERO, lambdaIndentElse), - ) - } - } - is KtArrayAccessExpression -> { - formatArrayAccessBrackets(ktExpression) - builder.close() - } - is KtPostfixExpression -> { - builder.token(ktExpression.operationReference.text) - builder.close() + val selectorExpression = ktExpression.selectorExpression as KtCallExpression + + // emit `doIt` from `doIt(1, 2) { it }` + format(selectorExpression.calleeExpression) + + val isLastPartOrBlockLikeCall = + isLast || !options.manageTrailingCommas && selectorExpression.isBlockLikeCall + val argsIndentElse = if (isLastPartOrBlockLikeCall) ZERO else expressionBreakIndent + val lambdaIndentElse = if (isTrailingLambda) -expressionBreakIndent else ZERO + + // remember to emit `(1, 2) { it }` from `doIt(1, 2) { it }` + postfix = + Triple( + selectorExpression, + Indentation.Conditional(nameTag, expressionBreakIndent, argsIndentElse), + Indentation.Conditional(nameTag, ZERO, lambdaIndentElse), + ) } - else -> { - format(ktExpression) + is KtQualifiedExpression -> { + builder.token(ktExpression.operationSign.value) + format(ktExpression.selectorExpression) } + is KtArrayAccessExpression -> formatArrayAccessBrackets(ktExpression) + is KtPostfixExpression -> builder.token(ktExpression.operationReference.text) + else -> format(ktExpression) + } + repeat(closingGroups) { builder.close() } + + postfix?.let { (selectorExpression, argumentsIndent, lambdaIndent) -> + formatFunctionCall( + null, + selectorExpression.typeArgumentList, + selectorExpression.valueArgumentList, + selectorExpression.lambdaArguments, + argumentsIndent = argumentsIndent, + lambdaIndent = lambdaIndent, + ) } } } diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/GroupInfo.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/GroupInfo.kt index 0846aa75..d9d316b7 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/GroupInfo.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/GroupInfo.kt @@ -12,16 +12,17 @@ import org.jetbrains.kotlin.psi.KtThisExpression internal data class GroupInfo( val expression: KtExpression, -) { - var groupOpenCount: Int = 0 - var shouldCloseGroup: Boolean = false - var isLast: Boolean = false + var openingGroups: Int = 0, + var closingGroups: Int = 0, + var isTrailingLambda: Boolean = false, + var isLast: Boolean = false, +) - operator fun component2() = groupOpenCount - - operator fun component3() = shouldCloseGroup - - operator fun component4() = isLast +internal fun KtExpression.computeGroups(continuationIndent: Indentation.Const): List { + val parts = this.chainParts + // whether we want to make a lambda look like a block, this make Kotlin DSLs look as expected + val hasTrailingLambda = parts.last().isLambda && parts.count { it.isLambda } == 1 + return computeGroupingInfo(parts, hasTrailingLambda, continuationIndent) } /** @@ -34,9 +35,9 @@ internal data class GroupInfo( * 3. [KtQualifiedExpression] `a . b` (this will be `parts[1]`) * 4. [KtSimpleNameExpression] `a` (this will be `parts[0]`) * - * Once in parts, these are in the reverse order. To render the array correct we need to make sure - * `b` and [2] are in a group so we avoid splitting them. To do so we need to open a group for `b` - * (that will be done in part 2), and always close a group for an array. + * Once in parts, these are in the reverse order. To render the array access correctly we need to + * make sure `b` and `[2]` are in a group so we avoid splitting them. To do so we need to open a + * group for `b`, and always close a group for an array. * * Here is the same expression, with justified braces marking the groupings it will get: * ``` @@ -47,15 +48,11 @@ internal data class GroupInfo( * {a . b [2]} --> Grouping `a.b` with `[2]`, since otherwise we may break inside the brackets * instead of preferring breaks before dots. So we open a group at [0], but since * we always close a group after brackets, we don't store that information. - * {c . d} --> another group to attach the first function name to the fields before it - * this time we don't start the group in the beginning, and use - * lastIndexToOpen to track the spot after the last time we stopped - * grouping. * ``` * * The final expression with groupings: * ``` - * {{a.b}[2]}.{c.d}() + * {{a.b}[2]}.c.d() * ``` */ internal fun computeGroupingInfo( @@ -64,39 +61,38 @@ internal fun computeGroupingInfo( continuationIndent: Indentation.Const, ): List { val groupingInfos = List(parts.size) { GroupInfo(parts[it]) } - groupingInfos.lastOrNull()?.let { it.isLast = true } + groupingInfos.lastOrNull()?.let { + it.isTrailingLambda = hasTrailingLambda && it.expression.isLambda + it.isLast = true + } + + fun group(from: Int, to: Int) { + groupingInfos[from].openingGroups++ + groupingInfos[to].closingGroups++ + } var inPrefix = true - var lastIndexToOpen = 0 + var lastAnchor = 0 for ((index, part) in parts.withIndex()) { when (part) { is KtQualifiedExpression -> { - if ( - lastIndexToOpen == 0 && - part.shouldGroupWithPrevious(index, parts.lastIndex, continuationIndent) - ) { - // this and the previous items should be grouped for better style - // we add another group to open in index 0 - groupingInfos[0].groupOpenCount++ - // we don't always close a group when emitting this node, so we need this flag to - // mark if we need to close a group - groupingInfos[index].shouldCloseGroup = true + if (inPrefix && part.shouldGroupWithPrevious(index, parts.lastIndex, continuationIndent)) { + // all parts of the prefix are grouped together + group(0, index) } else { - // use this index in to open future groups for arrays and postfixes - // we will also stop grouping field access to the beginning of the expression - lastIndexToOpen = index + inPrefix = false + // future arrays and postfixes will be anchored to this part + lastAnchor = index } } is KtArrayAccessExpression, is KtPostfixExpression -> { - // we group these with the last item with a name, and we always close them - groupingInfos[lastIndexToOpen].groupOpenCount++ + group(lastAnchor, index) } } } if (hasTrailingLambda) { - // a trailing lambda adds a group that we stop before emitting the lambda - groupingInfos[0].groupOpenCount++ + group(0, groupingInfos.lastIndex) } return groupingInfos } diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter.kt index a5eeef78..60dbb939 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter.kt @@ -1,7 +1,6 @@ package org.jetbrains.ktfmt.format.visitor import com.google.googlejavaformat.FormattingError -import com.google.googlejavaformat.Indent.Const.ZERO import com.google.googlejavaformat.OpsBuilder import com.google.googlejavaformat.Output.BreakTag import java.util.ArrayDeque diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/PsiUtils.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/PsiUtils.kt index 66261083..03233696 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/PsiUtils.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/PsiUtils.kt @@ -56,7 +56,7 @@ private fun onlyEmptyParenthesis(left: PsiElement?, right: PsiElement?): Boolean left != null && right != null && left.getNextSiblingIgnoringWhitespace() == right /** - * [KotlinInputAstVisitor.emitQualifiedExpression] formats call expressions that are either part of + * [CallFormatter.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. */ From a595682bdafa4dd3e46c460cf652193f65aff678 Mon Sep 17 00:00:00 2001 From: Azat Abdullin Date: Tue, 18 Aug 2026 17:53:02 +0200 Subject: [PATCH 04/10] Refactor `formatLambdaExpression` --- .../ktfmt/format/visitor/CallFormatter.kt | 156 ++++++++++-------- .../ktfmt/format/visitor/Indentation.kt | 10 ++ .../ktfmt/format/visitor/PsiUtils.kt | 4 +- 3 files changed, 101 insertions(+), 69 deletions(-) diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt index 7aa32ddd..d3cb16c5 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt @@ -11,6 +11,7 @@ import org.jetbrains.kotlin.psi.KtCallExpression import org.jetbrains.kotlin.psi.KtExpression import org.jetbrains.kotlin.psi.KtLambdaArgument import org.jetbrains.kotlin.psi.KtLambdaExpression +import org.jetbrains.kotlin.psi.KtParameterList import org.jetbrains.kotlin.psi.KtPostfixExpression import org.jetbrains.kotlin.psi.KtQualifiedExpression import org.jetbrains.kotlin.psi.KtReturnExpression @@ -23,12 +24,13 @@ import org.jetbrains.kotlin.psi.psiUtil.children import org.jetbrains.kotlin.psi.psiUtil.startsWithComment import org.jetbrains.ktfmt.format.ParseError import org.jetbrains.ktfmt.format.visitor.Indentation.Companion.ZERO +import org.jetbrains.ktfmt.format.visitor.Indentation.Companion.makeCond interface CallFormatter : KotlinAstFormatter { override fun formatArgument( - argument: KtValueArgument, - wrapInBlock: Boolean, - brokeBeforeBrace: BreakTag?, + argument: KtValueArgument, + wrapInBlock: Boolean, + brokeBeforeBrace: BreakTag?, ) { builder.sync(argument) val hasArgName = argument.getArgumentName() != null @@ -51,8 +53,8 @@ interface CallFormatter : KotlinAstFormatter { } if (isLambda) { formatLambdaExpression( - argument.getArgumentExpression() as KtLambdaExpression, - brokeBeforeBrace = brokeBeforeBrace, + argument.getArgumentExpression() as KtLambdaExpression, + brokeBeforeBrace = brokeBeforeBrace, ) } else { format(argument.getArgumentExpression()) @@ -61,12 +63,12 @@ interface CallFormatter : KotlinAstFormatter { } override fun formatFunctionCall( - callee: KtExpression?, - typeArgumentList: KtTypeArgumentList?, - argumentList: KtValueArgumentList?, - lambdaArguments: List, - argumentsIndent: Indentation, - lambdaIndent: Indentation, + callee: KtExpression?, + typeArgumentList: KtTypeArgumentList?, + argumentList: KtValueArgumentList?, + lambdaArguments: List, + argumentsIndent: Indentation, + lambdaIndent: Indentation, ) { // Apply the lambda indent to the callee, type args, value args, and the lambda. // This is undone for the first three by the negative lambda indent. @@ -91,34 +93,29 @@ interface CallFormatter : KotlinAstFormatter { 1 -> { builder.space() formatArgument( - lambdaArguments.single(), - wrapInBlock = false, - brokeBeforeBrace = brokeBeforeBrace, + lambdaArguments.single(), + wrapInBlock = false, + brokeBeforeBrace = brokeBeforeBrace, ) } + else -> throw ParseError("Maximum one trailing lambda is allowed", lambdaArguments[1]) } } } override fun formatLambdaExpression( - lambdaExpression: KtLambdaExpression, - brokeBeforeBrace: BreakTag?, + lambdaExpression: KtLambdaExpression, + brokeBeforeBrace: BreakTag?, ) { builder.sync(lambdaExpression) - val valueParams = lambdaExpression.valueParameters - val hasParams = valueParams.isNotEmpty() val bodyExpression = lambdaExpression.bodyExpression ?: fail() - val expressionStatements = bodyExpression.children - val hasStatements = expressionStatements.isNotEmpty() + val hasStatements = bodyExpression.children.isNotEmpty() val hasComments = bodyExpression.children().any { it is PsiComment } - val hasArrow = lambdaExpression.functionLiteral.arrow != null - fun ifBrokeBeforeBrace(onTrue: Indentation, onFalse: Indentation): Indentation { - if (brokeBeforeBrace == null) return onFalse - return Indentation.Conditional(brokeBeforeBrace, onTrue, onFalse) - } + val hasDeclaration = lambdaExpression.valueParameters.isNotEmpty() || lambdaExpression.functionLiteral.arrow != null + val hasBody = hasDeclaration || hasStatements || hasComments /** * Enable correct formatting of the `fun foo() = scope {` syntax. @@ -131,56 +128,89 @@ interface CallFormatter : KotlinAstFormatter { * These conditional indents should not be used inside interior blocks, since that would apply * the condition twice. */ - val bracePlusBlockIndent = ifBrokeBeforeBrace(blockIndent + expressionBreakIndent, blockIndent) - val bracePlusExpressionIndent = - ifBrokeBeforeBrace(expressionBreakIndent * 2, expressionBreakIndent) - val bracePlusZeroIndent = ifBrokeBeforeBrace(expressionBreakIndent, ZERO) + val bodyIndent = + makeCond(brokeBeforeBrace, blockIndent + expressionBreakIndent, blockIndent) + val declarationIndent = + makeCond(brokeBeforeBrace, expressionBreakIndent * 2, expressionBreakIndent) + val closingBraceIndent = makeCond(brokeBeforeBrace, expressionBreakIndent, ZERO) builder.token("{") - if (hasParams || hasArrow) { - builder.space() - builder.block(bracePlusExpressionIndent) { formatCommaSeparatedList(valueParams) } - builder.block(bracePlusBlockIndent) { - if (lambdaExpression.functionLiteral.valueParameterList?.trailingComma != null) { - builder.token(",") - builder.forcedBreak() - } else if (hasParams) { - builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO) - } - builder.token("->") - } + if (hasDeclaration) { + formatLambdaArguments( + lambdaExpression.functionLiteral.valueParameterList!!, + declarationIndent, + bodyIndent, + ) + } + + if (hasBody) { + builder.breakOp(Doc.FillMode.UNIFIED, " ", closingBraceIndent) } - if (hasParams || hasArrow || hasStatements || hasComments) { - builder.breakOp(Doc.FillMode.UNIFIED, " ", bracePlusZeroIndent) + formatLambdaBody(lambdaExpression, bodyIndent, closingBraceIndent) + + if (hasBody) { + // If we had to break in the body, ensure there is a break before the closing brace + builder.breakOp(Doc.FillMode.UNIFIED, "", closingBraceIndent) + } + builder.block(closingBraceIndent) { + builder.fenceComments() + builder.token("}", blockIndent) } + } - if (hasStatements) { - builder.breakOp(Doc.FillMode.UNIFIED, "", bracePlusBlockIndent) - builder.block(bracePlusBlockIndent) { + private fun formatLambdaArguments( + valueParameterList: KtParameterList, + valueParametersIndent: Indentation, + arrowIndent: Indentation, + ) { + builder.space() + builder.block(valueParametersIndent) { formatCommaSeparatedList(valueParameterList.parameters) } + builder.block(arrowIndent) { + if (valueParameterList.trailingComma != null) { + builder.token(",") + builder.forcedBreak() + } else if (valueParameterList.parameters.isNotEmpty()) { + builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO) + } + builder.token("->") + } + } + + private fun formatLambdaBody( + lambdaExpression: KtLambdaExpression, + bodyIndent: Indentation, + braceIndent: Indentation, + ) { + val bodyExpression = lambdaExpression.bodyExpression ?: fail() + val expressionStatements = bodyExpression.children + val blockComments = + bodyExpression.children().filter { it is PsiComment && it.text.startsWith("/*") }.toList() + + val hasBody = expressionStatements.isNotEmpty() || blockComments.isNotEmpty() + + if (!hasBody) return + + builder.breakOp(Doc.FillMode.UNIFIED, "", bodyIndent) + builder.block(bodyIndent) { + if (expressionStatements.isNotEmpty()) { builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO) val shouldForceMultiline = - options.preserveLambdaBreaks && lambdaExpression.hasSourceNewlineInLambdaBody + options.preserveLambdaBreaks && lambdaExpression.hasSourceNewlineInLambdaBody val singleLineStatement = - expressionStatements.size == 1 && - expressionStatements.first() !is KtReturnExpression && - !bodyExpression.startsWithComment() + expressionStatements.size == 1 && + expressionStatements.first() !is KtReturnExpression && + !bodyExpression.startsWithComment() if (!shouldForceMultiline && singleLineStatement) { formatStatement(expressionStatements[0]) } else { formatStatements(expressionStatements) } - builder.breakOp(Doc.FillMode.UNIFIED, " ", bracePlusZeroIndent) - } - } else if (hasComments) { - val blockComments = - bodyExpression.children().filter { it is PsiComment && it.text.startsWith("/*") }.toList() - builder.breakOp(Doc.FillMode.UNIFIED, "", bracePlusBlockIndent) - builder.block(bracePlusBlockIndent) { + } else { builder.fenceComments() builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO) for ((i, comment) in blockComments.withIndex()) { @@ -189,20 +219,12 @@ interface CallFormatter : KotlinAstFormatter { } builder.token(comment.text) } - builder.breakOp(Doc.FillMode.UNIFIED, " ", bracePlusZeroIndent) } - } - - if (hasParams || hasArrow || hasStatements || hasComments) { - // If we had to break in the body, ensure there is a break before the closing brace - builder.breakOp(Doc.FillMode.UNIFIED, "", bracePlusZeroIndent) - } - builder.block(bracePlusZeroIndent) { - builder.fenceComments() - builder.token("}", blockIndent) + builder.breakOp(Doc.FillMode.UNIFIED, " ", braceIndent) } } + override fun formatChainedBlockLikeCall( expression: KtQualifiedExpression, emitLeadingBreak: Boolean, diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/Indentation.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/Indentation.kt index bc5f7eab..e6cbbc25 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/Indentation.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/Indentation.kt @@ -35,5 +35,15 @@ sealed class Indentation { companion object { val ZERO = Const(0) + + fun makeCond( + condition: Output.BreakTag?, + trueIndent: Indentation, + falseIndent: Indentation, + ): Indentation = + when { + condition == null -> falseIndent + else -> Conditional(condition, trueIndent, falseIndent) + } } } diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/PsiUtils.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/PsiUtils.kt index 03233696..5e4c2ddb 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/PsiUtils.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/PsiUtils.kt @@ -56,8 +56,8 @@ private fun onlyEmptyParenthesis(left: PsiElement?, right: PsiElement?): Boolean left != null && right != null && left.getNextSiblingIgnoringWhitespace() == right /** - * [CallFormatter.emitQualifiedExpression] formats call expressions that are either part of - * a qualified expression, or standing alone. This method makes it easier to handle both cases + * [CallFormatter.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. */ val KtExpression.callExpression: KtCallExpression? From 6f695720fdc69e7a19ac7caf3c5a6fc8fa4fd878 Mon Sep 17 00:00:00 2001 From: Azat Abdullin Date: Wed, 19 Aug 2026 09:35:51 +0200 Subject: [PATCH 05/10] Fix API --- .../org/jetbrains/ktfmt/format/visitor/CallFormatter.kt | 4 ++-- .../org/jetbrains/ktfmt/format/visitor/Indentation.kt | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt index d3cb16c5..65ad274f 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt @@ -389,8 +389,8 @@ interface CallFormatter : KotlinAstFormatter { postfix = Triple( selectorExpression, - Indentation.Conditional(nameTag, expressionBreakIndent, argsIndentElse), - Indentation.Conditional(nameTag, ZERO, lambdaIndentElse), + Indentation.Cond(nameTag, expressionBreakIndent, argsIndentElse), + Indentation.Cond(nameTag, ZERO, lambdaIndentElse), ) } is KtQualifiedExpression -> { diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/Indentation.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/Indentation.kt index e6cbbc25..8f0480ec 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/Indentation.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/Indentation.kt @@ -20,15 +20,15 @@ sealed class Indentation { operator fun times(other: Int): Const = Const(value * other) } - data class Conditional( + data class Cond( val condition: Output.BreakTag, val trueIndent: Indentation, val falseIndent: Indentation, ) : Indentation() { override val indent: Indent = Indent.If.make(condition, trueIndent.indent, falseIndent.indent) - override operator fun unaryMinus(): Conditional = - Conditional(condition, -trueIndent, -falseIndent) + override operator fun unaryMinus(): Cond = + Cond(condition, -trueIndent, -falseIndent) } abstract operator fun unaryMinus(): Indentation @@ -43,7 +43,7 @@ sealed class Indentation { ): Indentation = when { condition == null -> falseIndent - else -> Conditional(condition, trueIndent, falseIndent) + else -> Cond(condition, trueIndent, falseIndent) } } } From 24908ce342edb97d920037f429348cb9acdc4396 Mon Sep 17 00:00:00 2001 From: Azat Abdullin Date: Wed, 19 Aug 2026 09:43:30 +0200 Subject: [PATCH 06/10] Reformat --- .../ktfmt/format/visitor/CallFormatter.kt | 70 +++++++++---------- .../ktfmt/format/visitor/Indentation.kt | 3 +- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt index 65ad274f..731016da 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt @@ -28,9 +28,9 @@ import org.jetbrains.ktfmt.format.visitor.Indentation.Companion.makeCond interface CallFormatter : KotlinAstFormatter { override fun formatArgument( - argument: KtValueArgument, - wrapInBlock: Boolean, - brokeBeforeBrace: BreakTag?, + argument: KtValueArgument, + wrapInBlock: Boolean, + brokeBeforeBrace: BreakTag?, ) { builder.sync(argument) val hasArgName = argument.getArgumentName() != null @@ -53,8 +53,8 @@ interface CallFormatter : KotlinAstFormatter { } if (isLambda) { formatLambdaExpression( - argument.getArgumentExpression() as KtLambdaExpression, - brokeBeforeBrace = brokeBeforeBrace, + argument.getArgumentExpression() as KtLambdaExpression, + brokeBeforeBrace = brokeBeforeBrace, ) } else { format(argument.getArgumentExpression()) @@ -63,12 +63,12 @@ interface CallFormatter : KotlinAstFormatter { } override fun formatFunctionCall( - callee: KtExpression?, - typeArgumentList: KtTypeArgumentList?, - argumentList: KtValueArgumentList?, - lambdaArguments: List, - argumentsIndent: Indentation, - lambdaIndent: Indentation, + callee: KtExpression?, + typeArgumentList: KtTypeArgumentList?, + argumentList: KtValueArgumentList?, + lambdaArguments: List, + argumentsIndent: Indentation, + lambdaIndent: Indentation, ) { // Apply the lambda indent to the callee, type args, value args, and the lambda. // This is undone for the first three by the negative lambda indent. @@ -93,9 +93,9 @@ interface CallFormatter : KotlinAstFormatter { 1 -> { builder.space() formatArgument( - lambdaArguments.single(), - wrapInBlock = false, - brokeBeforeBrace = brokeBeforeBrace, + lambdaArguments.single(), + wrapInBlock = false, + brokeBeforeBrace = brokeBeforeBrace, ) } @@ -105,8 +105,8 @@ interface CallFormatter : KotlinAstFormatter { } override fun formatLambdaExpression( - lambdaExpression: KtLambdaExpression, - brokeBeforeBrace: BreakTag?, + lambdaExpression: KtLambdaExpression, + brokeBeforeBrace: BreakTag?, ) { builder.sync(lambdaExpression) @@ -114,7 +114,9 @@ interface CallFormatter : KotlinAstFormatter { val hasStatements = bodyExpression.children.isNotEmpty() val hasComments = bodyExpression.children().any { it is PsiComment } - val hasDeclaration = lambdaExpression.valueParameters.isNotEmpty() || lambdaExpression.functionLiteral.arrow != null + val hasDeclaration = + lambdaExpression.valueParameters.isNotEmpty() || + lambdaExpression.functionLiteral.arrow != null val hasBody = hasDeclaration || hasStatements || hasComments /** @@ -128,19 +130,18 @@ interface CallFormatter : KotlinAstFormatter { * These conditional indents should not be used inside interior blocks, since that would apply * the condition twice. */ - val bodyIndent = - makeCond(brokeBeforeBrace, blockIndent + expressionBreakIndent, blockIndent) + val bodyIndent = makeCond(brokeBeforeBrace, blockIndent + expressionBreakIndent, blockIndent) val declarationIndent = - makeCond(brokeBeforeBrace, expressionBreakIndent * 2, expressionBreakIndent) + makeCond(brokeBeforeBrace, expressionBreakIndent * 2, expressionBreakIndent) val closingBraceIndent = makeCond(brokeBeforeBrace, expressionBreakIndent, ZERO) builder.token("{") if (hasDeclaration) { formatLambdaArguments( - lambdaExpression.functionLiteral.valueParameterList!!, - declarationIndent, - bodyIndent, + lambdaExpression.functionLiteral.valueParameterList!!, + declarationIndent, + bodyIndent, ) } @@ -161,9 +162,9 @@ interface CallFormatter : KotlinAstFormatter { } private fun formatLambdaArguments( - valueParameterList: KtParameterList, - valueParametersIndent: Indentation, - arrowIndent: Indentation, + valueParameterList: KtParameterList, + valueParametersIndent: Indentation, + arrowIndent: Indentation, ) { builder.space() builder.block(valueParametersIndent) { formatCommaSeparatedList(valueParameterList.parameters) } @@ -179,14 +180,14 @@ interface CallFormatter : KotlinAstFormatter { } private fun formatLambdaBody( - lambdaExpression: KtLambdaExpression, - bodyIndent: Indentation, - braceIndent: Indentation, + lambdaExpression: KtLambdaExpression, + bodyIndent: Indentation, + braceIndent: Indentation, ) { val bodyExpression = lambdaExpression.bodyExpression ?: fail() val expressionStatements = bodyExpression.children val blockComments = - bodyExpression.children().filter { it is PsiComment && it.text.startsWith("/*") }.toList() + bodyExpression.children().filter { it is PsiComment && it.text.startsWith("/*") }.toList() val hasBody = expressionStatements.isNotEmpty() || blockComments.isNotEmpty() @@ -198,12 +199,12 @@ interface CallFormatter : KotlinAstFormatter { builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO) val shouldForceMultiline = - options.preserveLambdaBreaks && lambdaExpression.hasSourceNewlineInLambdaBody + options.preserveLambdaBreaks && lambdaExpression.hasSourceNewlineInLambdaBody val singleLineStatement = - expressionStatements.size == 1 && - expressionStatements.first() !is KtReturnExpression && - !bodyExpression.startsWithComment() + expressionStatements.size == 1 && + expressionStatements.first() !is KtReturnExpression && + !bodyExpression.startsWithComment() if (!shouldForceMultiline && singleLineStatement) { formatStatement(expressionStatements[0]) @@ -224,7 +225,6 @@ interface CallFormatter : KotlinAstFormatter { } } - override fun formatChainedBlockLikeCall( expression: KtQualifiedExpression, emitLeadingBreak: Boolean, diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/Indentation.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/Indentation.kt index 8f0480ec..c4b8d8de 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/Indentation.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/Indentation.kt @@ -27,8 +27,7 @@ sealed class Indentation { ) : Indentation() { override val indent: Indent = Indent.If.make(condition, trueIndent.indent, falseIndent.indent) - override operator fun unaryMinus(): Cond = - Cond(condition, -trueIndent, -falseIndent) + override operator fun unaryMinus(): Cond = Cond(condition, -trueIndent, -falseIndent) } abstract operator fun unaryMinus(): Indentation From e11c39157e657c67912111afdbbab64993297e53 Mon Sep 17 00:00:00 2001 From: Azat Abdullin Date: Wed, 19 Aug 2026 10:02:00 +0200 Subject: [PATCH 07/10] Fix native image compilation --- .../src/main/kotlin/org/jetbrains/ktfmt/NativeImagePlugin.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/buildSrc/src/main/kotlin/org/jetbrains/ktfmt/NativeImagePlugin.kt b/buildSrc/src/main/kotlin/org/jetbrains/ktfmt/NativeImagePlugin.kt index 141f1434..24dc5b0f 100644 --- a/buildSrc/src/main/kotlin/org/jetbrains/ktfmt/NativeImagePlugin.kt +++ b/buildSrc/src/main/kotlin/org/jetbrains/ktfmt/NativeImagePlugin.kt @@ -209,6 +209,7 @@ class NativeImagePlugin : Plugin { add("--future-defaults=all") add("--link-at-build-time=org.jetbrains.ktfmt") add("--initialize-at-build-time=org.jetbrains.ktfmt") + add($$"--initialize-at-build-time=com.google.googlejavaformat.Indent$Const") add("--add-opens=java.base/java.util=ALL-UNNAMED") add("--color=always") add("-H:+ReportExceptionStackTraces") From e56f4ae410520fc8b6652cacdf8f8779354e8d0b Mon Sep 17 00:00:00 2001 From: Azat Abdullin Date: Thu, 20 Aug 2026 11:50:10 +0200 Subject: [PATCH 08/10] Review fixes --- .../ktfmt/format/visitor/CallFormatter.kt | 64 +++++++++++++++---- .../ktfmt/format/visitor/GroupInfo.kt | 20 ++++-- .../ktfmt/format/visitor/Indentation.kt | 62 ++++++++++++------ .../ktfmt/format/visitor/PsiUtils.kt | 2 + 4 files changed, 112 insertions(+), 36 deletions(-) diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt index 731016da..3f43bc0b 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt @@ -24,8 +24,15 @@ import org.jetbrains.kotlin.psi.psiUtil.children import org.jetbrains.kotlin.psi.psiUtil.startsWithComment import org.jetbrains.ktfmt.format.ParseError import org.jetbrains.ktfmt.format.visitor.Indentation.Companion.ZERO -import org.jetbrains.ktfmt.format.visitor.Indentation.Companion.makeCond +/** + * Formatter that handles the formatting of anything related to call expressions: + * - Function calls + * - Function arguments + * - Lambda expressions + * - Qualified expressions + * - Array access expressions + */ interface CallFormatter : KotlinAstFormatter { override fun formatArgument( argument: KtValueArgument, @@ -62,6 +69,18 @@ interface CallFormatter : KotlinAstFormatter { } } + /** + * Format a single function call expression. + * + * @param callee the function call expression + * @param typeArgumentList the type arguments of the function call expression + * @param argumentList the value arguments of the function call expression + * @param lambdaArguments trailing lambda arguments of the call expression; in correspondence with + * Kotlin syntax a function call can't have more than one trailing lambda, but + * [KtCallExpression] represents them as a list (see [KtCallExpression.getLambdaArguments]) + * @param argumentsIndent how to indent [argumentList], if present + * @param lambdaIndent how to indent [lambdaArguments], if present + */ override fun formatFunctionCall( callee: KtExpression?, typeArgumentList: KtTypeArgumentList?, @@ -130,10 +149,11 @@ interface CallFormatter : KotlinAstFormatter { * These conditional indents should not be used inside interior blocks, since that would apply * the condition twice. */ - val bodyIndent = makeCond(brokeBeforeBrace, blockIndent + expressionBreakIndent, blockIndent) + val bodyIndent = + Indentation.If(brokeBeforeBrace, blockIndent + expressionBreakIndent, blockIndent) val declarationIndent = - makeCond(brokeBeforeBrace, expressionBreakIndent * 2, expressionBreakIndent) - val closingBraceIndent = makeCond(brokeBeforeBrace, expressionBreakIndent, ZERO) + Indentation.If(brokeBeforeBrace, expressionBreakIndent * 2, expressionBreakIndent) + val closingBraceIndent = Indentation.If(brokeBeforeBrace, expressionBreakIndent, ZERO) builder.token("{") @@ -359,6 +379,18 @@ interface CallFormatter : KotlinAstFormatter { } } + /** + * Handles a chain of qualified expressions, i.e. `a[5].b!!.c()[4].f()` + * + * This is by far the most complicated part of this formatter. We start by breaking the expression + * into a list of [GroupInfo]'s, each representing a step in the execution of the expression. + * [GroupInfo]'s are ordered in the opposite order of how the syntax tree is built. + * + * Each group is then emitted one by one to the [builder] while opening and closing groups. Each + * group is opened **before** a corresponding expression is emitted and closed **after**. However, + * if an expression represents a function call, e.g. `doIt(1, 2) { it }`, the group is closed + * after `doIt`, and the `(1, 2) { it }` part is emitted after. + */ private fun emitQualifiedExpression(expression: KtExpression) { val groupingInfos = expression.computeGroups(expressionBreakIndent) builder.block(expressionBreakIndent) { @@ -370,7 +402,7 @@ interface CallFormatter : KotlinAstFormatter { builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO, Optional.of(nameTag)) } - var postfix: Triple? = null + var deferredCallArguments: DeferredCallArguments? = null repeat(openingGroups) { builder.open(ZERO) } when (ktExpression) { is KtQualifiedExpression if ktExpression.selectorExpression is KtCallExpression -> { @@ -386,11 +418,11 @@ interface CallFormatter : KotlinAstFormatter { val lambdaIndentElse = if (isTrailingLambda) -expressionBreakIndent else ZERO // remember to emit `(1, 2) { it }` from `doIt(1, 2) { it }` - postfix = - Triple( + deferredCallArguments = + DeferredCallArguments( selectorExpression, - Indentation.Cond(nameTag, expressionBreakIndent, argsIndentElse), - Indentation.Cond(nameTag, ZERO, lambdaIndentElse), + Indentation.If(nameTag, expressionBreakIndent, argsIndentElse), + Indentation.If(nameTag, ZERO, lambdaIndentElse), ) } is KtQualifiedExpression -> { @@ -403,12 +435,12 @@ interface CallFormatter : KotlinAstFormatter { } repeat(closingGroups) { builder.close() } - postfix?.let { (selectorExpression, argumentsIndent, lambdaIndent) -> + deferredCallArguments?.let { (callee, argumentsIndent, lambdaIndent) -> formatFunctionCall( null, - selectorExpression.typeArgumentList, - selectorExpression.valueArgumentList, - selectorExpression.lambdaArguments, + callee.typeArgumentList, + callee.valueArgumentList, + callee.lambdaArguments, argumentsIndent = argumentsIndent, lambdaIndent = lambdaIndent, ) @@ -417,6 +449,12 @@ interface CallFormatter : KotlinAstFormatter { } } + private data class DeferredCallArguments( + val call: KtCallExpression, + val argumentsIndent: Indentation, + val lambdaIndent: Indentation, + ) + override fun formatArrayAccessExpression(expression: KtArrayAccessExpression) { builder.sync(expression) if (expression.arrayExpression is KtQualifiedExpression) { diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/GroupInfo.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/GroupInfo.kt index d9d316b7..cf8dd8a2 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/GroupInfo.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/GroupInfo.kt @@ -10,6 +10,16 @@ import org.jetbrains.kotlin.psi.KtSimpleNameExpression import org.jetbrains.kotlin.psi.KtSuperExpression import org.jetbrains.kotlin.psi.KtThisExpression +/** + * Represents a part of an execution chain (see [chainParts]) with the necessary grouping + * information: + * + * @property [openingGroups] is the number of groups that need to be opened before this expression + * @property [closingGroups] is the number of groups that need to be closed after this expression + * @property [isTrailingLambda] is true if this expression is the only lambda in the chain, and it + * is the last part of the chain + * @property [isLast] is true if this expression is the last part of the chain + */ internal data class GroupInfo( val expression: KtExpression, var openingGroups: Int = 0, @@ -28,12 +38,12 @@ internal fun KtExpression.computeGroups(continuationIndent: Indentation.Const): /** * Generates the [GroupInfo] array to go with an array of [KtQualifiedExpression] parts * - * For example, the expression `a.b[2].c.d()` is made of four expressions: + * For example, the expression `a.b[2].c.d()` is made of five expressions: * 1. [KtQualifiedExpression] `a.b[2].c . d()` (this will be `parts[4]`) - * 1. [KtQualifiedExpression] `a.b[2] . c` (this will be `parts[3]`) - * 2. [KtArrayAccessExpression] `a.b [2]` (this will be `parts[2]`) - * 3. [KtQualifiedExpression] `a . b` (this will be `parts[1]`) - * 4. [KtSimpleNameExpression] `a` (this will be `parts[0]`) + * 2. [KtQualifiedExpression] `a.b[2] . c` (this will be `parts[3]`) + * 3. [KtArrayAccessExpression] `a.b [2]` (this will be `parts[2]`) + * 4. [KtQualifiedExpression] `a . b` (this will be `parts[1]`) + * 5. [KtSimpleNameExpression] `a` (this will be `parts[0]`) * * Once in parts, these are in the reverse order. To render the array access correctly we need to * make sure `b` and `[2]` are in a group so we avoid splitting them. To do so we need to open a diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/Indentation.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/Indentation.kt index c4b8d8de..f1755579 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/Indentation.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/Indentation.kt @@ -3,46 +3,72 @@ package org.jetbrains.ktfmt.format.visitor import com.google.googlejavaformat.Indent import com.google.googlejavaformat.Output +/** + * A wrapper over GJF's [Indent] that provides utility operator functions. + * + * - A constant value [Const] that represents plain indent of [Const.value] space chars + * + * ``` + * fun foo(): Int = 10 + * ^^^^ // Indentantion.Const(value = 4) + * ``` + * + * - A conditional indent [If] whose value depends on whether [Indentation.If.condition] break tag + * has been taken. + * + * ``` + * // Indentantion.If(condition = condition, thenIndent = Const(8), elseIndent = Const(1)) + * + * fun foo(): Int = 10 + * ^ // [condition] is not taken, render with [elseIndent] + * + * fun foo(): Int = + * 10 + * ^^^^^^^^ // [condition] is taken, render with [trueIndent] + * + * ``` + */ sealed class Indentation { internal abstract val indent: Indent - data class Const(val value: Int) : Indentation() { + class Const(val value: Int) : Indentation() { override val indent: Indent = Indent.Const.make(value, 1) operator fun plus(other: Const): Const = Const(value + other.value) operator fun minus(other: Const): Const = Const(value - other.value) - operator fun unaryPlus(): Const = Const(+value) - override operator fun unaryMinus(): Const = Const(-value) operator fun times(other: Int): Const = Const(value * other) } - data class Cond( + class If + private constructor( val condition: Output.BreakTag, - val trueIndent: Indentation, - val falseIndent: Indentation, + val thenIndent: Indentation, + val elseIndent: Indentation, ) : Indentation() { - override val indent: Indent = Indent.If.make(condition, trueIndent.indent, falseIndent.indent) + override val indent: Indent = Indent.If.make(condition, thenIndent.indent, elseIndent.indent) + + override operator fun unaryMinus(): If = If(condition, -thenIndent, -elseIndent) - override operator fun unaryMinus(): Cond = Cond(condition, -trueIndent, -falseIndent) + companion object { + operator fun invoke( + condition: Output.BreakTag?, + thenIndent: Indentation, + elseIndent: Indentation, + ): Indentation = + when { + condition == null -> elseIndent + else -> If(condition, thenIndent, elseIndent) + } + } } abstract operator fun unaryMinus(): Indentation companion object { val ZERO = Const(0) - - fun makeCond( - condition: Output.BreakTag?, - trueIndent: Indentation, - falseIndent: Indentation, - ): Indentation = - when { - condition == null -> falseIndent - else -> Cond(condition, trueIndent, falseIndent) - } } } diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/PsiUtils.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/PsiUtils.kt index 5e4c2ddb..5841243d 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/PsiUtils.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/PsiUtils.kt @@ -28,6 +28,7 @@ import org.jetbrains.kotlin.psi.KtBinaryExpressionWithTypeRHS import org.jetbrains.kotlin.psi.KtCallExpression import org.jetbrains.kotlin.psi.KtExpression import org.jetbrains.kotlin.psi.KtLabeledExpression +import org.jetbrains.kotlin.psi.KtLambdaArgument import org.jetbrains.kotlin.psi.KtLambdaExpression import org.jetbrains.kotlin.psi.KtParameterList import org.jetbrains.kotlin.psi.KtPostfixExpression @@ -39,6 +40,7 @@ import org.jetbrains.kotlin.psi.psiUtil.children import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespace import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespace import org.jetbrains.ktfmt.format.FormattingOptions +import org.jetbrains.ktfmt.format.ParseError /** Returns true if the expression represents an invocation that is also a lambda */ val KtExpression.isLambda: Boolean From 7d466ccd0c84cb62eddf96cd34730d16a9d73375 Mon Sep 17 00:00:00 2001 From: Azat Abdullin Date: Thu, 20 Aug 2026 11:57:57 +0200 Subject: [PATCH 09/10] Introduce `KtCallElement.trailingLambda` util to simplify the logic of `formatFunctionCall` --- core/api/ktfmt.api | 6 ++-- .../ktfmt/format/KotlinInputAstVisitor.kt | 9 ++--- .../ktfmt/format/visitor/CallFormatter.kt | 34 +++++++------------ .../format/visitor/KotlinAstFormatter.kt | 2 +- .../ktfmt/format/visitor/PsiUtils.kt | 16 +++++++++ 5 files changed, 38 insertions(+), 29 deletions(-) diff --git a/core/api/ktfmt.api b/core/api/ktfmt.api index c41c496a..d15c4f31 100644 --- a/core/api/ktfmt.api +++ b/core/api/ktfmt.api @@ -456,7 +456,7 @@ public abstract interface class org/jetbrains/ktfmt/format/visitor/CallFormatter public fun formatArrayAccessExpression (Lorg/jetbrains/kotlin/psi/KtArrayAccessExpression;)V public fun formatChainedBlockLikeCall (Lorg/jetbrains/kotlin/psi/KtQualifiedExpression;Z)V public fun formatChainedScopingFunction (Lorg/jetbrains/kotlin/psi/KtQualifiedExpression;Z)V - public fun formatFunctionCall (Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/psi/KtTypeArgumentList;Lorg/jetbrains/kotlin/psi/KtValueArgumentList;Ljava/util/List;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;)V + public fun formatFunctionCall (Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/psi/KtTypeArgumentList;Lorg/jetbrains/kotlin/psi/KtValueArgumentList;Lorg/jetbrains/kotlin/psi/KtLambdaArgument;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;)V public fun formatLambdaExpression (Lorg/jetbrains/kotlin/psi/KtLambdaExpression;Lcom/google/googlejavaformat/Output$BreakTag;)V public fun formatLambdaOrScopingFunction (Lorg/jetbrains/kotlin/com/intellij/psi/PsiElement;Z)V public fun formatQualifiedExpression (Lorg/jetbrains/kotlin/psi/KtQualifiedExpression;Z)V @@ -516,8 +516,8 @@ public abstract interface class org/jetbrains/ktfmt/format/visitor/KotlinAstForm public abstract fun formatFileAnnotationList (Lorg/jetbrains/kotlin/psi/KtFileAnnotationList;)V public fun formatFinallySection (Lorg/jetbrains/kotlin/psi/KtFinallySection;)V public fun formatForExpression (Lorg/jetbrains/kotlin/psi/KtForExpression;)V - public abstract fun formatFunctionCall (Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/psi/KtTypeArgumentList;Lorg/jetbrains/kotlin/psi/KtValueArgumentList;Ljava/util/List;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;)V - public static synthetic fun formatFunctionCall$default (Lorg/jetbrains/ktfmt/format/visitor/KotlinAstFormatter;Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/psi/KtTypeArgumentList;Lorg/jetbrains/kotlin/psi/KtValueArgumentList;Ljava/util/List;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;ILjava/lang/Object;)V + public abstract fun formatFunctionCall (Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/psi/KtTypeArgumentList;Lorg/jetbrains/kotlin/psi/KtValueArgumentList;Lorg/jetbrains/kotlin/psi/KtLambdaArgument;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;)V + public static synthetic fun formatFunctionCall$default (Lorg/jetbrains/ktfmt/format/visitor/KotlinAstFormatter;Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/psi/KtTypeArgumentList;Lorg/jetbrains/kotlin/psi/KtValueArgumentList;Lorg/jetbrains/kotlin/psi/KtLambdaArgument;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;ILjava/lang/Object;)V public abstract fun formatFunctionType (Lorg/jetbrains/kotlin/psi/KtFunctionType;)V public fun formatIfExpression (Lorg/jetbrains/kotlin/psi/KtIfExpression;)V public fun formatImportDirective (Lorg/jetbrains/kotlin/psi/KtImportDirective;)V diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/KotlinInputAstVisitor.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/KotlinInputAstVisitor.kt index d282df64..721dfdfc 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/KotlinInputAstVisitor.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/KotlinInputAstVisitor.kt @@ -126,6 +126,7 @@ import org.jetbrains.ktfmt.format.visitor.isLambdaOrScopingFunction import org.jetbrains.ktfmt.format.visitor.open import org.jetbrains.ktfmt.format.visitor.sync import org.jetbrains.ktfmt.format.visitor.token +import org.jetbrains.ktfmt.format.visitor.trailingLambda import org.jetbrains.ktfmt.util.CONTEXT_PARAMETER_LIST import org.jetbrains.ktfmt.util.ownValOrVarKeywordText @@ -343,7 +344,7 @@ open class KotlinInputAstVisitor( calleeExpression, typeArgumentList, valueArgumentList, - lambdaArguments, + trailingLambda, ) } } @@ -829,7 +830,7 @@ open class KotlinInputAstVisitor( null, call.typeArgumentList, call.valueArgumentList, - call.lambdaArguments, + call.trailingLambda, ) } } @@ -908,7 +909,7 @@ open class KotlinInputAstVisitor( override fun visitSuperTypeCallEntry(call: KtSuperTypeCallEntry) { builder.sync(call) - formatFunctionCall(call.calleeExpression, null, call.valueArgumentList, call.lambdaArguments) + formatFunctionCall(call.calleeExpression, null, call.valueArgumentList, call.trailingLambda) } /** @@ -1285,7 +1286,7 @@ open class KotlinInputAstVisitor( receiverExpression.calleeExpression, receiverExpression.typeArgumentList, receiverExpression.valueArgumentList, - receiverExpression.lambdaArguments, + receiverExpression.trailingLambda, ) } else { visit(receiverExpression) diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt index 3f43bc0b..5b77a2b2 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt @@ -22,7 +22,6 @@ import org.jetbrains.kotlin.psi.KtValueArgumentList import org.jetbrains.kotlin.psi.KtWhenExpression import org.jetbrains.kotlin.psi.psiUtil.children import org.jetbrains.kotlin.psi.psiUtil.startsWithComment -import org.jetbrains.ktfmt.format.ParseError import org.jetbrains.ktfmt.format.visitor.Indentation.Companion.ZERO /** @@ -75,17 +74,15 @@ interface CallFormatter : KotlinAstFormatter { * @param callee the function call expression * @param typeArgumentList the type arguments of the function call expression * @param argumentList the value arguments of the function call expression - * @param lambdaArguments trailing lambda arguments of the call expression; in correspondence with - * Kotlin syntax a function call can't have more than one trailing lambda, but - * [KtCallExpression] represents them as a list (see [KtCallExpression.getLambdaArguments]) + * @param trailingLambda trailing lambda arguments of the call expression * @param argumentsIndent how to indent [argumentList], if present - * @param lambdaIndent how to indent [lambdaArguments], if present + * @param lambdaIndent how to indent [trailingLambda], if present */ override fun formatFunctionCall( callee: KtExpression?, typeArgumentList: KtTypeArgumentList?, argumentList: KtValueArgumentList?, - lambdaArguments: List, + trailingLambda: KtLambdaArgument?, argumentsIndent: Indentation, lambdaIndent: Indentation, ) { @@ -107,18 +104,13 @@ interface CallFormatter : KotlinAstFormatter { } } } - when (lambdaArguments.size) { - 0 -> {} - 1 -> { - builder.space() - formatArgument( - lambdaArguments.single(), - wrapInBlock = false, - brokeBeforeBrace = brokeBeforeBrace, - ) - } - - else -> throw ParseError("Maximum one trailing lambda is allowed", lambdaArguments[1]) + trailingLambda?.let { + builder.space() + formatArgument( + it, + wrapInBlock = false, + brokeBeforeBrace = brokeBeforeBrace, + ) } } } @@ -267,7 +259,7 @@ interface CallFormatter : KotlinAstFormatter { null, selectorExpression.typeArgumentList, selectorExpression.valueArgumentList, - selectorExpression.lambdaArguments, + selectorExpression.trailingLambda, ) } else { format(selectorExpression) @@ -304,7 +296,7 @@ interface CallFormatter : KotlinAstFormatter { null, selectorExpression.typeArgumentList, selectorExpression.valueArgumentList, - selectorExpression.lambdaArguments, + selectorExpression.trailingLambda, ) } else { format(selectorExpression) @@ -440,7 +432,7 @@ interface CallFormatter : KotlinAstFormatter { null, callee.typeArgumentList, callee.valueArgumentList, - callee.lambdaArguments, + callee.trailingLambda, argumentsIndent = argumentsIndent, lambdaIndent = lambdaIndent, ) diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter.kt index 60dbb939..0b5d961e 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter.kt @@ -523,7 +523,7 @@ interface KotlinAstFormatter { callee: KtExpression?, typeArgumentList: KtTypeArgumentList?, argumentList: KtValueArgumentList?, - lambdaArguments: List, + trailingLambda: KtLambdaArgument?, argumentsIndent: Indentation = expressionBreakIndent, lambdaIndent: Indentation = Indentation.ZERO, ) diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/PsiUtils.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/PsiUtils.kt index 5841243d..8bc112ee 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/PsiUtils.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/PsiUtils.kt @@ -25,6 +25,7 @@ import org.jetbrains.kotlin.lexer.KtSingleValueToken import org.jetbrains.kotlin.psi.KtArrayAccessExpression import org.jetbrains.kotlin.psi.KtBinaryExpression import org.jetbrains.kotlin.psi.KtBinaryExpressionWithTypeRHS +import org.jetbrains.kotlin.psi.KtCallElement import org.jetbrains.kotlin.psi.KtCallExpression import org.jetbrains.kotlin.psi.KtExpression import org.jetbrains.kotlin.psi.KtLabeledExpression @@ -300,3 +301,18 @@ internal fun KtExpression?.startsWithUpperCase(): Boolean { internal val KtExpression?.isBinaryExpression: Boolean get() = this is KtBinaryExpression || this is KtBinaryExpressionWithTypeRHS + +/** + * Returns the trailing lambda argument of a function call expression or null if its not present. + * + * A function call can't have more than one trailing lambda, but [KtCallElement] and + * [KtCallExpression] represent them as a list (see [KtCallExpression.getLambdaArguments] for more + * details). + */ +internal val KtCallElement.trailingLambda: KtLambdaArgument? + get() { + val lambdas = lambdaArguments + if (lambdas.isEmpty()) return null + if (lambdas.size == 1) return lambdas.first() + else throw ParseError("Maximum one trailing lambda is allowed", lambdaArguments[1]) + } From 17f48b5c3bd6fe1264ac94fcf8673fb4fdb11e6f Mon Sep 17 00:00:00 2001 From: Azat Abdullin Date: Fri, 21 Aug 2026 15:53:43 +0200 Subject: [PATCH 10/10] Rebase --- core/api/ktfmt.api | 82 ++++++++++--------- .../ktfmt/format/KotlinInputAstVisitor.kt | 24 ------ .../format/visitor/AnnotationFormatter.kt | 6 +- .../format/visitor/KotlinAstFormatter.kt | 16 ---- .../KotlinLangAnnotationFormatter.kt | 3 +- 5 files changed, 48 insertions(+), 83 deletions(-) diff --git a/core/api/ktfmt.api b/core/api/ktfmt.api index d15c4f31..93dacecc 100644 --- a/core/api/ktfmt.api +++ b/core/api/ktfmt.api @@ -183,27 +183,15 @@ public final class org/jetbrains/ktfmt/format/KotlinInput : com/google/googlejav public fun getkN ()I } -public class org/jetbrains/ktfmt/format/KotlinInputAstVisitor : org/jetbrains/ktfmt/format/visitor/AbstractFormatterVisitor, org/jetbrains/ktfmt/format/visitor/AnnotationFormatter, org/jetbrains/ktfmt/format/visitor/ExpressionFormatter, org/jetbrains/ktfmt/format/visitor/FileFormatter, org/jetbrains/ktfmt/format/visitor/ListFormatter, org/jetbrains/ktfmt/format/visitor/TypeFormatter { -public class org/jetbrains/ktfmt/format/KotlinInputAstVisitor : org/jetbrains/ktfmt/format/visitor/AbstractFormatterVisitor, org/jetbrains/ktfmt/format/visitor/CallFormatter, org/jetbrains/ktfmt/format/visitor/ExpressionFormatter, org/jetbrains/ktfmt/format/visitor/FileFormatter, org/jetbrains/ktfmt/format/visitor/ListFormatter, org/jetbrains/ktfmt/format/visitor/TypeFormatter { +public class org/jetbrains/ktfmt/format/KotlinInputAstVisitor : org/jetbrains/ktfmt/format/visitor/AbstractFormatterVisitor, org/jetbrains/ktfmt/format/visitor/AnnotationFormatter, org/jetbrains/ktfmt/format/visitor/CallFormatter, org/jetbrains/ktfmt/format/visitor/ExpressionFormatter, org/jetbrains/ktfmt/format/visitor/FileFormatter, org/jetbrains/ktfmt/format/visitor/ListFormatter, org/jetbrains/ktfmt/format/visitor/TypeFormatter { public fun (Lorg/jetbrains/ktfmt/format/FormattingOptions;Lcom/google/googlejavaformat/OpsBuilder;)V - public fun formatCallElement (Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/psi/KtTypeArgumentList;Lorg/jetbrains/kotlin/psi/KtValueArgumentList;Ljava/util/List;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;)V - public fun getBlockIndent ()Lcom/google/googlejavaformat/Indent$Const; - public fun getBlockPlusExpressionBreakIndent ()Lcom/google/googlejavaformat/Indent$Const; + public fun getBlockIndent ()Lorg/jetbrains/ktfmt/format/visitor/Indentation$Const; public fun getBuilder ()Lcom/google/googlejavaformat/OpsBuilder; - public fun getDoubleExpressionBreakIndent ()Lcom/google/googlejavaformat/Indent$Const; - public fun getExpressionBreakIndent ()Lcom/google/googlejavaformat/Indent$Const; - public fun getExpressionBreakNegativeIndent ()Lcom/google/googlejavaformat/Indent$Const; + public fun getExpressionBreakIndent ()Lorg/jetbrains/ktfmt/format/visitor/Indentation$Const; public fun getInExpression ()Ljava/util/ArrayDeque; public fun getInImport ()Z public fun getOptions ()Lorg/jetbrains/ktfmt/format/FormattingOptions; - public fun visitArgument (Lorg/jetbrains/kotlin/psi/KtValueArgument;)V - public fun visitArrayAccessExpression (Lorg/jetbrains/kotlin/psi/KtArrayAccessExpression;)V public fun setInImport (Z)V - public fun visitAnnotatedExpression (Lorg/jetbrains/kotlin/psi/KtAnnotatedExpression;)V - public fun visitAnnotation (Lorg/jetbrains/kotlin/psi/KtAnnotation;)V - public fun visitAnnotationEntry (Lorg/jetbrains/kotlin/psi/KtAnnotationEntry;)V - public synthetic fun visitAnnotationUseSiteTarget (Lorg/jetbrains/kotlin/psi/KtAnnotationUseSiteTarget;Ljava/lang/Object;)Ljava/lang/Object; - public fun visitAnnotationUseSiteTarget (Lorg/jetbrains/kotlin/psi/KtAnnotationUseSiteTarget;Ljava/lang/Void;)Ljava/lang/Void; public fun visitBinaryExpression (Lorg/jetbrains/kotlin/psi/KtBinaryExpression;)V public fun visitBinaryWithTypeRHSExpression (Lorg/jetbrains/kotlin/psi/KtBinaryExpressionWithTypeRHS;)V public fun visitBlockExpression (Lorg/jetbrains/kotlin/psi/KtBlockExpression;)V @@ -456,12 +444,10 @@ public abstract interface class org/jetbrains/ktfmt/format/visitor/CallFormatter public fun formatArrayAccessExpression (Lorg/jetbrains/kotlin/psi/KtArrayAccessExpression;)V public fun formatChainedBlockLikeCall (Lorg/jetbrains/kotlin/psi/KtQualifiedExpression;Z)V public fun formatChainedScopingFunction (Lorg/jetbrains/kotlin/psi/KtQualifiedExpression;Z)V - public fun formatFunctionCall (Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/psi/KtTypeArgumentList;Lorg/jetbrains/kotlin/psi/KtValueArgumentList;Lorg/jetbrains/kotlin/psi/KtLambdaArgument;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;)V + public fun formatFunctionCall (Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/psi/KtTypeArgumentList;Lorg/jetbrains/kotlin/psi/KtValueArgumentList;Lorg/jetbrains/kotlin/psi/KtLambdaArgument;Lorg/jetbrains/ktfmt/format/visitor/Indentation;Lorg/jetbrains/ktfmt/format/visitor/Indentation;)V public fun formatLambdaExpression (Lorg/jetbrains/kotlin/psi/KtLambdaExpression;Lcom/google/googlejavaformat/Output$BreakTag;)V public fun formatLambdaOrScopingFunction (Lorg/jetbrains/kotlin/com/intellij/psi/PsiElement;Z)V - public fun formatQualifiedExpression (Lorg/jetbrains/kotlin/psi/KtQualifiedExpression;Z)V - public abstract fun getBlockPlusExpressionBreakIndent ()Lcom/google/googlejavaformat/Indent$Const; - public abstract fun getDoubleExpressionBreakIndent ()Lcom/google/googlejavaformat/Indent$Const; + public fun formatQualifiedExpression (Lorg/jetbrains/kotlin/psi/KtQualifiedExpression;)V } public abstract interface class org/jetbrains/ktfmt/format/visitor/ExpressionFormatter : org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter { @@ -475,6 +461,39 @@ public abstract interface class org/jetbrains/ktfmt/format/visitor/FileFormatter public fun formatStatements ([Lorg/jetbrains/kotlin/com/intellij/psi/PsiElement;)V } +public abstract class org/jetbrains/ktfmt/format/visitor/Indentation { + public static final field Companion Lorg/jetbrains/ktfmt/format/visitor/Indentation$Companion; + public abstract fun unaryMinus ()Lorg/jetbrains/ktfmt/format/visitor/Indentation; +} + +public final class org/jetbrains/ktfmt/format/visitor/Indentation$Companion { + public final fun getZERO ()Lorg/jetbrains/ktfmt/format/visitor/Indentation$Const; +} + +public final class org/jetbrains/ktfmt/format/visitor/Indentation$Const : org/jetbrains/ktfmt/format/visitor/Indentation { + public fun (I)V + public final fun getValue ()I + public final fun minus (Lorg/jetbrains/ktfmt/format/visitor/Indentation$Const;)Lorg/jetbrains/ktfmt/format/visitor/Indentation$Const; + public final fun plus (Lorg/jetbrains/ktfmt/format/visitor/Indentation$Const;)Lorg/jetbrains/ktfmt/format/visitor/Indentation$Const; + public final fun times (I)Lorg/jetbrains/ktfmt/format/visitor/Indentation$Const; + public fun unaryMinus ()Lorg/jetbrains/ktfmt/format/visitor/Indentation$Const; + public synthetic fun unaryMinus ()Lorg/jetbrains/ktfmt/format/visitor/Indentation; +} + +public final class org/jetbrains/ktfmt/format/visitor/Indentation$If : org/jetbrains/ktfmt/format/visitor/Indentation { + public static final field Companion Lorg/jetbrains/ktfmt/format/visitor/Indentation$If$Companion; + public synthetic fun (Lcom/google/googlejavaformat/Output$BreakTag;Lorg/jetbrains/ktfmt/format/visitor/Indentation;Lorg/jetbrains/ktfmt/format/visitor/Indentation;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getCondition ()Lcom/google/googlejavaformat/Output$BreakTag; + public final fun getElseIndent ()Lorg/jetbrains/ktfmt/format/visitor/Indentation; + public final fun getThenIndent ()Lorg/jetbrains/ktfmt/format/visitor/Indentation; + public fun unaryMinus ()Lorg/jetbrains/ktfmt/format/visitor/Indentation$If; + public synthetic fun unaryMinus ()Lorg/jetbrains/ktfmt/format/visitor/Indentation; +} + +public final class org/jetbrains/ktfmt/format/visitor/Indentation$If$Companion { + public final fun invoke (Lcom/google/googlejavaformat/Output$BreakTag;Lorg/jetbrains/ktfmt/format/visitor/Indentation;Lorg/jetbrains/ktfmt/format/visitor/Indentation;)Lorg/jetbrains/ktfmt/format/visitor/Indentation; +} + public abstract interface class org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter { public fun fail (Ljava/lang/String;)Ljava/lang/Void; public static synthetic fun fail$default (Lorg/jetbrains/ktfmt/format/visitor/KotlinAstFormatter;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/Void; @@ -489,8 +508,6 @@ public abstract interface class org/jetbrains/ktfmt/format/visitor/KotlinAstForm public fun formatBinaryWithTypeRHSExpression (Lorg/jetbrains/kotlin/psi/KtBinaryExpressionWithTypeRHS;)V public fun formatBlockExpression (Lorg/jetbrains/kotlin/psi/KtBlockExpression;)V public fun formatBreakExpression (Lorg/jetbrains/kotlin/psi/KtBreakExpression;)V - public abstract fun formatCallElement (Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/psi/KtTypeArgumentList;Lorg/jetbrains/kotlin/psi/KtValueArgumentList;Ljava/util/List;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;)V - public static synthetic fun formatCallElement$default (Lorg/jetbrains/ktfmt/format/visitor/KotlinAstFormatter;Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/psi/KtTypeArgumentList;Lorg/jetbrains/kotlin/psi/KtValueArgumentList;Ljava/util/List;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;ILjava/lang/Object;)V public fun formatCallExpression (Lorg/jetbrains/kotlin/psi/KtCallExpression;)V public fun formatCallableReferenceExpression (Lorg/jetbrains/kotlin/psi/KtCallableReferenceExpression;)V public fun formatCatchSection (Lorg/jetbrains/kotlin/psi/KtCatchClause;)V @@ -516,8 +533,8 @@ public abstract interface class org/jetbrains/ktfmt/format/visitor/KotlinAstForm public abstract fun formatFileAnnotationList (Lorg/jetbrains/kotlin/psi/KtFileAnnotationList;)V public fun formatFinallySection (Lorg/jetbrains/kotlin/psi/KtFinallySection;)V public fun formatForExpression (Lorg/jetbrains/kotlin/psi/KtForExpression;)V - public abstract fun formatFunctionCall (Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/psi/KtTypeArgumentList;Lorg/jetbrains/kotlin/psi/KtValueArgumentList;Lorg/jetbrains/kotlin/psi/KtLambdaArgument;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;)V - public static synthetic fun formatFunctionCall$default (Lorg/jetbrains/ktfmt/format/visitor/KotlinAstFormatter;Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/psi/KtTypeArgumentList;Lorg/jetbrains/kotlin/psi/KtValueArgumentList;Lorg/jetbrains/kotlin/psi/KtLambdaArgument;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;Lcom/google/googlejavaformat/Indent;ILjava/lang/Object;)V + public abstract fun formatFunctionCall (Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/psi/KtTypeArgumentList;Lorg/jetbrains/kotlin/psi/KtValueArgumentList;Lorg/jetbrains/kotlin/psi/KtLambdaArgument;Lorg/jetbrains/ktfmt/format/visitor/Indentation;Lorg/jetbrains/ktfmt/format/visitor/Indentation;)V + public static synthetic fun formatFunctionCall$default (Lorg/jetbrains/ktfmt/format/visitor/KotlinAstFormatter;Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/psi/KtTypeArgumentList;Lorg/jetbrains/kotlin/psi/KtValueArgumentList;Lorg/jetbrains/kotlin/psi/KtLambdaArgument;Lorg/jetbrains/ktfmt/format/visitor/Indentation;Lorg/jetbrains/ktfmt/format/visitor/Indentation;ILjava/lang/Object;)V public abstract fun formatFunctionType (Lorg/jetbrains/kotlin/psi/KtFunctionType;)V public fun formatIfExpression (Lorg/jetbrains/kotlin/psi/KtIfExpression;)V public fun formatImportDirective (Lorg/jetbrains/kotlin/psi/KtImportDirective;)V @@ -542,8 +559,7 @@ public abstract interface class org/jetbrains/ktfmt/format/visitor/KotlinAstForm public fun formatPrefixExpression (Lorg/jetbrains/kotlin/psi/KtPrefixExpression;)V public fun formatPrimaryConstructor (Lorg/jetbrains/kotlin/psi/KtPrimaryConstructor;)V public fun formatProperty (Lorg/jetbrains/kotlin/psi/KtProperty;)V - public fun formatQualifiedExpression (Lorg/jetbrains/kotlin/psi/KtQualifiedExpression;Z)V - public static synthetic fun formatQualifiedExpression$default (Lorg/jetbrains/ktfmt/format/visitor/KotlinAstFormatter;Lorg/jetbrains/kotlin/psi/KtQualifiedExpression;ZILjava/lang/Object;)V + public abstract fun formatQualifiedExpression (Lorg/jetbrains/kotlin/psi/KtQualifiedExpression;)V public fun formatReferenceExpression (Lorg/jetbrains/kotlin/psi/KtReferenceExpression;)V public fun formatReturnExpression (Lorg/jetbrains/kotlin/psi/KtReturnExpression;)V public fun formatSecondaryConstructor (Lorg/jetbrains/kotlin/psi/KtSecondaryConstructor;)V @@ -572,10 +588,9 @@ public abstract interface class org/jetbrains/ktfmt/format/visitor/KotlinAstForm public fun formatWhenConditionWithExpression (Lorg/jetbrains/kotlin/psi/KtWhenConditionWithExpression;)V public fun formatWhenExpression (Lorg/jetbrains/kotlin/psi/KtWhenExpression;)V public fun formatWhileExpression (Lorg/jetbrains/kotlin/psi/KtWhileExpression;)V - public abstract fun getBlockIndent ()Lcom/google/googlejavaformat/Indent$Const; + public abstract fun getBlockIndent ()Lorg/jetbrains/ktfmt/format/visitor/Indentation$Const; public abstract fun getBuilder ()Lcom/google/googlejavaformat/OpsBuilder; - public abstract fun getExpressionBreakIndent ()Lcom/google/googlejavaformat/Indent$Const; - public abstract fun getExpressionBreakNegativeIndent ()Lcom/google/googlejavaformat/Indent$Const; + public abstract fun getExpressionBreakIndent ()Lorg/jetbrains/ktfmt/format/visitor/Indentation$Const; public abstract fun getInExpression ()Ljava/util/ArrayDeque; public abstract fun getInImport ()Z public abstract fun getOptions ()Lorg/jetbrains/ktfmt/format/FormattingOptions; @@ -597,16 +612,6 @@ public abstract interface class org/jetbrains/ktfmt/format/visitor/ListFormatter public fun formatValueArgumentList (Lorg/jetbrains/kotlin/psi/KtValueArgumentList;)Lcom/google/googlejavaformat/Output$BreakTag; } -public final class org/jetbrains/ktfmt/format/visitor/OpsUtilsKt { - public static final fun block (Lcom/google/googlejavaformat/OpsBuilder;Lcom/google/googlejavaformat/Indent;ZLkotlin/jvm/functions/Function0;)V - public static synthetic fun block$default (Lcom/google/googlejavaformat/OpsBuilder;Lcom/google/googlejavaformat/Indent;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V - public static final fun fenceComments (Lcom/google/googlejavaformat/OpsBuilder;)V - public static final fun getAsIndent (I)Lcom/google/googlejavaformat/Indent$Const; - public static final fun sync (Lcom/google/googlejavaformat/OpsBuilder;Lorg/jetbrains/kotlin/com/intellij/psi/PsiElement;)V - public static final fun token (Lcom/google/googlejavaformat/OpsBuilder;Ljava/lang/String;Lcom/google/googlejavaformat/Indent;)V - public static synthetic fun token$default (Lcom/google/googlejavaformat/OpsBuilder;Ljava/lang/String;Lcom/google/googlejavaformat/Indent;ILjava/lang/Object;)V -} - public final class org/jetbrains/ktfmt/format/visitor/PsiUtilsKt { public static final fun chainedSelectorsHaveValueArguments (Lorg/jetbrains/kotlin/psi/KtExpression;)Z public static final fun getCallExpression (Lorg/jetbrains/kotlin/psi/KtExpression;)Lorg/jetbrains/kotlin/psi/KtCallExpression; @@ -616,7 +621,6 @@ public final class org/jetbrains/ktfmt/format/visitor/PsiUtilsKt { public static final fun getHasEmptyParenthesis (Lorg/jetbrains/kotlin/psi/KtValueArgumentList;)Z public static final fun getHasLineBreakingCommentBefore (Lorg/jetbrains/kotlin/com/intellij/psi/PsiElement;)Z public static final fun getHasSourceNewlineInLambdaBody (Lorg/jetbrains/kotlin/psi/KtLambdaExpression;)Z - public static final fun getScopingLambda (Lorg/jetbrains/kotlin/psi/KtExpression;)Lorg/jetbrains/kotlin/psi/KtLambdaExpression; public static final fun isBlockLikeArgument (Lorg/jetbrains/kotlin/psi/KtValueArgument;)Z public static final fun isBlockLikeCall (Lorg/jetbrains/kotlin/psi/KtExpression;)Z public static final fun isChainedBlockLikeCall (Lorg/jetbrains/kotlin/psi/KtExpression;)Z diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/KotlinInputAstVisitor.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/KotlinInputAstVisitor.kt index 721dfdfc..50d093e6 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/KotlinInputAstVisitor.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/KotlinInputAstVisitor.kt @@ -30,10 +30,6 @@ import kotlin.jvm.optionals.getOrNull import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.com.intellij.psi.stubs.PsiFileStubImpl import org.jetbrains.kotlin.lexer.KtTokens -import org.jetbrains.kotlin.psi.KtAnnotatedExpression -import org.jetbrains.kotlin.psi.KtAnnotation -import org.jetbrains.kotlin.psi.KtAnnotationEntry -import org.jetbrains.kotlin.psi.KtAnnotationUseSiteTarget import org.jetbrains.kotlin.psi.KtBackingField import org.jetbrains.kotlin.psi.KtBinaryExpression import org.jetbrains.kotlin.psi.KtBinaryExpressionWithTypeRHS @@ -349,26 +345,6 @@ open class KotlinInputAstVisitor( } } - override fun formatCallElement( - callee: KtExpression?, - typeArgumentList: KtTypeArgumentList?, - argumentList: KtValueArgumentList?, - lambdaArguments: List, - argumentsIndent: Indent, - lambdaIndent: Indent, - negativeLambdaIndent: Indent, - ) { - visitCallElement( - callee, - typeArgumentList, - argumentList, - lambdaArguments, - argumentsIndent, - lambdaIndent, - negativeLambdaIndent, - ) - } - /** Example `this` or `this@Foo` */ override fun visitThisExpression(expression: KtThisExpression) { builder.sync(expression) diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/AnnotationFormatter.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/AnnotationFormatter.kt index df6167c7..2f02c73d 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/AnnotationFormatter.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/AnnotationFormatter.kt @@ -1,7 +1,6 @@ package org.jetbrains.ktfmt.format.visitor import com.google.googlejavaformat.Doc -import com.google.googlejavaformat.Indent.Const.ZERO import org.jetbrains.kotlin.psi.KtAnnotatedExpression import org.jetbrains.kotlin.psi.KtAnnotation import org.jetbrains.kotlin.psi.KtAnnotationEntry @@ -9,6 +8,7 @@ import org.jetbrains.kotlin.psi.KtAnnotationUseSiteTarget import org.jetbrains.kotlin.psi.KtBlockExpression import org.jetbrains.kotlin.psi.KtLambdaExpression import org.jetbrains.kotlin.psi.KtReturnExpression +import org.jetbrains.ktfmt.format.visitor.Indentation.Companion.ZERO interface AnnotationFormatter : KotlinAstFormatter { override fun formatAnnotatedExpression(expression: KtAnnotatedExpression) { @@ -90,11 +90,11 @@ interface AnnotationFormatter : KotlinAstFormatter { format(useSiteTarget) builder.token(":") } - formatCallElement( + formatFunctionCall( annotationEntry.calleeExpression, null, // Type-arguments are included in the annotation's callee expression. annotationEntry.valueArgumentList, - listOf(), + null, ) } } diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter.kt index 0b5d961e..b492d3f3 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/KotlinAstFormatter.kt @@ -551,22 +551,6 @@ interface KotlinAstFormatter { emitLeadingBreak: Boolean, ) - /** - * Examples `foo(a, b)`, `foo(a)`, `boo()`, `super(a)` - * - * @param lambdaIndent how to indent [lambdaArguments], if present - * @param negativeLambdaIndent the negative indentation of [lambdaIndent] - */ - fun formatCallElement( - callee: KtExpression?, - typeArgumentList: KtTypeArgumentList?, - argumentList: KtValueArgumentList?, - lambdaArguments: List, - argumentsIndent: Indent = expressionBreakIndent, - lambdaIndent: Indent = ZERO, - negativeLambdaIndent: Indent = ZERO, - ) - /** * markForPartialFormat is used to delineate the smallest areas of code that must be formatted * together. diff --git a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/kotlinlang/KotlinLangAnnotationFormatter.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/kotlinlang/KotlinLangAnnotationFormatter.kt index 4a757556..db02e86a 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/kotlinlang/KotlinLangAnnotationFormatter.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/kotlinlang/KotlinLangAnnotationFormatter.kt @@ -1,13 +1,14 @@ package org.jetbrains.ktfmt.format.visitor.kotlinlang import com.google.googlejavaformat.Doc -import com.google.googlejavaformat.Indent.Const.ZERO import org.jetbrains.kotlin.psi.KtAnnotatedExpression import org.jetbrains.kotlin.psi.KtBlockExpression import org.jetbrains.kotlin.psi.KtLambdaExpression import org.jetbrains.kotlin.psi.KtReturnExpression import org.jetbrains.ktfmt.format.visitor.AnnotationFormatter +import org.jetbrains.ktfmt.format.visitor.Indentation.Companion.ZERO import org.jetbrains.ktfmt.format.visitor.block +import org.jetbrains.ktfmt.format.visitor.breakOp import org.jetbrains.ktfmt.format.visitor.isBinaryExpression import org.jetbrains.ktfmt.format.visitor.sync