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") diff --git a/core/api/ktfmt.api b/core/api/ktfmt.api index ec9d61ab..93dacecc 100644 --- a/core/api/ktfmt.api +++ b/core/api/ktfmt.api @@ -183,19 +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/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 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 ()Lorg/jetbrains/ktfmt/format/visitor/Indentation$Const; public fun getBuilder ()Lcom/google/googlejavaformat/OpsBuilder; - 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 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 +219,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 +227,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 +439,17 @@ 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;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;)V +} + 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,22 +457,57 @@ 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 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; 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 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 @@ -493,6 +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;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 @@ -503,7 +545,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 +559,13 @@ 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 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 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 +588,14 @@ 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 ()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; 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 { @@ -566,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; @@ -585,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 4f734abe..50d093e6 100644 --- a/core/src/main/kotlin/org/jetbrains/ktfmt/format/KotlinInputAstVisitor.kt +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/KotlinInputAstVisitor.kt @@ -22,18 +22,14 @@ 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 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.KtBackingField import org.jetbrains.kotlin.psi.KtBinaryExpression import org.jetbrains.kotlin.psi.KtBinaryExpressionWithTypeRHS @@ -57,7 +53,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 +63,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,45 +88,41 @@ 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.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.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.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.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.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 @@ -143,32 +133,27 @@ 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: 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 - - private val blockPlusExpressionBreakIndent: Indent.Const = - (options.blockIndent + options.continuationIndent).asIndent - - private 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)) /** 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) { @@ -273,7 +258,7 @@ open class KotlinInputAstVisitor( } emitTypeOrDelegationCall { builder.space() - builder.block(expressionBreakNegativeIndent) { visit(typeOrDelegationCall) } + builder.block(-expressionBreakIndent) { visit(typeOrDelegationCall) } } } } @@ -305,7 +290,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) { @@ -319,26 +309,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,477 +333,18 @@ 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, - lambdaArguments, + trailingLambda, ) } } - 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, - ) - } - - /** - * 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 +373,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 +406,7 @@ open class KotlinInputAstVisitor( visit(expression.left) builder.space() builder.token(expression.operationReference.text) - visitLambdaOrScopingFunction(expression.right) + formatLambdaOrScopingFunction(expression.right) return } @@ -1124,12 +586,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 +705,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,11 +802,11 @@ 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, - call.lambdaArguments, + call.trailingLambda, ) } } @@ -1542,7 +885,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.trailingLambda) } /** @@ -1563,7 +906,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) { @@ -1674,7 +1022,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 +1085,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,11 +1258,11 @@ open class KotlinInputAstVisitor( builder.sync(expression) val receiverExpression = expression.receiverExpression if (receiverExpression is KtCallExpression) { - visitCallElement( + formatFunctionCall( receiverExpression.calleeExpression, receiverExpression.typeArgumentList, receiverExpression.valueArgumentList, - receiverExpression.lambdaArguments, + receiverExpression.trailingLambda, ) } else { visit(receiverExpression) @@ -2110,16 +1430,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) @@ -2151,7 +1461,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/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/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/CallFormatter.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt new file mode 100644 index 00000000..5b77a2b2 --- /dev/null +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/CallFormatter.kt @@ -0,0 +1,472 @@ +package org.jetbrains.ktfmt.format.visitor + +import com.google.googlejavaformat.Doc +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.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 +import org.jetbrains.kotlin.psi.KtStringTemplateExpression +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.visitor.Indentation.Companion.ZERO + +/** + * 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, + 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()) + } + } + } + + /** + * 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 trailingLambda trailing lambda arguments of the call expression + * @param argumentsIndent how to indent [argumentList], if present + * @param lambdaIndent how to indent [trailingLambda], if present + */ + override fun formatFunctionCall( + callee: KtExpression?, + typeArgumentList: KtTypeArgumentList?, + argumentList: KtValueArgumentList?, + trailingLambda: KtLambdaArgument?, + 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. + // 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(-lambdaIndent) { + format(callee) + builder.block(argumentsIndent) { + builder.block(ZERO) { format(typeArgumentList) } + if (argumentList != null) { + brokeBeforeBrace = formatValueArgumentList(argumentList) + } + } + } + trailingLambda?.let { + builder.space() + formatArgument( + it, + wrapInBlock = false, + brokeBeforeBrace = brokeBeforeBrace, + ) + } + } + } + + override fun formatLambdaExpression( + lambdaExpression: KtLambdaExpression, + brokeBeforeBrace: BreakTag?, + ) { + builder.sync(lambdaExpression) + + val bodyExpression = lambdaExpression.bodyExpression ?: fail() + val hasStatements = bodyExpression.children.isNotEmpty() + val hasComments = bodyExpression.children().any { it is PsiComment } + + val hasDeclaration = + lambdaExpression.valueParameters.isNotEmpty() || + lambdaExpression.functionLiteral.arrow != null + val hasBody = hasDeclaration || hasStatements || hasComments + + /** + * 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 bodyIndent = + Indentation.If(brokeBeforeBrace, blockIndent + expressionBreakIndent, blockIndent) + val declarationIndent = + Indentation.If(brokeBeforeBrace, expressionBreakIndent * 2, expressionBreakIndent) + val closingBraceIndent = Indentation.If(brokeBeforeBrace, expressionBreakIndent, ZERO) + + builder.token("{") + + if (hasDeclaration) { + formatLambdaArguments( + lambdaExpression.functionLiteral.valueParameterList!!, + declarationIndent, + bodyIndent, + ) + } + + if (hasBody) { + builder.breakOp(Doc.FillMode.UNIFIED, " ", closingBraceIndent) + } + + 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) + } + } + + 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 + + val singleLineStatement = + expressionStatements.size == 1 && + expressionStatements.first() !is KtReturnExpression && + !bodyExpression.startsWithComment() + + if (!shouldForceMultiline && singleLineStatement) { + formatStatement(expressionStatements[0]) + } else { + formatStatements(expressionStatements) + } + } else { + builder.fenceComments() + builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO) + for ((i, comment) in blockComments.withIndex()) { + if (i > 0) { + builder.forcedBreak() + } + builder.token(comment.text) + } + } + builder.breakOp(Doc.FillMode.UNIFIED, " ", braceIndent) + } + } + + 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.trailingLambda, + ) + } 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.trailingLambda, + ) + } 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), + ) + + val scopingLambda = expr.scopingLambda ?: throw AssertionError(expr) + scopingLambda.receiverExpression?.let { + format(it) + builder.token(scopingLambda.operation!!.value) + } + scopingLambda.calleeExpression?.let { + format(it) + builder.space() + } + scopingLambda.labeledExpression?.let { + format(it.labelQualifier) + } + formatLambdaExpression(scopingLambda.lambdaExpression, breakToExpr) + } + + override fun formatQualifiedExpression(expression: KtQualifiedExpression) { + builder.sync(expression) + val receiver = expression.receiverExpression + when { + inImport -> { + format(receiver) + val selectorExpression = expression.selectorExpression + if (selectorExpression != null) { + builder.token(".") + format(selectorExpression) + } + } + receiver is KtStringTemplateExpression -> { + builder.block(expressionBreakIndent) { + format(receiver) + builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO) + builder.token(expression.operationSign.value) + format(expression.selectorExpression) + } + } + receiver is KtWhenExpression -> { + builder.block(ZERO) { + format(receiver) + builder.token(expression.operationSign.value) + format(expression.selectorExpression) + } + } + expression.isChainedScopingFunction && + expression.chainRoot.isMultilineScopingFunction && + !chainedSelectorsHaveValueArguments(expression) -> { + formatChainedScopingFunction(expression, emitLeadingBreak = false) + } + expression.isChainedBlockLikeCall -> { + formatChainedBlockLikeCall(expression, emitLeadingBreak = false) + } + else -> { + emitQualifiedExpression(expression) + } + } + } + + /** + * 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) { + // 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)) + } + + var deferredCallArguments: DeferredCallArguments? = null + repeat(openingGroups) { builder.open(ZERO) } + when (ktExpression) { + is KtQualifiedExpression if ktExpression.selectorExpression is KtCallExpression -> { + builder.token(ktExpression.operationSign.value) + 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 }` + deferredCallArguments = + DeferredCallArguments( + selectorExpression, + Indentation.If(nameTag, expressionBreakIndent, argsIndentElse), + Indentation.If(nameTag, ZERO, lambdaIndentElse), + ) + } + 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() } + + deferredCallArguments?.let { (callee, argumentsIndent, lambdaIndent) -> + formatFunctionCall( + null, + callee.typeArgumentList, + callee.valueArgumentList, + callee.trailingLambda, + argumentsIndent = argumentsIndent, + lambdaIndent = lambdaIndent, + ) + } + } + } + } + + 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) { + emitQualifiedExpression(expression) + } else { + format(expression.arrayExpression) + 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 f08e179a..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 @@ -2,6 +2,7 @@ package org.jetbrains.ktfmt.format.visitor 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 +69,24 @@ interface FileFormatter : KotlinAstFormatter { } markForPartialFormat() } + + override fun formatStatement(statement: PsiElement) { + builder.block { 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/GroupInfo.kt b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/GroupInfo.kt new file mode 100644 index 00000000..cf8dd8a2 --- /dev/null +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/GroupInfo.kt @@ -0,0 +1,133 @@ +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 + +/** + * 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, + var closingGroups: Int = 0, + var isTrailingLambda: Boolean = false, + var isLast: Boolean = false, +) + +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) +} + +/** + * Generates the [GroupInfo] array to go with an array of [KtQualifiedExpression] parts + * + * 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]`) + * 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 + * 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: + * ``` + * 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. + * ``` + * + * 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.isTrailingLambda = hasTrailingLambda && it.expression.isLambda + it.isLast = true + } + + fun group(from: Int, to: Int) { + groupingInfos[from].openingGroups++ + groupingInfos[to].closingGroups++ + } + + var inPrefix = true + var lastAnchor = 0 + for ((index, part) in parts.withIndex()) { + when (part) { + is KtQualifiedExpression -> { + if (inPrefix && part.shouldGroupWithPrevious(index, parts.lastIndex, continuationIndent)) { + // all parts of the prefix are grouped together + group(0, index) + } else { + inPrefix = false + // future arrays and postfixes will be anchored to this part + lastAnchor = index + } + } + is KtArrayAccessExpression, + is KtPostfixExpression -> { + group(lastAnchor, index) + } + } + } + if (hasTrailingLambda) { + group(0, groupingInfos.lastIndex) + } + 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..f1755579 --- /dev/null +++ b/core/src/main/kotlin/org/jetbrains/ktfmt/format/visitor/Indentation.kt @@ -0,0 +1,74 @@ +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 + + 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) + + override operator fun unaryMinus(): Const = Const(-value) + + operator fun times(other: Int): Const = Const(value * other) + } + + class If + private constructor( + val condition: Output.BreakTag, + val thenIndent: Indentation, + val elseIndent: Indentation, + ) : Indentation() { + override val indent: Indent = Indent.If.make(condition, thenIndent.indent, elseIndent.indent) + + override operator fun unaryMinus(): If = If(condition, -thenIndent, -elseIndent) + + 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) + } +} 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..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 @@ -1,7 +1,6 @@ package org.jetbrains.ktfmt.format.visitor -import com.google.googlejavaformat.Indent -import com.google.googlejavaformat.Indent.Const.ZERO +import com.google.googlejavaformat.FormattingError import com.google.googlejavaformat.OpsBuilder import com.google.googlejavaformat.Output.BreakTag import java.util.ArrayDeque @@ -128,18 +127,24 @@ interface KotlinAstFormatter { val options: FormattingOptions val builder: OpsBuilder - 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 + 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 +197,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,17 +230,29 @@ interface KotlinAstFormatter { TODO("Unreachable code path") } - fun formatQualifiedExpression(expression: KtQualifiedExpression) { - TODO("Unreachable code path") - } + fun formatQualifiedExpression(expression: KtQualifiedExpression) fun formatCallExpression(callExpression: KtCallExpression) { 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 +519,15 @@ interface KotlinAstFormatter { /** See [isLambdaOrScopingFunction] for examples. */ fun formatLambdaOrScopingFunction(expr: PsiElement?, emitLeadingBreak: Boolean = true) + fun formatFunctionCall( + callee: KtExpression?, + typeArgumentList: KtTypeArgumentList?, + argumentList: KtValueArgumentList?, + trailingLambda: KtLambdaArgument?, + argumentsIndent: Indentation = expressionBreakIndent, + lambdaIndent: Indentation = Indentation.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 @@ -536,18 +565,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)) + } } 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..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 @@ -21,12 +21,15 @@ 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 +import org.jetbrains.kotlin.psi.KtCallElement 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 @@ -38,7 +41,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.KotlinInputAstVisitor +import org.jetbrains.ktfmt.format.ParseError /** Returns true if the expression represents an invocation that is also a lambda */ val KtExpression.isLambda: Boolean @@ -56,8 +59,8 @@ 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 - * 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? @@ -164,7 +167,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 +179,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 +214,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 +278,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 +295,24 @@ 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 + +/** + * 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]) + } 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