diff --git a/build.gradle.kts b/build.gradle.kts index 695df045e..b6e71c90e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -66,7 +66,6 @@ class KtfmtArgumentsProvider( @get:Input val check: Boolean, ) : CommandLineArgumentProvider { override fun asArguments(): Iterable = buildList { - add("--quiet") if (check) { add("--dry-run") add("--set-exit-if-changed") diff --git a/core/api/ktfmt.api b/core/api/ktfmt.api index 7056fddb4..cf7d81adf 100644 --- a/core/api/ktfmt.api +++ b/core/api/ktfmt.api @@ -192,7 +192,6 @@ public class com/facebook/ktfmt/format/KotlinInputAstVisitor : org/jetbrains/kot public fun visitAnnotationUseSiteTarget (Lorg/jetbrains/kotlin/psi/KtAnnotationUseSiteTarget;Ljava/lang/Void;)Ljava/lang/Void; public fun visitArgument (Lorg/jetbrains/kotlin/psi/KtValueArgument;)V public fun visitArrayAccessExpression (Lorg/jetbrains/kotlin/psi/KtArrayAccessExpression;)V - public final fun visitBackingField (Lorg/jetbrains/kotlin/psi/KtBackingField;)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 diff --git a/core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt b/core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt index 909e8de2d..c1ae3ab02 100644 --- a/core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt +++ b/core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt @@ -22,21 +22,18 @@ import com.facebook.ktfmt.util.ownValOrVarKeywordText import com.google.common.base.Throwables import com.google.common.collect.ImmutableList import com.google.googlejavaformat.Doc -import com.google.googlejavaformat.Doc.Level 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.OpsBuilder.BlankLineWanted import com.google.googlejavaformat.Output.BreakTag import java.util.ArrayDeque import java.util.Optional -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.contract 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.PsiWhiteSpace -import org.jetbrains.kotlin.com.intellij.psi.stubs.PsiFileStubImpl import org.jetbrains.kotlin.lexer.KtModifierKeywordToken import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.KtAnnotatedExpression @@ -76,6 +73,7 @@ import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFileAnnotationList import org.jetbrains.kotlin.psi.KtFinallySection import org.jetbrains.kotlin.psi.KtForExpression +import org.jetbrains.kotlin.psi.KtFunction import org.jetbrains.kotlin.psi.KtFunctionType import org.jetbrains.kotlin.psi.KtIfExpression import org.jetbrains.kotlin.psi.KtImportDirective @@ -89,6 +87,7 @@ import org.jetbrains.kotlin.psi.KtLambdaExpression import org.jetbrains.kotlin.psi.KtModifierList import org.jetbrains.kotlin.psi.KtNamedFunction import org.jetbrains.kotlin.psi.KtNullableType +import org.jetbrains.kotlin.psi.KtObjectLiteralExpression import org.jetbrains.kotlin.psi.KtPackageDirective import org.jetbrains.kotlin.psi.KtParameter import org.jetbrains.kotlin.psi.KtParameterList @@ -129,14 +128,12 @@ 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.KtWhenEntry 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.getPrevSiblingIgnoringWhitespace import org.jetbrains.kotlin.psi.psiUtil.startOffset import org.jetbrains.kotlin.psi.psiUtil.startsWithComment -import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes -import org.jetbrains.kotlin.psi.stubs.impl.KotlinPlaceHolderStubImpl /** An AST visitor that builds a stream of {@link Op}s to format. */ open class KotlinInputAstVisitor( @@ -145,6 +142,18 @@ open class KotlinInputAstVisitor( ) : KtTreeVisitorVoid() { internal open val forceAnnotationBreaks: Boolean = false + internal open val forceLineBreakAfterAssignment: Boolean = true + internal open val forceLineBreakAfterNamedParameter: Boolean = true + internal open val forceLineBreakAfterSupertypeColon: Boolean = true + internal open val forceLineBreakBeforeAccessors: Boolean = true + internal open val hugBlockLikeInfixCalls: Boolean = false + internal open val hugCallsWithTrailingLambda: Boolean = false + internal open val hugChainsAfterTrailingLambda: Boolean = false + internal open val hugConditionParens: Boolean = false + internal open val hugWhenExpressions: Boolean = false + internal open val indentBooleanConditions: Boolean = true + internal open val forceLineBreakInWhenConditionList: Boolean = true + internal open val forceLineBreaksBetweenEmptyMethods: Boolean = true /** Standard indentation for a block */ private val blockIndent: Indent.Const = Indent.Const.make(options.blockIndent, 1) @@ -190,27 +199,28 @@ open class KotlinInputAstVisitor( } } - /** Example `Int`, `(String)` or `() -> Int` */ - override fun visitTypeReference(typeReference: KtTypeReference) { - builder.sync(typeReference) - // Normally we'd visit the children nodes through accessors on 'typeReference', and we wouldn't - // loop over children. - // But, in this case the modifier list can either be inside the parenthesis: - // ... (@Composable (x) -> Unit) - // or outside of them: - // ... @Composable ((x) -> Unit) - val modifierList = typeReference.modifierList - val typeElement = typeReference.typeElement - for (child in typeReference.node.children()) { + /** Emits a type together with the parentheses around it, by walking [element]'s children. */ + private fun emitParenthesizedType( + element: KtElement, + modifierList: KtModifierList?, + innerType: PsiElement?, + ) { + for (child in element.node.children()) { when { child.psi == modifierList -> visit(modifierList) - child.psi == typeElement -> visit(typeElement) + child.psi == innerType -> visit(innerType) child.elementType == KtTokens.LPAR -> builder.token("(") child.elementType == KtTokens.RPAR -> builder.token(")") } } } + /** Example `Int`, `(String)` or `() -> Int` */ + override fun visitTypeReference(typeReference: KtTypeReference) { + builder.sync(typeReference) + emitParenthesizedType(typeReference, typeReference.modifierList, typeReference.typeElement) + } + override fun visitDynamicType(type: KtDynamicType) { builder.token("dynamic") } @@ -218,18 +228,7 @@ open class KotlinInputAstVisitor( /** Example: `String?` or `((Int) -> Unit)?` */ override fun visitNullableType(nullableType: KtNullableType) { builder.sync(nullableType) - - // Normally we wouldn't loop over children, but there can be multiple layers of parens. - val modifierList = nullableType.modifierList - val innerType = nullableType.innerType - for (child in nullableType.node.children()) { - when { - child.psi == modifierList -> visit(modifierList) - child.psi == innerType -> visit(innerType) - child.elementType == KtTokens.LPAR -> builder.token("(") - child.elementType == KtTokens.RPAR -> builder.token(")") - } - } + emitParenthesizedType(nullableType, nullableType.modifierList, nullableType.innerType) builder.token("?") } @@ -254,9 +253,7 @@ open class KotlinInputAstVisitor( // TODO(strulovich): Should this have the same indentation behaviour as `x && y`? visit(type.getLeftTypeRef()) - builder.space() - builder.token("&") - builder.space() + builder.spacedToken("&") visit(type.getRightTypeRef()) } @@ -277,13 +274,11 @@ open class KotlinInputAstVisitor( val typeReference = typeProjection.typeReference when (typeProjection.projectionKind) { KtProjectionKind.IN -> { - builder.token("in") - builder.space() + builder.tokenThenSpace("in") visit(typeReference) } KtProjectionKind.OUT -> { - builder.token("out") - builder.space() + builder.tokenThenSpace("out") visit(typeReference) } KtProjectionKind.STAR -> builder.token("*") @@ -342,7 +337,7 @@ open class KotlinInputAstVisitor( builder.block(ZERO) { if (receiverTypeReference != null) { visit(receiverTypeReference) - builder.breakOp(Doc.FillMode.INDEPENDENT, "", expressionBreakIndent) + builder.breakToFill(expressionBreakIndent) builder.token(".") } if (name != null) { @@ -355,8 +350,7 @@ open class KotlinInputAstVisitor( builder.token("(") builder.token(")") emitTypeOrDelegationCall { - builder.breakOp(Doc.FillMode.INDEPENDENT, " ", expressionBreakIndent) - builder.block(expressionBreakIndent) { visit(typeOrDelegationCall) } + builder.breakToFillThenBlock(" ", expressionBreakIndent) { visit(typeOrDelegationCall) } } } } else { @@ -378,9 +372,7 @@ open class KotlinInputAstVisitor( } } - if (typeConstraintList != null) { - visit(typeConstraintList) - } + visit(typeConstraintList) if (bodyExpression is KtBlockExpression) { builder.space() visit(bodyExpression) @@ -388,34 +380,24 @@ open class KotlinInputAstVisitor( builder.space() builder.block(ZERO) { builder.token("=") - if (isLambdaOrScopingFunction(bodyExpression)) { - visitLambdaOrScopingFunction(bodyExpression) - } else if (isChainedScopingFunction(bodyExpression)) { - visitChainedScopingFunction(bodyExpression, emitLeadingBreak = true) - } else if (isBlockLikeCall(bodyExpression)) { - builder.space() - visit(bodyExpression) - } else if (isChainedBlockLikeCall(bodyExpression)) { - visitChainedBlockLikeCall(bodyExpression, emitLeadingBreak = true) - } else { + if ( + !emitExpressionAfterOperator(bodyExpression, indentHuggedBinaryExpression = true) && + !emitChainAfterOperator(bodyExpression) + ) { builder.block(expressionBreakIndent) { - builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO) + builder.breakToFill(" ") builder.block(ZERO) { visit(bodyExpression) } } } } } - builder.guessToken(";") + builder.guessSemicolon() } if (forceTrailingBreak) { builder.forcedBreak() } } - private fun genSym(): BreakTag { - return BreakTag() - } - private fun emitBracedBlock( bodyBlockExpression: PsiElement, emitChildren: (Array) -> Unit, @@ -425,29 +407,27 @@ open class KotlinInputAstVisitor( if (statements.isNotEmpty()) { builder.block(blockIndent) { builder.forcedBreak() - builder.blankLineWanted(OpsBuilder.BlankLineWanted.PRESERVE) + builder.blankLineWanted(BlankLineWanted.PRESERVE) emitChildren(statements) } builder.forcedBreak() - builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO) + builder.blankLineWanted(BlankLineWanted.NO) } builder.token("}", blockIndent) } private fun visitStatement(statement: PsiElement) { builder.block(ZERO) { visit(statement) } - builder.guessToken(";") + builder.guessSemicolon() } private fun visitStatements(statements: Array) { - var first = true - builder.guessToken(";") - for (statement in statements) { + builder.guessSemicolon() + for ((index, statement) in statements.withIndex()) { builder.forcedBreak() - if (!first) { - builder.blankLineWanted(OpsBuilder.BlankLineWanted.PRESERVE) + if (index > 0) { + builder.blankLineWanted(BlankLineWanted.PRESERVE) } - first = false markForPartialFormat() visitStatement(statement) markForPartialFormat() @@ -457,8 +437,8 @@ open class KotlinInputAstVisitor( override fun visitProperty(property: KtProperty) { builder.sync(property) builder.block(ZERO) { - declareOne( - kind = DeclarationKind.FIELD, + emitVariableLikeDeclaration( + isField = true, modifiers = property.modifierList, valOrVarKeyword = property.valOrVarKeyword.text, typeParameters = property.typeParameterList, @@ -472,16 +452,12 @@ open class KotlinInputAstVisitor( backingField = property.fieldDeclaration, ) } - builder.guessToken(";") + builder.guessSemicolon() if (property.parent !is KtWhenExpression) { builder.forcedBreak() } } - fun visitBackingField(backingField: KtBackingField) { - emitBackingField(backingField) - } - /** * Example: "com.facebook.bla.bla" in imports or "a.b.c.d" in expressions. * @@ -504,7 +480,7 @@ open class KotlinInputAstVisitor( receiver is KtStringTemplateExpression -> { builder.block(expressionBreakIndent) { visit(receiver) - builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO) + builder.breakOp() builder.token(expression.operationSign.value) visit(expression.selectorExpression) } @@ -516,14 +492,16 @@ open class KotlinInputAstVisitor( visit(expression.selectorExpression) } } - isChainedScopingFunction(expression) && - isMultilineScopingFunction(chainRoot(expression)) && - chainedSelectorsHaveNoValueArguments(expression) -> { + expression.isChainedScopingFunction && + expression.chainRoot.isMultilineScopingFunction && + !chainedSelectorsHaveValueArguments(expression) -> { visitChainedScopingFunction(expression, emitLeadingBreak = false) } - isChainedBlockLikeCall(expression) -> { + expression.isChainedBlockLikeCall -> { visitChainedBlockLikeCall(expression, emitLeadingBreak = false) } + hugChainsAfterTrailingLambda && + visitChainAfterTrailingLambda(expression, emitLeadingBreak = false) -> Unit else -> { emitQualifiedExpression(expression) } @@ -536,6 +514,28 @@ open class KotlinInputAstVisitor( var shouldCloseGroup = false } + /** + * A chain of qualified expressions, decomposed into everything needed to lay it out. + * + * @param useBlockLikeLambdaStyle whether we want to make a lambda look like a block, this makes + * Kotlin DSLs look as expected + */ + private data class ChainLayout( + val parts: List, + val useBlockLikeLambdaStyle: Boolean, + val groupingInfos: List, + ) + + private fun chainLayout(expression: KtExpression): ChainLayout { + val parts = breakIntoParts(expression) + val useBlockLikeLambdaStyle = parts.last().isLambda() && parts.count { it.isLambda() } == 1 + return ChainLayout( + parts, + useBlockLikeLambdaStyle, + computeGroupingInfo(parts, useBlockLikeLambdaStyle), + ) + } + /** * Handles a chain of qualified expressions, i.e. `a[5].b!!.c()[4].f()` * @@ -547,104 +547,257 @@ open class KotlinInputAstVisitor( * part, emitting it to the [builder] while closing and opening groups. */ private fun emitQualifiedExpression(expression: KtExpression) { - val parts = breakIntoParts(expression) - // whether we want to make a lambda look like a block, this make Kotlin DSLs look as expected - val useBlockLikeLambdaStyle = parts.last().isLambda() && parts.count { it.isLambda() } == 1 - val groupingInfos = computeGroupingInfo(parts, useBlockLikeLambdaStyle) + val chain = chainLayout(expression) 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 && isBlockLikeCall(selectorExpression) - 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), + emitQualifiedExpressionParts(chain, range = chain.parts.indices, nameTag = BreakTag()) + } + } + + /** + * Lays out expression that follows an operator such as `=` in two parts, deciding whether to + * break after an operator, of after the head of the expression. + * + * @param [emitHead] emits a head of the operator, i.e. something that can fit into the same line + * as the operator; e.g. `when (a) {`, `if (cond) {`, etc. + * @param emitTail emits the rest, given the indents to lay it out at. A tail that aligns with the + * head -- the body of a `when` -- uses `headIndent`; one that continues the head's line -- an + * argument list -- uses `tailIndent`, or `headIndent` plus a level of its own. + */ + private fun emitSplitAfterOperator( + emitHead: () -> Unit, + emitTail: (headIndent: Indent, tailIndent: Indent) -> Unit, + ) { + val brokeAfterOperator = BreakTag() + val headIndent = Indent.If.make(brokeAfterOperator, expressionBreakIndent, ZERO) + val tailIndent = + Indent.If.make(brokeAfterOperator, doubleExpressionBreakIndent, expressionBreakIndent) + builder.block(expressionBreakIndent) { + builder.breakOp(" ", ZERO, Optional.of(brokeAfterOperator)) + builder.block(headIndent) { emitHead() } + } + emitTail(headIndent, tailIndent) + } + + /** + * Lays out a chain of qualified expressions that follows an operator such as `=`, deciding + * whether to break after that operator from the width of the chain's receiver alone. + */ + private fun emitQualifiedExpressionAfterOperator(expression: KtExpression): Boolean { + val chain = chainLayout(expression) + val receiverEnd = receiverSegmentEnd(chain) ?: return false + val lastIndex = chain.parts.lastIndex + val nameTag = BreakTag() // allows adjusting arguments indentation if a break will be made + + emitSplitAfterOperator( + emitHead = { + emitQualifiedExpressionParts(chain, range = 0..receiverEnd, nameTag = nameTag) + }, + emitTail = { _, selectorsIndent -> + if (receiverEnd < lastIndex) { + builder.block(selectorsIndent) { + emitQualifiedExpressionParts( + chain, + range = receiverEnd + 1..lastIndex, + nameTag = nameTag, ) } } - is KtArrayAccessExpression -> { - visitArrayAccessBrackets(ktExpression) - builder.close() - } - is KtPostfixExpression -> { - builder.token(ktExpression.operationReference.text) - builder.close() - } - else -> { - check(index == 0) - visit(ktExpression) + }, + ) + return true + } + + /** + * Lays out a call that follows an operator such as the `=` of a named argument, deciding whether + * to break after that operator from the width of the callee alone. + */ + private fun emitCallAfterOperator(expression: KtExpression?): Boolean { + if (expression !is KtCallExpression) return false + // A leading comment brings its own forced break, which throws off the indents below. + if (expression.hasLeadingComment) return false + return emitCallAfterOperator( + expression.calleeExpression, + expression.typeArgumentList, + expression.valueArgumentList, + expression.lambdaArguments, + ) + } + + /** + * The parts-wise version of [emitCallAfterOperator], for callers that aren't a [KtExpression]. + */ + private fun emitCallAfterOperator( + callee: KtExpression?, + typeArgumentList: KtTypeArgumentList?, + argumentList: KtValueArgumentList?, + lambdaArguments: List, + ): Boolean { + if (!canEmitCallAfterOperator(callee, argumentList, lambdaArguments)) return false + callee as KtExpression + argumentList as KtValueArgumentList + + emitSplitAfterOperator( + emitHead = { visit(callee) }, + emitTail = { _, argumentsIndent -> + builder.block(argumentsIndent) { + builder.block(ZERO) { visit(typeArgumentList) } + visitValueArgumentListInternal(argumentList) } + }, + ) + return true + } + + /** + * Whether [emitCallAfterOperator] can lay out these call parts, so that callers can tell before + * they [OpsBuilder.sync] past anything. + */ + private fun canEmitCallAfterOperator( + callee: KtExpression?, + argumentList: KtValueArgumentList?, + lambdaArguments: List, + ): Boolean { + // A trailing lambda is laid out by visitCallElement, which indents the callee along with it. + if (lambdaArguments.isNotEmpty()) return false + if (callee == null) return false + // Without arguments there is nothing to break at, so keeping the callee here buys nothing. + return argumentList != null && !argumentList.hasEmptyParens() + } + + /** + * Lays out a supertype list whose first entry is a constructor call on the class header line, so + * that only the call's arguments break and any remaining supertypes trail its closing paren. + */ + private fun emitSuperTypeCallAfterColon(list: KtSuperTypeList): Boolean { + if (forceLineBreakAfterSupertypeColon) return false + val entries = list.entries + val call = entries.firstOrNull() as? KtSuperTypeCallEntry ?: return false + // A leading comment brings its own forced break, which throws off the indents below. + if (call.hasLeadingComment) return false + if ( + !canEmitCallAfterOperator( + call.calleeExpression, + call.valueArgumentList, + call.lambdaArguments, + ) + ) { + return false + } + + builder.sync(list) + builder.sync(call) + // The type arguments are part of the constructor callee, as in visitSuperTypeCallEntry. + emitCallAfterOperator(call.calleeExpression, null, call.valueArgumentList, call.lambdaArguments) + + // The remaining supertypes trail the call's closing paren, sharing its line when they fit and + // otherwise taking one line each. + if (entries.size > 1) { + builder.block(expressionBreakIndent) { + for (entry in entries.drop(1)) { + builder.token(",") + builder.breakOp(" ") + visit(entry) } } } + return true } /** - * Decomposes a qualified expression into parts, so `rainbow.red.orange.yellow` becomes `[rainbow, - * rainbow.red, rainbow.red.orange, rainbow.orange.yellow]` + * The index of the last part of the chain's receiver -- the head that is kept together, such as + * `a.b[2]` in `a.b[2].c.d()` -- or null when every group opened in the chain is still open by the + * time the last part is emitted, so there is no point at which the chain can be split in two. */ - private fun breakIntoParts(expression: KtExpression): List { - val parts = ArrayDeque() - - // use an ArrayDeque and add elements to the beginning so the innermost expression comes first - // foo.bar.yay -> [yay, bar.yay, foo.bar.yay] - - var node: KtExpression? = expression - while (node != null) { - parts.addFirst(node) - node = - when (node) { - is KtQualifiedExpression -> node.receiverExpression - is KtArrayAccessExpression -> node.arrayExpression - is KtPostfixExpression -> node.baseExpression - else -> null - } + private fun receiverSegmentEnd(chain: ChainLayout): Int? { + val (parts, useBlockLikeLambdaStyle, groupingInfos) = chain + // The group of a block-like trailing lambda is opened at the root and closed at the very last + // part, so it always spans the whole chain. + if (useBlockLikeLambdaStyle) return null + + var openGroups = 0 + for ((index, part) in parts.withIndex()) { + openGroups += groupingInfos[index].groupOpenCount + if (groupingInfos[index].shouldCloseGroup) openGroups-- + if (part is KtArrayAccessExpression || part is KtPostfixExpression) openGroups-- + if (openGroups == 0) return index } + return null + } - return parts.toList() + /** Emits [range] of the parts of a chain, see [emitQualifiedExpression]. */ + private fun emitQualifiedExpressionParts( + chain: ChainLayout, + range: IntRange, + nameTag: BreakTag, + ) { + val (parts, useBlockLikeLambdaStyle, groupingInfos) = chain + for (index in range) { + val ktExpression = parts[index] + if (ktExpression is KtQualifiedExpression) { + builder.breakOp("", 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) + } + } + } } /** @@ -861,7 +1014,8 @@ open class KotlinInputAstVisitor( if (isSingleUnnamedLambda) { wrapInBlock = true breakBeforePostfix = false - leadingBreak = !hasEmptyParens && hasTrailingComma + // The lambda itself sits between the parens, so they are never empty here. + leadingBreak = hasTrailingComma breakAfterPrefix = false } else { // A call without a trailing comma that is nonetheless forced onto multiple lines (because one @@ -872,8 +1026,8 @@ open class KotlinInputAstVisitor( arguments.any { argument -> val argumentExpression = argument.getArgumentExpression() argumentExpression != null && - (isBlockLikeCall(argumentExpression) || - isChainedBlockLikeCall(argumentExpression)) + (argumentExpression.isBlockLikeCall || + argumentExpression.isChainedBlockLikeCall) } wrapInBlock = !options.manageTrailingCommas breakBeforePostfix = @@ -959,23 +1113,22 @@ open class KotlinInputAstVisitor( builder.token(",") builder.forcedBreak() } else if (hasParams) { - builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO) + builder.breakToFill(" ") } builder.token("->") } } if (hasParams || hasArrow || hasStatements || hasComments) { - builder.breakOp(Doc.FillMode.UNIFIED, " ", bracePlusZeroIndent) + builder.breakOp(" ", bracePlusZeroIndent) } if (hasStatements) { - builder.breakOp(Doc.FillMode.UNIFIED, "", bracePlusBlockIndent) - builder.block(bracePlusBlockIndent) { - builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO) + builder.breakOpThenBlock(bracePlusBlockIndent) { + builder.blankLineWanted(BlankLineWanted.NO) val shouldForceMultiline = - options.preserveLambdaBreaks && hasSourceNewlineInLambdaBody(lambdaExpression) + options.preserveLambdaBreaks && lambdaExpression.hasSourceNewlineInLambdaBody if ( !shouldForceMultiline && @@ -987,32 +1140,27 @@ open class KotlinInputAstVisitor( } else { visitStatements(expressionStatements) } - builder.breakOp(Doc.FillMode.UNIFIED, " ", bracePlusZeroIndent) + builder.breakOp(" ", 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.breakOpThenBlock(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.blankLineWanted(BlankLineWanted.NO) + for ((i, comment) in blockComments.withIndex()) { + if (i > 0) { + builder.forcedBreak() } + builder.token(comment.text) } - builder.breakOp(Doc.FillMode.UNIFIED, " ", bracePlusZeroIndent) + builder.breakOp(" ", 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.breakOp(bracePlusZeroIndent) } builder.block(bracePlusZeroIndent) { builder.fenceComments() @@ -1090,9 +1238,14 @@ open class KotlinInputAstVisitor( * ``` * * @param wrapInBlock if true, place all the elements in a block. When there's no [leadingBreak], - * this will be negatively indented. Note that the [prefix] and [postfix] aren't included in the - * block. + * this will be negatively indented unless [compensateMissingLeadingBreak] says otherwise. Note + * that the [prefix] and [postfix] aren't included in the block. * @param leadingBreak if true, break before the first element. + * @param compensateMissingLeadingBreak if true, and there is no [leadingBreak], pull the block + * back by one level. Callers that emit a [prefix] are indented on the assumption that the + * leading break fires, so without it the elements have to be pulled back to meet the prefix. + * Callers that instead place the first element with a break of their own -- such as the `:` of + * a supertype list -- are already at the right level and pass false. * @param prefix if provided, emit this before the first element. * @param postfix if provided, emit this after the last element (or trailing comma). * @param breakAfterPrefix if true, emit a break after [prefix], but before the start of the @@ -1135,18 +1288,19 @@ open class KotlinInputAstVisitor( hasTrailingComma: Boolean = false, wrapInBlock: Boolean = true, leadingBreak: Boolean = true, + compensateMissingLeadingBreak: Boolean = true, prefix: String? = null, postfix: String? = null, breakAfterPrefix: Boolean = true, breakBeforePostfix: Boolean = options.manageTrailingCommas, ): BreakTag? { val breakAfterLastElement = hasTrailingComma || (postfix != null && breakBeforePostfix) - val nameTag = if (breakAfterLastElement) null else genSym() + val nameTag = if (breakAfterLastElement) null else BreakTag() if (prefix != null) { builder.token(prefix) if (breakAfterPrefix) { - builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO, Optional.ofNullable(nameTag)) + builder.breakOp("", ZERO, Optional.ofNullable(nameTag)) } } @@ -1156,16 +1310,15 @@ open class KotlinInputAstVisitor( builder.breakOp(breakType, " ", ZERO) } - val indent = if (leadingBreak) ZERO else expressionBreakNegativeIndent + val indent = + if (leadingBreak || !compensateMissingLeadingBreak) ZERO else expressionBreakNegativeIndent builder.block(indent, isEnabled = wrapInBlock) { if (leadingBreak) { builder.breakOp(breakType, "", ZERO) } - var first = true - for (value in list) { - if (!first) emitComma() - first = false + for ((index, value) in list.withIndex()) { + if (index > 0) emitComma() visit(value) } @@ -1218,16 +1371,20 @@ open class KotlinInputAstVisitor( val isLambda = argument.getArgumentExpression() is KtLambdaExpression if (hasArgName) { visit(argument.getArgumentName()) - builder.space() - builder.token("=") + builder.spaceThenToken("=") if (isLambda) { builder.space() } } + if (hasArgName && !isLambda && !argument.isSpread && !forceLineBreakAfterNamedParameter) { + if (emitCallAfterOperator(argument.getArgumentExpression())) return + if (emitExpressionAfterOperator(argument.getArgumentExpression()!!)) return + } + val indent = if (hasArgName && !isLambda) expressionBreakIndent else ZERO builder.block(indent, isEnabled = wrapInBlock) { if (hasArgName && !isLambda) { - builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO) + builder.breakToFill(" ") } if (argument.isSpread) { builder.token("*") @@ -1257,7 +1414,7 @@ open class KotlinInputAstVisitor( builder.space() visit(returnedExpression) } - builder.guessToken(";") + builder.guessSemicolon() } /** @@ -1271,12 +1428,18 @@ open class KotlinInputAstVisitor( builder.sync(expression) val op = expression.operationToken - if (KtTokens.ALL_ASSIGNMENTS.contains(op) && isLambdaOrScopingFunction(expression.right)) { + val right = expression.right + if ( + KtTokens.ALL_ASSIGNMENTS.contains(op) && + right != null && + (!forceLineBreakAfterAssignment || right.isLambdaOrScopingFunction) + ) { // Assignments are statements in Kotlin; we don't have to worry about compound assignment. visit(expression.left) - builder.space() - builder.token(expression.operationReference.text) - visitLambdaOrScopingFunction(expression.right) + builder.spaceThenToken(expression.operationReference.text) + // The level keeps the break after the operator out of reach of any forced break the + // left-hand side brought with it -- an annotation before the statement, say. + builder.block(ZERO) { emitAssignedExpression(right) } return } @@ -1306,18 +1469,17 @@ open class KotlinInputAstVisitor( if (isFirst) { builder.open(expressionBreakIndent) } - builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO) - builder.token(leftExpression.operationReference.text) - builder.space() + builder.breakOp(" ") + builder.tokenThenSpace(leftExpression.operationReference.text) } else -> { builder.space() if (isFirst) { - builder.open(expressionBreakIndent) + builder.open(if (indentBooleanConditions) expressionBreakIndent else ZERO) } builder.token(leftExpression.operationReference.text) val fillMode = - if (hasLineBreakingCommentBefore(leftExpression.operationReference)) + if (leftExpression.operationReference.hasLineBreakingCommentBefore) Doc.FillMode.INDEPENDENT else Doc.FillMode.UNIFIED builder.breakOp(fillMode, " ", ZERO) @@ -1328,28 +1490,6 @@ open class KotlinInputAstVisitor( builder.close() } - /** - * Checks if a line-breaking comment precedes [element] in the PSI tree. - * - * Line comments (`//`) always force a break. Block comments (`/* */`) only count if they are on - * their own line (preceded by whitespace with a newline). Inline block comments like `x /*tag*/ - * ||` do not force a break and should not trigger INDEPENDENT fill mode. - */ - private fun hasLineBreakingCommentBefore(element: PsiElement): Boolean { - var prev = element.prevSibling - while (prev is PsiWhiteSpace) { - prev = prev.prevSibling - } - if (prev !is PsiComment) return false - - // Line comments always force a line break - if (prev.text.startsWith("//")) return true - - // Block comments force a break only if on their own line - val beforeComment = prev.prevSibling - return beforeComment is PsiWhiteSpace && beforeComment.text.contains('\n') - } - override fun visitPostfixExpression(expression: KtPostfixExpression) { builder.sync(expression) builder.block(ZERO) { @@ -1393,11 +1533,6 @@ open class KotlinInputAstVisitor( visit(expression.baseExpression) } - internal enum class DeclarationKind { - FIELD, - PARAMETER, - } - /** * Declare one variable or variable-like thing. * @@ -1406,8 +1541,8 @@ open class KotlinInputAstVisitor( * - `a: Int` * - `private val b: */ - private fun declareOne( - kind: DeclarationKind, + private fun emitVariableLikeDeclaration( + isField: Boolean, modifiers: KtModifierList?, valOrVarKeyword: String?, typeParameters: KtTypeParameterList? = null, @@ -1419,21 +1554,17 @@ open class KotlinInputAstVisitor( delegate: KtPropertyDelegate? = null, accessors: List? = null, backingField: KtBackingField? = null, - ): Int { - val verticalAnnotationBreak = genSym() - - val isField = kind == DeclarationKind.FIELD - + ) { + val verticalAnnotationBreak = BreakTag() if (isField) { - builder.blankLineWanted(OpsBuilder.BlankLineWanted.conditional(verticalAnnotationBreak)) + builder.blankLineWanted(BlankLineWanted.conditional(verticalAnnotationBreak)) } visit(modifiers) builder.block(ZERO) { builder.block(ZERO) { if (valOrVarKeyword != null) { - builder.token(valOrVarKeyword) - builder.space() + builder.tokenThenSpace(valOrVarKeyword) } if (typeParameters != null) { @@ -1457,7 +1588,7 @@ open class KotlinInputAstVisitor( if (type != null) { if (name != null) { builder.token(":") - builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO) + builder.breakOp(" ") } visit(type) } @@ -1471,63 +1602,39 @@ open class KotlinInputAstVisitor( // for example `by lazy { compute() }` if (delegate != null) { - builder.space() - builder.token("by") + builder.spaceThenToken("by") val delegateExpr = delegate.expression - if (isLambdaOrScopingFunction(delegateExpr)) { - builder.space() - visit(delegate) - } else if (delegateExpr != null && isChainedScopingFunction(delegateExpr)) { - visitChainedScopingFunction(delegateExpr, emitLeadingBreak = true) - } else if (isBlockLikeCall(delegateExpr)) { - builder.space() - visit(delegate) - } else if (delegateExpr != null && isChainedBlockLikeCall(delegateExpr)) { - visitChainedBlockLikeCall(delegateExpr, emitLeadingBreak = true) - } else { - builder.breakOp(Doc.FillMode.UNIFIED, " ", expressionBreakIndent) - builder.block(expressionBreakIndent) { + val laidOut = + delegateExpr != null && + emitExpressionAfterOperator( + delegateExpr, + scopingFunctionHugs = true, + // The delegate node carries the expression; visiting it keeps the `by` intact. + emitHugged = { + builder.space() + visit(delegate) + }, + ) + if (!laidOut) { + builder.breakOpThenBlock(" ", expressionBreakIndent) { builder.fenceComments() visit(delegate) } } } else if (initializer != null) { - builder.space() - builder.token("=") - if (isLambdaOrScopingFunction(initializer)) { - visitLambdaOrScopingFunction(initializer) - } else if (isChainedScopingFunction(initializer)) { - visitChainedScopingFunction(initializer, emitLeadingBreak = true) - } else if (isBlockLikeCall(initializer)) { - builder.space() - visit(initializer) - } else if (isChainedBlockLikeCall(initializer)) { - visitChainedBlockLikeCall(initializer, emitLeadingBreak = true) - } else { - builder.breakOp(Doc.FillMode.UNIFIED, " ", expressionBreakIndent) - builder.block(expressionBreakIndent) { - builder.fenceComments() - visit(initializer) - } - } + emitInitializer(initializer) } } // for example `field = value`, `private set`, or `get = 2 * field` - val propertyComponents = buildList { - if (backingField != null) { - add(backingField) - } - if (accessors != null) { - addAll(accessors) - } - } - .sortedBy { it.startOffset } + val propertyComponents = + (listOfNotNull(backingField) + accessors.orEmpty()).sortedBy { it.startOffset } if (propertyComponents.isNotEmpty()) { builder.block(blockIndent) { for (component in propertyComponents) { - builder.forcedBreak() + if (forceBreakBeforePropertyComponent(component)) builder.forcedBreak() + else builder.breakOp(" ") // The semicolon must come after the newline, or the output code will not parse. - builder.guessToken(";") + builder.guessSemicolon() when (component) { is KtPropertyAccessor -> { @@ -1539,7 +1646,7 @@ open class KotlinInputAstVisitor( typeParameters = null, receiverTypeReference = null, name = null, - parameterList = getParameterListWithBugFixes(component), + parameterList = component.parameterList, typeConstraintList = null, bodyExpression = component.bodyBlockExpression ?: component.bodyExpression, typeOrDelegationCall = component.returnTypeReference, @@ -1553,13 +1660,120 @@ open class KotlinInputAstVisitor( } } - builder.guessToken(";") + builder.guessSemicolon() if (isField) { - builder.blankLineWanted(OpsBuilder.BlankLineWanted.conditional(verticalAnnotationBreak)) + builder.blankLineWanted(BlankLineWanted.conditional(verticalAnnotationBreak)) + } + } + + private fun forceBreakBeforePropertyComponent(component: KtExpression): Boolean = + when (component) { + is KtBackingField -> true + is KtPropertyAccessor -> { + forceLineBreakBeforeAccessors || component.modifierList != null + } + else -> true + } + + /** + * Lays out the right-hand side of an operator that a declaration is assigned across -- the `=` of + * an initializer or an expression body, or the `by` of a property delegate -- according to the + * kind of expression it is. + * + * Lambdas, scoping functions and block-like calls keep the operator's line instead of breaking + * after it, and in styles that hug them, so do `when` expressions, block-like infix calls and + * calls that carry a trailing lambda alongside their value arguments. + * + * Returns false, having emitted nothing, when [expression] is not one of those shapes. Callers + * then emit their own default layout, which differs between them. + * + * @param scopingFunctionHugs whether a lambda or scoping function is emitted by [emitHugged] + * instead of by [visitLambdaOrScopingFunction]. The `by` of a delegate is already followed by + * its expression on the same line, so its lambda hugs it rather than breaking to it. + * @param indentHuggedBinaryExpression whether the operands of a hugged binary expression are laid + * out one level in from the line the operator is on. The `=` of a declaration or of an + * assignment indents them; an operator that is itself already indented -- the `=` of a named + * argument, say -- keeps them at its own level. + * @param emitHugged emits [expression] on the operator's line, after a plain space. Callers whose + * expression is wrapped in another PSI node -- the `by` of a delegate -- visit that node here. + */ + private fun emitExpressionAfterOperator( + expression: KtExpression, + scopingFunctionHugs: Boolean = false, + indentHuggedBinaryExpression: Boolean = false, + emitHugged: () -> Unit = { + builder.space() + visit(expression) + }, + ): Boolean { + when { + expression.isLambdaOrScopingFunction -> + if (scopingFunctionHugs) emitHugged() else visitLambdaOrScopingFunction(expression) + expression.isChainedScopingFunction -> + visitChainedScopingFunction(expression, emitLeadingBreak = true) + expression.isBlockLikeCall -> emitHugged() + expression.isChainedBlockLikeCall -> + visitChainedBlockLikeCall(expression, emitLeadingBreak = true) + !forceLineBreakAfterAssignment && expression is KtObjectLiteralExpression -> emitHugged() + !forceLineBreakAfterAssignment && expression is KtTryExpression -> emitHugged() + hugCallsWithTrailingLambda && expression.isCallWithTrailingLambda -> emitHugged() + hugChainsAfterTrailingLambda && + !expression.hasLeadingComment && + visitChainAfterTrailingLambda(expression, emitLeadingBreak = true) -> Unit + hugBlockLikeInfixCalls && expression.isInfixBlockLikeCall -> + emitInfixBlockLikeCall(expression) + !forceLineBreakAfterAssignment && expression is KtBinaryExpression -> + if (indentHuggedBinaryExpression) { + builder.block(expressionBreakIndent) { emitHugged() } + } else { + emitHugged() + } + hugWhenExpressions && expression is KtWhenExpression && !expression.hasLeadingComment -> + emitWhenExpressionAfterOperator(expression) + else -> return false } + return true + } + + /** + * Lays out [expression] as a chain that keeps the receiver on the line of the operator it follows + * -- see [emitQualifiedExpressionAfterOperator] -- in the styles that allow it. + * + * Returns false, having emitted nothing, when [expression] is not a chain, when the style breaks + * after the operator unconditionally, or when the chain can't be split that way. Callers then + * emit their own default layout. + */ + private fun emitChainAfterOperator(expression: KtExpression): Boolean = + !forceLineBreakAfterAssignment && + expression.isPlainQualifiedChain && + !expression.hasLeadingComment && + emitQualifiedExpressionAfterOperator(expression) - return 0 + /** + * Emits `= `, laying the initializer out according to the kind of expression it is. + */ + private fun emitInitializer(initializer: KtExpression) { + builder.spaceThenToken("=") + emitAssignedExpression(initializer) + } + + /** + * Lays out the right-hand side of an assignment operator -- the `=` of an initializer or of an + * assignment statement -- according to the kind of expression it is. + */ + private fun emitAssignedExpression(expression: KtExpression) { + if (emitExpressionAfterOperator(expression, indentHuggedBinaryExpression = true)) { + return + } + // A chain gets to keep its receiver on the `=` line when it fits there; everything else + // breaks after the `=` and is laid out one level in. + if (!emitChainAfterOperator(expression)) { + builder.breakOpThenBlock(" ", expressionBreakIndent) { + builder.fenceComments() + visit(expression) + } + } } private fun emitBackingField(backingField: KtBackingField) { @@ -1571,170 +1785,80 @@ open class KotlinInputAstVisitor( if (type != null) { builder.block(expressionBreakIndent) { builder.token(":") - builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO) + builder.breakOp(" ") visit(type) } } val initializer = backingField.initializer if (initializer != null) { - builder.space() - builder.token("=") - if (isLambdaOrScopingFunction(initializer)) { - visitLambdaOrScopingFunction(initializer) - } else if (isChainedScopingFunction(initializer)) { - visitChainedScopingFunction(initializer, emitLeadingBreak = true) - } else if (isBlockLikeCall(initializer)) { - builder.space() - visit(initializer) - } else if (isChainedBlockLikeCall(initializer)) { - visitChainedBlockLikeCall(initializer, emitLeadingBreak = true) - } else { - builder.breakOp(Doc.FillMode.UNIFIED, " ", expressionBreakIndent) - builder.block(expressionBreakIndent) { - builder.fenceComments() - visit(initializer) - } - } - } - } - } - - // Bug in Kotlin 1.9.10: KtPropertyAccessor is the direct parent of the left and right paren - // elements. Also parameterList is always null for getters. As a workaround, we create our own - // fake KtParameterList. - // TODO: won't need this after https://youtrack.jetbrains.com/issue/KT-70922 - private fun getParameterListWithBugFixes(accessor: KtPropertyAccessor): KtParameterList? { - if (accessor.bodyExpression == null && accessor.bodyBlockExpression == null) return null - - val stub = accessor.stub ?: PsiFileStubImpl(accessor.containingFile) - - return object : - KtParameterList(KotlinPlaceHolderStubImpl(stub, KtStubElementTypes.VALUE_PARAMETER_LIST)) { - override fun getParameters(): List { - return accessor.valueParameters - } - - override fun getTrailingComma(): PsiElement? { - return accessor.parameterList?.trailingComma - } - - override fun getLeftParenthesis(): PsiElement? { - return accessor.parameterList?.leftParenthesis - } - - override fun getRightParenthesis(): PsiElement? { - return accessor.parameterList?.rightParenthesis + emitInitializer(initializer) } } } /** - * Returns whether an expression is a lambda or initializer expression in which case we will want - * to avoid indenting the lambda block - * - * Examples: - * 1. '... = { ... }' is a lambda expression - * 2. '... = Runnable { ... }' is considered a scoping function - * 3. '... = scope { ... }' '... = apply { ... }' is a scoping function - * 4. '... = scope.launch { ... }' is a dot-qualified scoping function - * - * but not: - * 1. '... = foo() { ... }' due to the empty parenthesis - * 2. '... = Runnable @Annotation { ... }' due to the annotation + * Emit an `a to Foo(\n ...,\n)` style infix call that follows an operator such as `=`, deciding + * whether to break after that operator from the width of the head alone. */ - private fun isLambdaOrScopingFunction(expression: KtExpression?): Boolean { - if (expression == null) return false - val prev = expression.getPrevSiblingIgnoringWhitespace() - if (prev is PsiComment && prev.text.startsWith("//")) { - return false // Leading line comments cause weird indentation; block comments are ok. - } - - var carry = expression - if (carry is KtQualifiedExpression && carry.receiverExpression is KtSimpleNameExpression) { - carry = carry.selectorExpression - } - if (carry is KtCallExpression) { - if ( - carry.valueArgumentList?.leftParenthesis == null && - carry.lambdaArguments.isNotEmpty() && - carry.typeArgumentList?.arguments.isNullOrEmpty() - ) { - carry = carry.lambdaArguments[0].getArgumentExpression() - } else { - return false - } - } - if (carry is KtLabeledExpression) { - carry = carry.baseExpression - } - if (carry is KtLambdaExpression) { - return true - } + private fun emitInfixBlockLikeCall(expression: KtBinaryExpression) { + builder.sync(expression) + val right = checkNotNull(expression.right) + val call = checkNotNull(right.callExpression) - return false + emitSplitAfterOperator( + emitHead = { + builder.fenceComments() + visit(expression.left) + builder.spaceThenToken(expression.operationReference.text) + builder.space() + // The call may be the selector of a qualifier, as in `a to Organizations.Override(...)`. + if (right is KtQualifiedExpression) { + visit(right.receiverExpression) + builder.token(right.operationSign.value) + } + builder.sync(call) + visit(call.calleeExpression) + builder.block(ZERO) { visit(call.typeArgumentList) } + }, + // The extra level carries the head's indent over to the arguments. + emitTail = { callIndent, _ -> + builder.block(callIndent) { + builder.block(expressionBreakIndent) { + visitValueArgumentListInternal(checkNotNull(call.valueArgumentList)) + } + } + }, + ) } /** - * Returns true when [expression] is a chain whose innermost receiver is a scoping function call. + * Emit a `when (...) { ... }` that follows an operator such as `=`, deciding whether to break + * after that operator from the width of the `when (...) {` head alone: + * ``` + * val affected = when (event) { // the head fits: it stays on the `=` line + * is Update -> emptyList() + * } * - * For example, this matches `runnnnn { ... }.baz()` (innermost receiver `runnnnn { ... }` is a - * scoping function). It does not match a chain whose root is a plain identifier or a non-scoping - * call, since those don't have a block-like opener to anchor the chain against. - */ - @OptIn(ExperimentalContracts::class) - private fun isChainedScopingFunction(expression: KtExpression): Boolean { - contract { returns(true) implies (expression is KtQualifiedExpression) } - - if (expression !is KtQualifiedExpression) return false - return isLambdaOrScopingFunction(chainRoot(expression)) - } - - /** Returns the innermost receiver of a (possibly nested) qualified [expression]. */ - private fun chainRoot(expression: KtExpression): KtExpression { - var root: KtExpression = expression - while (root is KtQualifiedExpression) { - root = root.receiverExpression - } - return root - } - - /** - * Returns true when [expression] is a call that is forced onto multiple lines regardless of the - * line width, either because its value argument list has a trailing comma (e.g. `foo(\n 1,\n - * 2,\n)`) or because one of its arguments is itself a block-like multiline call. + * val affected: List = // the head doesn't: the break is taken, and the body + * when (event) { // indents relative to the `when` + * is Update -> emptyList() + * } + * ``` * - * Such calls are rendered "block-like": they stay on the same line as the preceding `=`/`by` - * operator (instead of breaking and indenting after it), and any chained selectors break onto - * their own line, mirroring how scoping functions and lambdas are handled. + * The mechanics are the same as in [emitInfixBlockLikeCall], with the body of the `when` playing + * the part of the argument list there. */ - @OptIn(ExperimentalContracts::class) - private fun isBlockLikeCall(expression: KtExpression?): Boolean { - contract { returns(true) implies (expression is KtCallExpression) } - - if (expression == null) return false - val prev = expression.getPrevSiblingIgnoringWhitespace() - if (prev is PsiComment) { - return false // Leading comments cause weird indentation; keep the default layout. - } - - if (expression !is KtCallExpression) return false - val valueArgumentList = expression.valueArgumentList ?: return false - if (valueArgumentList.trailingComma != null) return true - return valueArgumentList.arguments.any { argument -> - val argumentExpression = argument.getArgumentExpression() - argumentExpression != null && - (isBlockLikeCall(argumentExpression) || isChainedBlockLikeCall(argumentExpression)) - } - } - - /** Returns true when [expression] is a chain whose innermost receiver is a [isBlockLikeCall]. */ - @OptIn(ExperimentalContracts::class) - private fun isChainedBlockLikeCall(expression: KtExpression): Boolean { - contract { returns(true) implies (expression is KtQualifiedExpression) } - - if (expression !is KtQualifiedExpression) return false - return isBlockLikeCall(chainRoot(expression)) + private fun emitWhenExpressionAfterOperator(expression: KtWhenExpression) { + builder.sync(expression) + emitSplitAfterOperator( + emitHead = { + builder.fenceComments() + emitWhenHead(expression) + }, + // The body's braces align with the `when`, so it is laid out at the head's own indent. + emitTail = { bodyIndent, _ -> builder.block(bodyIndent) { emitWhenBody(expression) } }, + ) } /** @@ -1752,10 +1876,56 @@ open class KotlinInputAstVisitor( } visit(parts[0]) + emitChainedSelectors(parts, forceBreak = true) + } + + /** + * Emit a `launch(dispatcher) { ... }.join()` style chain whose head is a call carrying a trailing + * lambda: render that call block-like, so its lambda body is indented one block in and its + * closing brace returns to the chain's own indent, then hang the remaining selectors off that + * brace: + * ``` + * GlobalScope.launch(Dispatchers.Main) { + * expect(2) + * }.join() + * ``` + * + * Unlike [visitChainedScopingFunction], the selectors are not forced onto their own line -- they + * only break when they don't fit, in which case they indent by [expressionBreakIndent]. + * + * Returns false, having emitted nothing, when [expression] isn't built on a trailing lambda that + * way; the caller then emits the regular chain layout. + */ + private fun visitChainAfterTrailingLambda( + expression: KtExpression, + emitLeadingBreak: Boolean, + ): Boolean { + val headIndex = expression.trailingLambdaChainHead ?: return false + val parts = breakIntoParts(expression) + if (emitLeadingBreak) { + builder.space() + } + + visit(parts[headIndex]) + emitChainedSelectors(parts.subList(headIndex, parts.size), forceBreak = false) + return true + } + + /** + * Emit the `.selector` parts of a chain (everything after the innermost receiver, [parts]`[0]`), + * each on its own line, indented by [expressionBreakIndent]. + * + * @param forceBreak whether the break before each selector is forced, or may stay flat + */ + private fun emitChainedSelectors(parts: List, forceBreak: Boolean) { builder.block(expressionBreakIndent) { for (i in 1 until parts.size) { val part = parts[i] as KtQualifiedExpression - builder.forcedBreak() + if (forceBreak) { + builder.forcedBreak() + } else { + builder.breakOp() + } builder.token(part.operationSign.value) val selectorExpression = part.selectorExpression if (selectorExpression is KtCallExpression) { @@ -1775,9 +1945,11 @@ open class KotlinInputAstVisitor( /** * Returns true when any chained selector after the innermost scoping-function receiver carries - * value arguments (i.e. `.foo(a)` or `.fold({ ... }, { ... })`). Used to decide formatting style - * for property initializers: value-arg chains stay on same line as `=`, while no-arg chains - * break. + * value arguments (i.e. `.foo(a)` or `.fold({ ... }, { ... })`). + * + * Such chains are excluded from the special scoping-function chain layout in + * [visitQualifiedExpression], since they are better served by the general qualified-expression + * layout. */ private fun chainedSelectorsHaveValueArguments(expression: KtExpression): Boolean { var current: KtExpression = expression @@ -1791,17 +1963,6 @@ open class KotlinInputAstVisitor( return false } - /** - * Returns true when every chained selector after the innermost scoping-function receiver carries - * no value arguments (i.e. only `.foo()` or `.foo { ... }` with a trailing lambda). Selectors - * that pass regular value arguments are excluded from special chained handling in qualified - * expressions, since those chains are better served by the general qualified-expression layout - * except in property initializer context where we handle them specially. - */ - private fun chainedSelectorsHaveNoValueArguments(expression: KtExpression): Boolean { - return !chainedSelectorsHaveValueArguments(expression) - } - /** * Emit `runnnnn { ... }.baz().qux()` style: render the innermost scoping-function receiver * block-like (so the lambda braces sit at the surrounding indent), then emit each `.selector` @@ -1816,81 +1977,18 @@ open class KotlinInputAstVisitor( ) { val parts = breakIntoParts(expression) val root = parts[0] - val forceBreakBeforeChain = isMultilineScopingFunction(root) + val forceBreakBeforeChain = root.isMultilineScopingFunction visitLambdaOrScopingFunction(root, emitLeadingBreak = emitLeadingBreak) - builder.block(expressionBreakIndent) { - for (i in 1 until parts.size) { - val part = parts[i] as KtQualifiedExpression - if (forceBreakBeforeChain) { - builder.forcedBreak() - } else { - builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO) - } - 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) - } - } - } - } - - /** - * Returns true when [expression] is a scoping-function call whose lambda body has source-level - * newlines (i.e. spans multiple lines). Used to decide whether chained selectors after the - * lambda's closing brace must break onto a new line. - */ - private fun isMultilineScopingFunction(expression: KtExpression): Boolean { - var carry: KtExpression? = expression - if (carry is KtQualifiedExpression && carry.receiverExpression is KtSimpleNameExpression) { - carry = carry.selectorExpression - } - if (carry is KtCallExpression) { - carry = carry.lambdaArguments.firstOrNull()?.getArgumentExpression() - } - if (carry is KtLabeledExpression) { - carry = carry.baseExpression - } - if (carry is KtLambdaExpression) { - return hasSourceNewlineInLambdaBody(carry) - } - return false - } - - /** - * Returns true if the source code contains a newline anywhere inside the body of - * [lambdaExpression] — that is, between the opening `{` and the closing `}` of the function - * literal. Used by [FormattingOptions.preserveLambdaBreaks] to keep user-authored multi-line - * lambdas multi-line. - */ - private fun hasSourceNewlineInLambdaBody(lambdaExpression: KtLambdaExpression): Boolean { - val functionLiteral = lambdaExpression.functionLiteral - for (child in functionLiteral.node.children()) { - if (child.psi is PsiWhiteSpace && child.textContains('\n')) return true - } - return false + emitChainedSelectors(parts, forceBreak = forceBreakBeforeChain) } /** See [isLambdaOrScopingFunction] for examples. */ private fun visitLambdaOrScopingFunction(expr: PsiElement?, emitLeadingBreak: Boolean = true) { - val breakToExpr = genSym() + val breakToExpr = BreakTag() val breakSpace = if (emitLeadingBreak) " " else "" - builder.breakOp( - Doc.FillMode.INDEPENDENT, - breakSpace, - expressionBreakIndent, - Optional.of(breakToExpr), - ) + builder.breakToFill(breakSpace, expressionBreakIndent, Optional.of(breakToExpr)) var carry = expr if (carry is KtQualifiedExpression && carry.receiverExpression is KtSimpleNameExpression) { @@ -1899,9 +1997,12 @@ open class KotlinInputAstVisitor( carry = carry.selectorExpression } if (carry is KtCallExpression) { - visit(carry.calleeExpression) + val call = carry + visit(call.calleeExpression) + // The extra level keeps the type arguments off the broken level the lambda body forces. + builder.block(ZERO) { visit(call.typeArgumentList) } builder.space() - carry = carry.lambdaArguments[0].getArgumentExpression() + carry = call.lambdaArguments[0].getArgumentExpression() } if (carry is KtLabeledExpression) { visit(carry.labelQualifier) @@ -1933,8 +2034,7 @@ open class KotlinInputAstVisitor( } val name = classOrObject.nameIdentifier if (name != null) { - builder.space() - builder.token(name.text) + builder.spaceThenToken(name.text) visit(classOrObject.typeParameterList) } visit(classOrObject.primaryConstructor) @@ -1943,8 +2043,11 @@ open class KotlinInputAstVisitor( builder.space() builder.block(ZERO) { builder.token(":") - builder.breakOp(Doc.FillMode.UNIFIED, " ", expressionBreakIndent) - visit(superTypes) + // A lone supertype constructor call keeps the header line, breaking only its arguments. + if (!emitSuperTypeCallAfterColon(superTypes)) { + builder.breakOp(" ", expressionBreakIndent) + visit(superTypes) + } } } val typeConstraintList = classOrObject.typeConstraintList @@ -1968,7 +2071,7 @@ open class KotlinInputAstVisitor( builder.sync(constructor) builder.block(ZERO) { if (constructor.hasConstructorKeyword()) { - builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO) + builder.breakOp(" ") } visitFunctionLikeExpression( contextReceiverList = null, @@ -2022,8 +2125,7 @@ open class KotlinInputAstVisitor( override fun visitClassInitializer(initializer: KtClassInitializer) { builder.sync(initializer) - builder.token("init") - builder.space() + builder.tokenThenSpace("init") visit(initializer.body) } @@ -2045,19 +2147,15 @@ open class KotlinInputAstVisitor( if (directive.packageKeyword == null) { return } - builder.token("package") - builder.space() - var first = true - for (packageName in directive.packageNames) { - if (first) { - first = false - } else { + builder.tokenThenSpace("package") + for ((index, packageName) in directive.packageNames.withIndex()) { + if (index > 0) { builder.token(".") } builder.token(packageName.getIdentifier()?.text ?: packageName.getReferencedName()) } - builder.guessToken(";") + builder.guessSemicolon() builder.forcedBreak() } @@ -2070,8 +2168,7 @@ open class KotlinInputAstVisitor( /** Example `import com.foo.A` */ override fun visitImportDirective(directive: KtImportDirective) { builder.sync(directive) - builder.token("import") - builder.space() + builder.tokenThenSpace("import") val importedReference = directive.importedReference if (importedReference != null) { @@ -2087,14 +2184,12 @@ open class KotlinInputAstVisitor( // Possible alias. val alias = directive.alias?.nameIdentifier if (alias != null) { - builder.space() - builder.token("as") - builder.space() + builder.spacedToken("as") builder.token(alias.text ?: fail()) } // Force a newline afterwards. - builder.guessToken(";") + builder.guessSemicolon() builder.forcedBreak() } @@ -2146,10 +2241,18 @@ open class KotlinInputAstVisitor( visit(psi) } - if (onlyAnnotationsSoFar && forceAnnotationBreaks && psi is KtAnnotationEntry) { + val shouldForceBreak = + forceAnnotationBreaks && + // don't force break on parameter annotations + list.parent !is KtParameter && + // don't force break on receiver type annotations + !(list.parent is KtTypeReference && list.parent.parent is KtFunction) && + // don't force break on parameter type annotations + !(list.parent is KtTypeReference && list.parent.parent is KtParameter) + if (onlyAnnotationsSoFar && shouldForceBreak) { builder.forcedBreak() } else if (onlyAnnotationsSoFar) { - builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO) + builder.breakOp(" ") } else { builder.space() } @@ -2181,7 +2284,7 @@ open class KotlinInputAstVisitor( val annotationEntries = expression.annotationEntries for (annotationEntry in annotationEntries) { if (annotationEntry !== annotationEntries.first()) { - builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO) + builder.breakOp(" ") } visit(annotationEntry) } @@ -2196,10 +2299,10 @@ open class KotlinInputAstVisitor( expression.parent is KtBlockExpression -> builder.forcedBreak() baseExpression is KtLambdaExpression -> builder.space() baseExpression is KtReturnExpression -> builder.forcedBreak() - else -> builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO) + else -> builder.breakOp(" ") } - visit(expression.baseExpression) + visit(baseExpression) } } @@ -2222,14 +2325,11 @@ open class KotlinInputAstVisitor( builder.token("[") builder.block(ZERO) { - var first = true - builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO) - for (value in annotation.entries) { - if (!first) { - builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO) + builder.breakOp() + for ((index, value) in annotation.entries.withIndex()) { + if (index > 0) { + builder.breakOp(" ") } - first = false - visit(value) } } @@ -2272,6 +2372,9 @@ open class KotlinInputAstVisitor( data: Void?, ): Void? { for (child in fileAnnotationList.node.children()) { + // Leaf nodes -- whitespace and the tokens of the annotations themselves -- implement both + // ASTNode and PsiElement, while composite nodes do not. This skips the leaves, leaving the + // annotation entries to be visited. if (child is PsiElement) { continue } @@ -2284,7 +2387,13 @@ open class KotlinInputAstVisitor( override fun visitSuperTypeList(list: KtSuperTypeList) { builder.sync(list) - builder.block(expressionBreakIndent) { visitEachCommaSeparated(list.entries) } + builder.block(expressionBreakIndent) { + visitEachCommaSeparated( + list.entries, + leadingBreak = forceLineBreakAfterSupertypeColon, + compensateMissingLeadingBreak = false, + ) + } } override fun visitSuperTypeCallEntry(call: KtSuperTypeCallEntry) { @@ -2298,70 +2407,89 @@ open class KotlinInputAstVisitor( override fun visitDelegatedSuperTypeEntry(specifier: KtDelegatedSuperTypeEntry) { builder.sync(specifier) visit(specifier.typeReference) - builder.space() - builder.token("by") - builder.space() + builder.spacedToken("by") visit(specifier.delegateExpression) } override fun visitWhenExpression(expression: KtWhenExpression) { builder.sync(expression) builder.block(ZERO) { - emitKeywordWithCondition("when", expression.subjectExpression) + emitWhenHead(expression) + emitWhenBody(expression) + } + } - builder.space() - builder.token("{", Doc.Token.RealOrImaginary.REAL, blockIndent, Optional.of(blockIndent)) + /** Emits `when (subject) {`, the part of a `when` expression that opens its body. */ + private fun emitWhenHead(expression: KtWhenExpression) { + emitKeywordWithCondition("when", expression.subjectExpression) - expression.entries.forEachIndexed { index, whenEntry -> - builder.block(blockIndent) { - if (index != 0) { - // preserve new line if there's one - builder.blankLineWanted(OpsBuilder.BlankLineWanted.PRESERVE) - } - builder.forcedBreak() - builder.block(ZERO) { - if (whenEntry.elseKeyword != null) { - builder.token("else") - } else { - val conditions = whenEntry.conditions - for ((index, condition) in conditions.withIndex()) { - visit(condition) - builder.guessToken(",") - if (index != conditions.lastIndex) { - builder.forcedBreak() - } - } - } - whenEntry.guard?.let { guard -> - builder.space() - emitKeywordWithCondition( - "if", - guard.getExpression(), - surroundConditionWithParens = false, - ) - } + builder.space() + builder.token("{", Doc.Token.RealOrImaginary.REAL, blockIndent, Optional.of(blockIndent)) + } + + /** Emits the entries of a `when` expression and the `}` closing its body. */ + private fun emitWhenBody(expression: KtWhenExpression) { + expression.entries.forEachIndexed { index, whenEntry -> + builder.block(blockIndent) { + if (index != 0) { + // preserve new line if there's one + builder.blankLineWanted(BlankLineWanted.PRESERVE) + } + builder.forcedBreak() + + val whenExpression = whenEntry.expression + val bodyIsBraced = + whenExpression is KtBlockExpression || whenExpression is KtLambdaExpression + // When comma-separated conditions are allowed to share a line, whether they actually fit + // depends on the `-> body` that trails them, so they have to be laid out in the same level + // as it. A braced body always breaks, so it is kept out of that level -- otherwise the + // conditions would always be broken apart too. + val conditionsShareLevelWithBody = !forceLineBreakInWhenConditionList && !bodyIsBraced + + builder.block(ZERO, isEnabled = conditionsShareLevelWithBody) { + builder.block(ZERO, isEnabled = !conditionsShareLevelWithBody) { + emitWhenEntryConditions(whenEntry) } - val whenExpression = whenEntry.expression if (whenEntry.trailingComma != null) { builder.forcedBreak() } else { builder.space() } builder.token("->") - if (whenExpression is KtBlockExpression || whenExpression is KtLambdaExpression) { + if (bodyIsBraced) { builder.space() visit(whenExpression) } else { builder.block(expressionBreakIndent) { - builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO) + builder.breakToFill(" ") visit(whenExpression) } } - builder.guessToken(";") } - builder.forcedBreak() + builder.guessSemicolon() } - builder.token("}") + builder.forcedBreak() + } + builder.token("}") + } + + /** Emits `else`, or the comma-separated conditions of a `when` entry, followed by its guard. */ + private fun emitWhenEntryConditions(whenEntry: KtWhenEntry) { + if (whenEntry.elseKeyword != null) { + builder.token("else") + } else { + val conditions = whenEntry.conditions + for ((conditionIndex, condition) in conditions.withIndex()) { + visit(condition) + builder.guessToken(",") + if (conditionIndex != conditions.lastIndex) { + if (forceLineBreakInWhenConditionList) builder.forcedBreak() else builder.breakOp(" ") + } + } + } + whenEntry.guard?.let { guard -> + builder.space() + emitKeywordWithCondition("if", guard.getExpression(), surroundConditionWithParens = false) } } @@ -2373,7 +2501,7 @@ open class KotlinInputAstVisitor( if (enumEntryList != null) { builder.block(ZERO) { - builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO) + builder.breakOp() for (value in enumEntryList.enumEntries) { visit(value) if (builder.peekToken().getOrNull() == ",") { @@ -2382,15 +2510,15 @@ open class KotlinInputAstVisitor( } } } - builder.guessToken(";") + builder.guessSemicolon() if (members.isNotEmpty()) { builder.forcedBreak() - builder.blankLineWanted(OpsBuilder.BlankLineWanted.YES) + builder.blankLineWanted(BlankLineWanted.YES) } } else { val parent = body.parent - if (parent is KtClass && parent.isEnum() && children.isNotEmpty()) { + if (parent is KtClass && parent.isEnum()) { builder.token(";") builder.forcedBreak() } @@ -2400,18 +2528,22 @@ open class KotlinInputAstVisitor( for (curr in members) { val blankLineBetweenMembers = when { - prev == null -> OpsBuilder.BlankLineWanted.PRESERVE - prev !is KtProperty -> OpsBuilder.BlankLineWanted.YES - prev.getter != null || prev.setter != null -> OpsBuilder.BlankLineWanted.YES - curr is KtProperty -> OpsBuilder.BlankLineWanted.PRESERVE - else -> OpsBuilder.BlankLineWanted.YES + prev == null -> BlankLineWanted.PRESERVE + !forceLineBreaksBetweenEmptyMethods && + prev is KtFunction && + prev.bodyBlockExpression == null && + prev.bodyExpression == null -> BlankLineWanted.PRESERVE + prev !is KtProperty -> BlankLineWanted.YES + prev.getter != null || prev.setter != null -> BlankLineWanted.YES + curr is KtProperty -> BlankLineWanted.PRESERVE + else -> BlankLineWanted.YES } builder.blankLineWanted(blankLineBetweenMembers) markForPartialFormat() builder.block(ZERO) { visit(curr) } markForPartialFormat() - builder.guessToken(";") + builder.guessSemicolon() builder.forcedBreak() prev = curr @@ -2431,8 +2563,7 @@ open class KotlinInputAstVisitor( override fun visitWhenConditionIsPattern(condition: KtWhenConditionIsPattern) { builder.sync(condition) - builder.token(if (condition.isNegated) "!is" else "is") - builder.space() + builder.tokenThenSpace(if (condition.isNegated) "!is" else "is") visit(condition.typeReference) } @@ -2442,8 +2573,7 @@ open class KotlinInputAstVisitor( // TODO: replace with 'condition.isNegated' once https://youtrack.jetbrains.com/issue/KT-34395 // is fixed. val isNegated = condition.firstChild?.node?.findChildByType(KtTokens.NOT_IN) != null - builder.token(if (isNegated) "!in" else "in") - builder.space() + builder.tokenThenSpace(if (isNegated) "!in" else "in") visit(condition.rangeExpression) } @@ -2456,8 +2586,7 @@ open class KotlinInputAstVisitor( builder.space() builder.block(ZERO) { visit(expression.then) } } else { - builder.breakOp(Doc.FillMode.INDEPENDENT, " ", expressionBreakIndent) - builder.block(expressionBreakIndent) { + builder.breakToFillThenBlock(" ", expressionBreakIndent) { builder.fenceComments() visit(expression.then) } @@ -2467,7 +2596,7 @@ open class KotlinInputAstVisitor( if (expression.then is KtBlockExpression) { builder.space() } else { - builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO) + builder.breakOp(" ") } builder.block(ZERO) { @@ -2476,8 +2605,7 @@ open class KotlinInputAstVisitor( builder.space() builder.block(ZERO) { visit(expression.`else`) } } else { - builder.breakOp(Doc.FillMode.INDEPENDENT, " ", expressionBreakIndent) - builder.block(expressionBreakIndent) { visit(expression.`else`) } + builder.breakToFillThenBlock(" ", expressionBreakIndent) { visit(expression.`else`) } } } } @@ -2496,22 +2624,35 @@ open class KotlinInputAstVisitor( } /** - * 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) + * Emits a comma-separated list wrapped in delimiters, with the closing one outside the level the + * elements are in so that it returns to the surrounding indent. */ - private fun visitArrayAccessBrackets(expression: KtArrayAccessExpression) { + private fun emitDelimitedList( + elements: Iterable, + hasTrailingComma: Boolean, + openingDelimiter: String, + closingDelimiter: String, + ) { builder.block(ZERO) { - builder.token("[") - builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakIndent) - builder.block(expressionBreakIndent) { - visitEachCommaSeparated( - expression.indexExpressions, - expression.trailingComma != null, - wrapInBlock = true, - ) + builder.token(openingDelimiter) + builder.breakOpThenBlock(expressionBreakIndent) { + visitEachCommaSeparated(elements, hasTrailingComma, wrapInBlock = true) } } - builder.token("]") + builder.token(closingDelimiter) + } + + /** + * 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) { + emitDelimitedList( + expression.indexExpressions, + expression.trailingComma != null, + openingDelimiter = "[", + closingDelimiter = "]", + ) } /** Example `val (a, b: Int) = Pair(1, 2)` or `val [a, b] = Pair(1, 2)` */ @@ -2519,32 +2660,22 @@ open class KotlinInputAstVisitor( builder.sync(destructuringDeclaration) val valOrVarKeyword = destructuringDeclaration.valOrVarKeyword if (valOrVarKeyword != null) { - builder.token(valOrVarKeyword.text) - builder.space() + builder.tokenThenSpace(valOrVarKeyword.text) } val hasTrailingComma = destructuringDeclaration.trailingComma != null - val openingDelimiter = destructuringDeclaration.lPar?.text ?: "(" - val closingDelimiter = destructuringDeclaration.rPar?.text ?: ")" - builder.block(ZERO) { - builder.token(openingDelimiter) - builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakIndent) - builder.block(expressionBreakIndent) { - visitEachCommaSeparated( - destructuringDeclaration.entries, - hasTrailingComma, - wrapInBlock = true, - ) - } - } - builder.token(closingDelimiter) + emitDelimitedList( + destructuringDeclaration.entries, + hasTrailingComma, + openingDelimiter = destructuringDeclaration.lPar?.text ?: "(", + closingDelimiter = destructuringDeclaration.rPar?.text ?: ")", + ) val initializer = destructuringDeclaration.initializer if (initializer != null) { - builder.space() - builder.token("=") + builder.spaceThenToken("=") if (hasTrailingComma) { builder.space() } else { - builder.breakOp(Doc.FillMode.INDEPENDENT, " ", expressionBreakIndent) + builder.breakToFill(" ", expressionBreakIndent) } builder.block(expressionBreakIndent, !hasTrailingComma) { visit(initializer) } } @@ -2555,9 +2686,9 @@ open class KotlinInputAstVisitor( multiDeclarationEntry: KtDestructuringDeclarationEntry, ) { builder.sync(multiDeclarationEntry) - declareOne( + emitVariableLikeDeclaration( initializer = multiDeclarationEntry.initializer, - kind = DeclarationKind.PARAMETER, + isField = false, modifiers = multiDeclarationEntry.modifierList, name = multiDeclarationEntry.nameIdentifier?.text ?: fail(), type = multiDeclarationEntry.typeReference, @@ -2604,9 +2735,7 @@ open class KotlinInputAstVisitor( builder.token(parameter.nameIdentifier?.text ?: "") val extendsBound = parameter.extendsBound if (extendsBound != null) { - builder.space() - builder.token(":") - builder.space() + builder.spacedToken(":") visit(extendsBound) } } @@ -2614,10 +2743,10 @@ open class KotlinInputAstVisitor( /** Example `where T : View, T : Listener` */ override fun visitTypeConstraintList(list: KtTypeConstraintList) { builder.block(expressionBreakIndent) { - builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO) + builder.breakToFill(" ") builder.token("where") builder.block(expressionBreakIndent) { - builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO) + builder.breakOp(" ") builder.sync(list) visitEachCommaSeparated(list.constraints, wrapInBlock = false) } @@ -2629,9 +2758,7 @@ open class KotlinInputAstVisitor( builder.sync(constraint) // TODO(nreid260): What about annotations on the type reference? `where @A T : Int` visit(constraint.subjectTypeParameterName) - builder.space() - builder.token(":") - builder.space() + builder.spacedToken(":") visit(constraint.boundTypeReference) } @@ -2639,15 +2766,12 @@ open class KotlinInputAstVisitor( override fun visitForExpression(expression: KtForExpression) { builder.sync(expression) builder.block(ZERO) { - builder.token("for") - builder.space() + builder.tokenThenSpace("for") builder.token("(") visit(expression.loopParameter) - builder.space() - builder.token("in") + builder.spaceThenToken("in") builder.block(ZERO) { - builder.breakOp(Doc.FillMode.UNIFIED, " ", expressionBreakIndent) - builder.block(expressionBreakIndent) { visit(expression.loopRange) } + builder.breakOpThenBlock(" ", expressionBreakIndent) { visit(expression.loopRange) } } builder.token(")") builder.space() @@ -2666,8 +2790,7 @@ open class KotlinInputAstVisitor( /** Example `do { ... } while (a < b)` */ override fun visitDoWhileExpression(expression: KtDoWhileExpression) { builder.sync(expression) - builder.token("do") - builder.space() + builder.tokenThenSpace("do") if (expression.body != null) { visit(expression.body) builder.space() @@ -2699,14 +2822,13 @@ open class KotlinInputAstVisitor( builder.block(ZERO) { visit(destructuringDeclaration) if (typeReference != null) { - builder.token(":") - builder.space() + builder.tokenThenSpace(":") visit(typeReference) } } } else { - declareOne( - kind = DeclarationKind.PARAMETER, + emitVariableLikeDeclaration( + isField = false, modifiers = parameter.modifierList, valOrVarKeyword = parameter.valOrVarKeyword?.text, name = parameter.nameIdentifier?.text, @@ -2732,7 +2854,7 @@ open class KotlinInputAstVisitor( builder.block(expressionBreakIndent) { builder.token("::") - builder.breakOp(Doc.FillMode.INDEPENDENT, "", ZERO) + builder.breakToFill() visit(expression.callableReference) } } @@ -2778,47 +2900,62 @@ open class KotlinInputAstVisitor( ) } } - builder.space() - builder.token("->") - builder.space() + builder.spacedToken("->") builder.block(expressionBreakIndent) { visit(type.returnTypeReference) } } + /** + * Emits an operation whose right-hand side is a type -- `a is Int`, `a as Int` -- as a single + * group, so that a break lands before the operator rather than inside the left-hand side. + * + * A qualified left-hand side lays out its own chain, so the group opens after it; anything else + * is enclosed by the group. + * + * @param emitSeparator emits what goes between the left-hand side and the operator + */ + private fun emitTypeOperation( + left: KtExpression?, + operationReference: PsiElement, + right: PsiElement?, + emitSeparator: () -> Unit, + ) { + val openGroupBeforeLeft = left !is KtQualifiedExpression + if (openGroupBeforeLeft) builder.open(ZERO) + visit(left) + if (!openGroupBeforeLeft) builder.open(ZERO) + emitSeparator() + visit(operationReference) + builder.breakToFillThenBlock(" ", expressionBreakIndent) { visit(right) } + builder.close() + } + /** Example `a is Int` or `b !is Int` */ override fun visitIsExpression(expression: KtIsExpression) { builder.sync(expression) - val openGroupBeforeLeft = expression.leftHandSide !is KtQualifiedExpression - if (openGroupBeforeLeft) builder.open(ZERO) - visit(expression.leftHandSide) - if (!openGroupBeforeLeft) builder.open(ZERO) - val parent = expression.parent - if ( - parent is KtValueArgument || - parent is KtParenthesizedExpression || - parent is KtContainerNode + emitTypeOperation( + expression.leftHandSide, + expression.operationReference, + expression.typeReference, ) { - builder.breakOp(Doc.FillMode.UNIFIED, " ", expressionBreakIndent) - } else { - builder.space() + val parent = expression.parent + if ( + parent is KtValueArgument || + parent is KtParenthesizedExpression || + parent is KtContainerNode + ) { + builder.breakOp(" ", expressionBreakIndent) + } else { + builder.space() + } } - visit(expression.operationReference) - builder.breakOp(Doc.FillMode.INDEPENDENT, " ", expressionBreakIndent) - builder.block(expressionBreakIndent) { visit(expression.typeReference) } - builder.close() } /** Example `a as Int` or `a as? Int` */ override fun visitBinaryWithTypeRHSExpression(expression: KtBinaryExpressionWithTypeRHS) { builder.sync(expression) - val openGroupBeforeLeft = expression.left !is KtQualifiedExpression - if (openGroupBeforeLeft) builder.open(ZERO) - visit(expression.left) - if (!openGroupBeforeLeft) builder.open(ZERO) - builder.breakOp(Doc.FillMode.UNIFIED, " ", expressionBreakIndent) - visit(expression.operationReference) - builder.breakOp(Doc.FillMode.INDEPENDENT, " ", expressionBreakIndent) - builder.block(expressionBreakIndent) { visit(expression.right) } - builder.close() + emitTypeOperation(expression.left, expression.operationReference, expression.right) { + builder.breakOp(" ", expressionBreakIndent) + } } /** @@ -2844,8 +2981,7 @@ open class KotlinInputAstVisitor( override fun visitTryExpression(expression: KtTryExpression) { builder.sync(expression) - builder.token("try") - builder.space() + builder.tokenThenSpace("try") visit(expression.tryBlock) for (catchClause in expression.catchClauses) { visit(catchClause) @@ -2855,13 +2991,11 @@ open class KotlinInputAstVisitor( override fun visitCatchSection(catchClause: KtCatchClause) { builder.sync(catchClause) - builder.space() - builder.token("catch") - builder.space() + builder.spacedToken("catch") builder.block(ZERO) { builder.token("(") builder.block(expressionBreakIndent) { - builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO) + builder.breakOp() visit(catchClause.catchParameter) builder.guessToken(",") } @@ -2873,16 +3007,13 @@ open class KotlinInputAstVisitor( override fun visitFinallySection(finallySection: KtFinallySection) { builder.sync(finallySection) - builder.space() - builder.token("finally") - builder.space() + builder.spacedToken("finally") visit(finallySection.finalExpression) } override fun visitThrowExpression(expression: KtThrowExpression) { builder.sync(expression) - builder.token("throw") - builder.space() + builder.tokenThenSpace("throw") visit(expression.thrownExpression) } @@ -2905,18 +3036,15 @@ open class KotlinInputAstVisitor( builder.sync(typeAlias) builder.block(ZERO) { visit(typeAlias.modifierList) - builder.token("typealias") - builder.space() + builder.tokenThenSpace("typealias") builder.token(typeAlias.nameIdentifier?.text ?: fail()) visit(typeAlias.typeParameterList) - builder.space() - builder.token("=") - builder.breakOp(Doc.FillMode.INDEPENDENT, " ", expressionBreakIndent) - builder.block(expressionBreakIndent) { + builder.spaceThenToken("=") + builder.breakToFillThenBlock(" ", expressionBreakIndent) { visit(typeAlias.getTypeReference()) visit(typeAlias.typeConstraintList) - builder.guessToken(";") + builder.guessSemicolon() } builder.forcedBreak() } @@ -2945,7 +3073,7 @@ open class KotlinInputAstVisitor( override fun visitKtFile(file: KtFile) { markForPartialFormat() - val importListEmpty = file.importList?.text?.isBlank() ?: true + val importListEmpty = file.importList?.text.isNullOrBlank() var isFirst = true for (child in file.children) { @@ -2955,10 +3083,10 @@ open class KotlinInputAstVisitor( builder.blankLineWanted( when { - isFirst -> OpsBuilder.BlankLineWanted.NO + isFirst -> BlankLineWanted.NO child is PsiComment -> continue - child is KtScript && importListEmpty -> OpsBuilder.BlankLineWanted.PRESERVE - else -> OpsBuilder.BlankLineWanted.YES + child is KtScript && importListEmpty -> BlankLineWanted.PRESERVE + else -> BlankLineWanted.YES }, ) @@ -2982,17 +3110,17 @@ open class KotlinInputAstVisitor( builder.forcedBreak() val childGetsBlankLineBefore = child !is KtProperty if (first) { - builder.blankLineWanted(OpsBuilder.BlankLineWanted.PRESERVE) + builder.blankLineWanted(BlankLineWanted.PRESERVE) } else if (lastChildIsContextReceiver) { - builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO) + builder.blankLineWanted(BlankLineWanted.NO) } else if ( child !is PsiComment && (childGetsBlankLineBefore || lastChildHadBlankLineBefore) ) { - builder.blankLineWanted(OpsBuilder.BlankLineWanted.YES) + builder.blankLineWanted(BlankLineWanted.YES) } builder.markForPartialFormat() visit(child) - builder.guessToken(";") + builder.guessSemicolon() builder.markForPartialFormat() lastChildHadBlankLineBefore = childGetsBlankLineBefore lastChildIsContextReceiver = @@ -3016,54 +3144,6 @@ open class KotlinInputAstVisitor( } } - /** - * Emit a [Doc.Token]. - * - * @param token the [String] to wrap in a [Doc.Token] - * @param plusIndentCommentsBefore extra block for comments before this token - */ - private fun OpsBuilder.token(token: String, plusIndentCommentsBefore: Indent = ZERO) { - token( - token, - Doc.Token.RealOrImaginary.REAL, - plusIndentCommentsBefore, - /* breakAndIndentTrailingComment */ Optional.empty(), - ) - } - - /** - * Opens a new level, emits into it and closes it. - * - * This is a helper method to make it easier to keep track of [OpsBuilder.open] and - * [OpsBuilder.close] calls - * - * @param plusIndent the block level to pass to the block - * @param block a code block to be run in this block level - */ - private fun OpsBuilder.block( - plusIndent: Indent = ZERO, - isEnabled: Boolean = true, - block: () -> Unit, - ) { - if (isEnabled) { - open(plusIndent) - } - block() - if (isEnabled) { - close() - } - } - - /** Helper method to sync the current offset to match any element in the AST */ - private fun OpsBuilder.sync(psiElement: PsiElement) { - sync(psiElement.startOffset) - } - - /** Prevent subsequent comments from being moved ahead of this point, into parent [Level]s. */ - private fun OpsBuilder.fenceComments() { - addAll(FenceCommentsOp.AS_LIST) - } - /** * Throws a formatting error * @@ -3096,19 +3176,18 @@ open class KotlinInputAstVisitor( } builder.block(ZERO) { - builder.token(keyword) - builder.space() + builder.tokenThenSpace(keyword) if (surroundConditionWithParens) { builder.token("(") } - if (options.manageTrailingCommas) { + if (options.manageTrailingCommas && !hugConditionParens) { builder.block(expressionBreakIndent) { - builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO) + builder.breakOp() visit(condition) - builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakNegativeIndent) + builder.breakOp(expressionBreakNegativeIndent) } } else { - builder.block(ZERO) { visit(condition) } + builder.block(if (hugConditionParens) expressionBreakIndent else ZERO) { visit(condition) } } } if (surroundConditionWithParens) { diff --git a/core/src/main/java/com/facebook/ktfmt/format/KotlinLangInputAstVisitor.kt b/core/src/main/java/com/facebook/ktfmt/format/KotlinLangInputAstVisitor.kt index ab88b2a5d..b0c92a0d2 100644 --- a/core/src/main/java/com/facebook/ktfmt/format/KotlinLangInputAstVisitor.kt +++ b/core/src/main/java/com/facebook/ktfmt/format/KotlinLangInputAstVisitor.kt @@ -7,4 +7,16 @@ internal class KotlinLangInputAstVisitor( builder: OpsBuilder, ) : KotlinInputAstVisitor(options, builder) { override val forceAnnotationBreaks: Boolean = true + override val forceLineBreakAfterAssignment: Boolean = false + override val forceLineBreakAfterNamedParameter: Boolean = false + override val forceLineBreakAfterSupertypeColon: Boolean = false + override val forceLineBreakBeforeAccessors: Boolean = false + override val hugBlockLikeInfixCalls: Boolean = true + override val hugCallsWithTrailingLambda: Boolean = true + override val hugChainsAfterTrailingLambda: Boolean = true + override val hugConditionParens: Boolean = true + override val hugWhenExpressions: Boolean = true + override val indentBooleanConditions: Boolean = false + override val forceLineBreakInWhenConditionList: Boolean = false + override val forceLineBreaksBetweenEmptyMethods: Boolean = false } diff --git a/core/src/main/java/com/facebook/ktfmt/format/OpsUtils.kt b/core/src/main/java/com/facebook/ktfmt/format/OpsUtils.kt new file mode 100644 index 000000000..9ab48b68f --- /dev/null +++ b/core/src/main/java/com/facebook/ktfmt/format/OpsUtils.kt @@ -0,0 +1,164 @@ +package com.facebook.ktfmt.format + +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.BreakTag +import java.util.Optional +import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.psi.psiUtil.startOffset + +/** + * Emit a [Doc.Token]. + * + * @param token the [String] to wrap in a [Doc.Token] + * @param plusIndentCommentsBefore extra block for comments before this token + */ +internal fun OpsBuilder.token(token: String, plusIndentCommentsBefore: Indent = ZERO) { + token( + token, + Token.RealOrImaginary.REAL, + plusIndentCommentsBefore, + /* breakAndIndentTrailingComment */ Optional.empty(), + ) +} + +/** Emit a [Doc.Token] followed by a [Doc.Space]. */ +internal fun OpsBuilder.tokenThenSpace(token: String) { + token(token) + space() +} + +/** Emit a [Doc.Space] followed by a [Doc.Token]. */ +internal fun OpsBuilder.spaceThenToken(token: String) { + space() + token(token) +} + +/** Emit a [Doc.Token] surrounded by [Doc.Space]s. */ +internal fun OpsBuilder.spacedToken(token: String) { + space() + token(token) + space() +} + +/** + * Emit a [Doc.Break] with a specified [flat] value and extra indent. + * + * [OpsBuilder] only provides overloads that set either [flat] or [plusIndent], but not both. + * + * @param flat the [Doc.Break] when not broken + * @param plusIndent extra indent if taken + * @param optionalTag an optional tag for remembering whether the break was taken + */ +internal fun OpsBuilder.breakOp( + flat: String, + plusIndent: Indent, + optionalTag: Optional = Optional.empty(), +) { + breakOp(Doc.FillMode.UNIFIED, flat, plusIndent, optionalTag) +} + +/** + * Emit a filled [Doc.Break] with extra indent. + * + * @param plusIndent extra indent if taken + */ +internal fun OpsBuilder.breakToFill(plusIndent: Indent) { + breakOp(Doc.FillMode.INDEPENDENT, "", plusIndent) +} + +/** + * Emit a filled [Doc.Break] with a specified [flat] value and extra indent. + * + * @param flat the [Doc.Break] when not broken + * @param plusIndent extra indent if taken + * @param optionalTag an optional tag for remembering whether the break was taken + */ +internal fun OpsBuilder.breakToFill( + flat: String, + plusIndent: Indent, + optionalTag: Optional = Optional.empty(), +) { + breakOp(Doc.FillMode.INDEPENDENT, flat, plusIndent, optionalTag) +} + +/** + * Opens a new level, emits into it and closes it. + * + * This is a helper method to make it easier to keep track of [OpsBuilder.open] and + * [OpsBuilder.close] calls + * + * @param plusIndent the block level to pass to the block + * @param block a code block to be run in this block level + */ +internal fun OpsBuilder.block( + plusIndent: Indent = ZERO, + isEnabled: Boolean = true, + block: () -> Unit, +) { + if (isEnabled) { + open(plusIndent) + } + block() + if (isEnabled) { + close() + } +} + +/** + * Emit a [Doc.Break], then open a level indented by the same amount, emit into it and close it. + * + * Breaking and then indenting the continuation by the same [plusIndent] is a very common + * combination, and stating the indent once keeps the two from drifting apart. + * + * @param plusIndent extra indent if the break is taken, and the indent of the level + * @param block a code block to be run in this block level + */ +internal fun OpsBuilder.breakOpThenBlock(plusIndent: Indent, block: () -> Unit) { + breakOpThenBlock("", plusIndent, block) +} + +/** + * Emit a [Doc.Break] with a specified [flat] value, then open a level indented by the same amount, + * emit into it and close it. + * + * @param flat the [Doc.Break] when not broken + * @param plusIndent extra indent if the break is taken, and the indent of the level + * @param block a code block to be run in this block level + */ +internal fun OpsBuilder.breakOpThenBlock(flat: String, plusIndent: Indent, block: () -> Unit) { + breakOp(flat, plusIndent) + block(plusIndent, block = block) +} + +/** + * Emit a filled [Doc.Break] with a specified [flat] value, then open a level indented by the same + * amount, emit into it and close it. + * + * @param flat the [Doc.Break] when not broken + * @param plusIndent extra indent if the break is taken, and the indent of the level + * @param block a code block to be run in this block level + */ +internal fun OpsBuilder.breakToFillThenBlock(flat: String, plusIndent: Indent, block: () -> Unit) { + breakToFill(flat, plusIndent) + block(plusIndent, block = block) +} + +/** Emit a `;` if the input has one at this point. */ +internal fun OpsBuilder.guessSemicolon() { + guessToken(";") +} + +/** Helper method to sync the current offset to match any element in the AST */ +internal fun OpsBuilder.sync(psiElement: PsiElement) { + sync(psiElement.startOffset) +} + +/** Prevent subsequent comments from being moved ahead of this point, into parent [Level]s. */ +internal fun OpsBuilder.fenceComments() { + addAll(FenceCommentsOp.AS_LIST) +} diff --git a/core/src/main/java/com/facebook/ktfmt/format/PsiUtils.kt b/core/src/main/java/com/facebook/ktfmt/format/PsiUtils.kt index acef0c8c6..28d9e7d1e 100644 --- a/core/src/main/java/com/facebook/ktfmt/format/PsiUtils.kt +++ b/core/src/main/java/com/facebook/ktfmt/format/PsiUtils.kt @@ -16,12 +16,29 @@ package com.facebook.ktfmt.format +import java.util.ArrayDeque +import kotlin.contracts.ExperimentalContracts +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.KtTokens +import org.jetbrains.kotlin.psi.KtArrayAccessExpression +import org.jetbrains.kotlin.psi.KtBinaryExpression import org.jetbrains.kotlin.psi.KtCallExpression import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.psi.KtLabeledExpression +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.KtSimpleNameExpression +import org.jetbrains.kotlin.psi.KtStringTemplateExpression 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.getNextSiblingIgnoringWhitespace +import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespace /** Returns true if the expression represents an invocation that is also a lambda */ fun KtExpression.isLambda(): Boolean = this.callExpression?.lambdaArguments?.isNotEmpty() ?: false @@ -45,5 +62,306 @@ fun KtValueArgumentList.hasEmptyParens(): Boolean { * a qualified expression, or standing alone. This method makes it easier to handle both cases * uniformly. */ -private val KtExpression.callExpression: KtCallExpression? +internal val KtExpression.callExpression: KtCallExpression? get() = ((this as? KtQualifiedExpression)?.selectorExpression ?: this) as? KtCallExpression + +/** Returns the innermost receiver of a (possibly nested) qualified [KtExpression]. */ +internal val KtExpression.chainRoot: KtExpression + get() { + var root: KtExpression = this + while (root is KtQualifiedExpression) { + root = root.receiverExpression + } + return root + } + +/** + * Whether a comment precedes this element. + * + * A leading comment brings its own forced break with it, which throws off any layout that decides + * indentation from whether the break before the element was taken. Such layouts fall back to the + * default one when this is true. + */ +internal val PsiElement.hasLeadingComment: Boolean + get() = getPrevSiblingIgnoringWhitespace() is PsiComment + +/** + * Returns true when [KtExpression] is a call that is forced onto multiple lines regardless of the + * line width, either because its value argument list has a trailing comma (e.g. `foo(\n 1,\n + * 2,\n)`) or because one of its arguments is itself a block-like multiline call. + * + * Such calls are rendered "block-like": they stay on the same line as the preceding `=`/`by` + * operator (instead of breaking and indenting after it), and any chained selectors break onto their + * own line, mirroring how scoping functions and lambdas are handled. + */ +@OptIn(ExperimentalContracts::class) +internal val KtExpression?.isBlockLikeCall: Boolean + get() { + contract { returns(true) implies (this@isBlockLikeCall is KtCallExpression) } + + if (this == null) return false + if (hasLeadingComment) return false + + if (this !is KtCallExpression) return false + val valueArgumentList = valueArgumentList ?: return false + return valueArgumentList.trailingComma != null || + valueArgumentList.arguments.any { argument -> + val argumentExpression = argument.getArgumentExpression() + argumentExpression != null && + (argumentExpression.isBlockLikeCall || argumentExpression.isChainedBlockLikeCall) + } + } + +/** + * Returns true when [KtExpression] is an infix function call -- `a to b`, `x and y` -- whose + * right-hand operand ends in a [isBlockLikeCall] + */ +@OptIn(ExperimentalContracts::class) +internal val KtExpression?.isInfixBlockLikeCall: Boolean + get() { + contract { returns(true) implies (this@isInfixBlockLikeCall is KtBinaryExpression) } + + if (this !is KtBinaryExpression) return false + if (hasLeadingComment) return false + // An identifier as the operator is what distinguishes `a to b` from `a + b`. + if (operationToken != KtTokens.IDENTIFIER) return false + val right = right ?: return false + if (right.hasLeadingComment) return false + val call = right.callExpression ?: return false + return call.isBlockLikeCall && call.lambdaArguments.isEmpty() + } + +/** Returns true when [KtExpression] is a chain whose innermost receiver is a [isBlockLikeCall]. */ +@OptIn(ExperimentalContracts::class) +internal val KtExpression.isChainedBlockLikeCall: Boolean + get() { + contract { returns(true) implies (this@isChainedBlockLikeCall is KtQualifiedExpression) } + + return this is KtQualifiedExpression && this.chainRoot.isBlockLikeCall + } + +/** + * Returns true when [KtExpression] is a chain of dotted parts that gets the regular chain layout, + * e.g. `a[5].b!!.c()[4].f()`. + * + * Chains whose receiver is special-cased elsewhere -- a string template (`"a".trim()`) or a `when` + * -- are excluded, as are the block-like and scoping-function chains that have their own layout. + */ +@OptIn(ExperimentalContracts::class) +internal val KtExpression.isPlainQualifiedChain: Boolean + get() { + contract { returns(true) implies (this@isPlainQualifiedChain is KtQualifiedExpression) } + + if (this !is KtQualifiedExpression) return false + if (receiverExpression is KtStringTemplateExpression) return false + if (receiverExpression is KtWhenExpression) return false + return !isChainedBlockLikeCall && !isChainedScopingFunction + } + +/** + * Returns true when [KtExpression] is a chain whose innermost receiver is a scoping function call. + * + * For example, this matches `runnnnn { ... }.baz()` (innermost receiver `runnnnn { ... }` is a + * scoping function). It does not match a chain whose root is a plain identifier or a non-scoping + * call, since those don't have a block-like opener to anchor the chain against. + */ +@OptIn(ExperimentalContracts::class) +internal val KtExpression.isChainedScopingFunction: Boolean + get() { + contract { returns(true) implies (this@isChainedScopingFunction is KtQualifiedExpression) } + + return this is KtQualifiedExpression && this.chainRoot.isLambdaOrScopingFunction + } + +/** + * Returns whether an expression is a lambda or initializer expression in which case we will want to + * avoid indenting the lambda block + * + * Examples: + * 1. '... = { ... }' is a lambda expression + * 2. '... = Runnable { ... }' is considered a scoping function + * 3. '... = scope { ... }' '... = apply { ... }' is a scoping function + * 4. '... = scope.launch { ... }' is a dot-qualified scoping function + * 5. '... = async { ... }' is a scoping function with a type argument + * + * but not: + * 1. '... = foo() { ... }' due to the empty parenthesis + * 2. '... = Runnable @Annotation { ... }' due to the annotation + */ +internal val KtExpression?.isLambdaOrScopingFunction: Boolean + get() { + if (this == null) return false + val prev = this.getPrevSiblingIgnoringWhitespace() + if (prev is PsiComment && prev.text.startsWith("//")) { + return false // Leading line comments cause weird indentation; block comments are ok. + } + + var carry = this + if (carry is KtQualifiedExpression && carry.receiverExpression is KtSimpleNameExpression) { + carry = carry.selectorExpression + } + if (carry is KtCallExpression) { + if (carry.valueArgumentList?.leftParenthesis == null && carry.lambdaArguments.isNotEmpty()) { + carry = carry.lambdaArguments[0].getArgumentExpression() + } else { + return false + } + } + if (carry is KtLabeledExpression) { + carry = carry.baseExpression + } + if (carry is KtLambdaExpression) { + return true + } + + return false + } + +/** + * Returns true when [KtExpression] is a call that carries both a value argument list and a trailing + * lambda, e.g. `launch(dispatcher) { ... }` or `scope.launch(dispatcher) { ... }`. + * + * These are exactly the calls [isLambdaOrScopingFunction] turns down because of their parentheses. + * Styles that hug them lay them out the same way: the call keeps the line of the `=`/`by` operator + * it follows, and the lambda body is indented one block in from the declaration. + */ +internal val KtExpression?.isCallWithTrailingLambda: Boolean + get() { + if (this == null) return false + val prev = this.getPrevSiblingIgnoringWhitespace() + if (prev is PsiComment && prev.text.startsWith("//")) { + return false // Leading line comments cause weird indentation; block comments are ok. + } + + var carry: KtExpression? = this + if (carry is KtQualifiedExpression && carry.receiverExpression is KtSimpleNameExpression) { + carry = carry.selectorExpression + } + if (carry !is KtCallExpression) return false + return carry.hasTrailingLambdaAfterArguments + } + +/** + * The index into [breakIntoParts] of the part that ends the trailing lambda a chain is built on -- + * `scope.launch(dispatcher) { ... }` in `scope.launch(dispatcher) { ... }.join().await()` -- or + * null when the chain isn't built on one. + * + * The innermost such part is the head: a chain grows leftwards, so the first part carrying a + * trailing lambda is the one the selectors after it hang off, whether or not they carry trailing + * lambdas of their own. + */ +internal val KtExpression.trailingLambdaChainHead: Int? + get() { + if (this !is KtQualifiedExpression) return null + val parts = breakIntoParts(this) + // Array accesses and postfix operators (`a[0]`, `a!!`) split a chain into segments that neither + // the head nor the selectors after it can be laid out across, so those chains keep the regular + // layout. Only the parts past the innermost receiver can be one of those. + if (parts.subList(1, parts.size).any { it !is KtQualifiedExpression }) return null + // The last part is the chain itself, so a head there would leave no selectors to hang off it. + return (0 until parts.lastIndex).firstOrNull { index -> + parts[index].callExpression?.hasTrailingLambdaAfterArguments == true + } + } + +/** + * Whether this call carries both a parenthesized value argument list and a trailing lambda, e.g. + * `launch(dispatcher) { ... }`. Without the parentheses it is a scoping function instead, which + * [isLambdaOrScopingFunction] covers. + */ +private val KtCallExpression.hasTrailingLambdaAfterArguments: Boolean + get() { + if (valueArgumentList?.leftParenthesis == null) return false + var carry: KtExpression? = + lambdaArguments.firstOrNull()?.getArgumentExpression() ?: return false + if (carry is KtLabeledExpression) { + carry = carry.baseExpression + } + return carry is KtLambdaExpression + } + +/** + * Returns true when [KtExpression] is a scoping-function call whose lambda body has source-level + * newlines (i.e. spans multiple lines). Used to decide whether chained selectors after the lambda's + * closing brace must break onto a new line. + */ +internal val KtExpression.isMultilineScopingFunction: Boolean + get() { + var carry: KtExpression? = this + if (carry is KtQualifiedExpression && carry.receiverExpression is KtSimpleNameExpression) { + carry = carry.selectorExpression + } + if (carry is KtCallExpression) { + carry = carry.lambdaArguments.firstOrNull()?.getArgumentExpression() + } + if (carry is KtLabeledExpression) { + carry = carry.baseExpression + } + if (carry is KtLambdaExpression) { + return carry.hasSourceNewlineInLambdaBody + } + return false + } + +/** + * Returns true if the source code contains a newline anywhere inside the body of + * [KtLambdaExpression] — that is, between the opening `{` and the closing `}` of the function + * literal. Used by [FormattingOptions.preserveLambdaBreaks] to keep user-authored multi-line + * lambdas multi-line. + */ +internal val KtLambdaExpression.hasSourceNewlineInLambdaBody: Boolean + get() { + val functionLiteral = this.functionLiteral + for (child in functionLiteral.node.children()) { + if (child.psi is PsiWhiteSpace && child.textContains('\n')) return true + } + return false + } + +/** + * Checks if a line-breaking comment precedes [PsiElement] in the PSI tree. + * + * Line comments (`//`) always force a break. Block comments (`/* */`) only count if they are on + * their own line (preceded by whitespace with a newline). Inline block comments like `x /*tag*/ ||` + * do not force a break and should not trigger INDEPENDENT fill mode. + */ +internal val PsiElement.hasLineBreakingCommentBefore: Boolean + get() { + var prev = this.prevSibling + while (prev is PsiWhiteSpace) { + prev = prev.prevSibling + } + if (prev !is PsiComment) return false + + // Line comments always force a line break + if (prev.text.startsWith("//")) return true + + // Block comments force a break only if on their own line + val beforeComment = prev.prevSibling + return beforeComment is PsiWhiteSpace && beforeComment.text.contains('\n') + } + +/** + * Decomposes a qualified expression into parts, so `rainbow.red.orange.yellow` becomes `[rainbow, + * rainbow.red, rainbow.red.orange, rainbow.orange.yellow]` + */ +internal fun breakIntoParts(expression: KtExpression): List { + val parts = ArrayDeque() + + // use an ArrayDeque and add elements to the beginning so the innermost expression comes first + // foo.bar.yay -> [yay, bar.yay, foo.bar.yay] + + var node: KtExpression? = expression + while (node != null) { + parts.addFirst(node) + node = + when (node) { + is KtQualifiedExpression -> node.receiverExpression + is KtArrayAccessExpression -> node.arrayExpression + is KtPostfixExpression -> node.baseExpression + else -> null + } + } + + return parts.toList() +} diff --git a/core/src/test/java/com/facebook/ktfmt/cli/ParsedArgsTest.kt b/core/src/test/java/com/facebook/ktfmt/cli/ParsedArgsTest.kt index c74010aac..f8e41735c 100644 --- a/core/src/test/java/com/facebook/ktfmt/cli/ParsedArgsTest.kt +++ b/core/src/test/java/com/facebook/ktfmt/cli/ParsedArgsTest.kt @@ -289,10 +289,9 @@ class ParsedArgsTest { @Test fun `processArgs use the @file option with non existing file`() { - val e = - assertThrows { - ParsedArgs.processArgs(arrayOf("@non-existing-file")) - } + val e = assertThrows { + ParsedArgs.processArgs(arrayOf("@non-existing-file")) + } assertContains(e.message, "non-existing-file") } diff --git a/core/src/test/resources/cases/new_codestyle/AnnotationsOnParameters.input b/core/src/test/resources/cases/new_codestyle/AnnotationsOnParameters.input new file mode 100644 index 000000000..229565716 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/AnnotationsOnParameters.input @@ -0,0 +1,28 @@ +private class AnnotatedConstructor( + @JvmField @Volatile private var state: Int, + @get:JvmName("ctx") val context: CoroutineContext, + @Suppress("UNUSED_PARAMETER") unused: String = "", + @property:Inject val dependency: Dependency, + @Deprecated("Use context instead", ReplaceWith("context")) val legacyContext: CoroutineContext, +) + +private class AnnotatedParameters { + fun single(@NotNull a: String) = a + + fun multiple(@NotNull @Size(min = 1) a: String, @Nullable b: String?) = a + b + + fun wrapped( + @NotNull @Size(min = 1, max = 100) someVeryLongParameterName: String, + @Nullable anotherRatherLongParameterName: String? = null, + ) = someVeryLongParameterName + anotherRatherLongParameterName + + fun varargs(@NotNull vararg values: String) = values.size + + fun higherOrder(@NotNull block: (@Nullable Int) -> Unit) = block(1) + + fun @receiver:NotNull String.extension(@NotNull other: String) = this + other + + fun withLambda(@NotNull name: String, @Nullable block: () -> Unit = {}) { + block() + } +} diff --git a/core/src/test/resources/cases/new_codestyle/AnnotationsOnParameters.new.output b/core/src/test/resources/cases/new_codestyle/AnnotationsOnParameters.new.output new file mode 100644 index 000000000..229565716 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/AnnotationsOnParameters.new.output @@ -0,0 +1,28 @@ +private class AnnotatedConstructor( + @JvmField @Volatile private var state: Int, + @get:JvmName("ctx") val context: CoroutineContext, + @Suppress("UNUSED_PARAMETER") unused: String = "", + @property:Inject val dependency: Dependency, + @Deprecated("Use context instead", ReplaceWith("context")) val legacyContext: CoroutineContext, +) + +private class AnnotatedParameters { + fun single(@NotNull a: String) = a + + fun multiple(@NotNull @Size(min = 1) a: String, @Nullable b: String?) = a + b + + fun wrapped( + @NotNull @Size(min = 1, max = 100) someVeryLongParameterName: String, + @Nullable anotherRatherLongParameterName: String? = null, + ) = someVeryLongParameterName + anotherRatherLongParameterName + + fun varargs(@NotNull vararg values: String) = values.size + + fun higherOrder(@NotNull block: (@Nullable Int) -> Unit) = block(1) + + fun @receiver:NotNull String.extension(@NotNull other: String) = this + other + + fun withLambda(@NotNull name: String, @Nullable block: () -> Unit = {}) { + block() + } +} diff --git a/core/src/test/resources/cases/new_codestyle/AnnotationsOnParameters.output b/core/src/test/resources/cases/new_codestyle/AnnotationsOnParameters.output new file mode 100644 index 000000000..7c4fdbc1b --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/AnnotationsOnParameters.output @@ -0,0 +1,28 @@ +private class AnnotatedConstructor( + @JvmField @Volatile private var state: Int, + @get:JvmName("ctx") val context: CoroutineContext, + @Suppress("UNUSED_PARAMETER") unused: String = "", + @property:Inject val dependency: Dependency, + @Deprecated("Use context instead", ReplaceWith("context")) val legacyContext: CoroutineContext, +) + +private class AnnotatedParameters { + fun single(@NotNull a: String) = a + + fun multiple(@NotNull @Size(min = 1) a: String, @Nullable b: String?) = a + b + + fun wrapped( + @NotNull @Size(min = 1, max = 100) someVeryLongParameterName: String, + @Nullable anotherRatherLongParameterName: String? = null, + ) = someVeryLongParameterName + anotherRatherLongParameterName + + fun varargs(@NotNull vararg values: String) = values.size + + fun higherOrder(@NotNull block: (@Nullable Int) -> Unit) = block(1) + + fun @receiver:NotNull String.extension(@NotNull other: String) = this + other + + fun withLambda(@NotNull name: String, @Nullable block: () -> Unit = {}) { + block() + } +} diff --git a/core/src/test/resources/cases/new_codestyle/AssignmentStatement.input b/core/src/test/resources/cases/new_codestyle/AssignmentStatement.input new file mode 100644 index 000000000..5712d2820 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/AssignmentStatement.input @@ -0,0 +1,47 @@ +fun singleCallChain() { + reports { + total { + html { + report { + htmlDir = conventionProject.layout.buildDirectory.dir("kover/${project.name}/html") + } + } + } + } +} + +fun splittableChain() { + reports { + total { + html { + htmlDir = conventionProject.layout.buildDirectory.dir("kover/html").asFile.absolutePath + } + } + } +} + +fun annotatedAssignment() { + var b + @Suppress("UNCHECKED_CAST") b = f(1) as Int + @Suppress("UNCHECKED_CAST") + b = f(1) as Int +} + +fun whenAssignment() { + reports { + total { + htmlDirectoryValue = when (someLongConditionValue) { + 1 -> firstValue + else -> secondValue + } + } + } +} + +fun scopingFunctionAssignment() { + reports { + total { + htmlDirectoryValue = conventionProjectValue.apply { someLongName = otherLongName } + } + } +} diff --git a/core/src/test/resources/cases/new_codestyle/AssignmentStatement.new.output b/core/src/test/resources/cases/new_codestyle/AssignmentStatement.new.output new file mode 100644 index 000000000..634836b2b --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/AssignmentStatement.new.output @@ -0,0 +1,51 @@ +fun singleCallChain() { + reports { + total { + html { + report { + htmlDir = + conventionProject.layout.buildDirectory.dir("kover/${project.name}/html") + } + } + } + } +} + +fun splittableChain() { + reports { + total { + html { + htmlDir = conventionProject.layout.buildDirectory + .dir("kover/html") + .asFile + .absolutePath + } + } + } +} + +fun annotatedAssignment() { + var b + @Suppress("UNCHECKED_CAST") b = f(1) as Int + @Suppress("UNCHECKED_CAST") + b = f(1) as Int +} + +fun whenAssignment() { + reports { + total { + htmlDirectoryValue = when (someLongConditionValue) { + 1 -> firstValue + else -> secondValue + } + } + } +} + +fun scopingFunctionAssignment() { + reports { + total { + htmlDirectoryValue = conventionProjectValue.apply { someLongName = otherLongName } + } + } +} diff --git a/core/src/test/resources/cases/new_codestyle/AssignmentStatement.output b/core/src/test/resources/cases/new_codestyle/AssignmentStatement.output new file mode 100644 index 000000000..b1cdc814d --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/AssignmentStatement.output @@ -0,0 +1,48 @@ +fun singleCallChain() { + reports { + total { + html { + report { + htmlDir = conventionProject.layout.buildDirectory.dir("kover/${project.name}/html") + } + } + } + } +} + +fun splittableChain() { + reports { + total { + html { + htmlDir = conventionProject.layout.buildDirectory.dir("kover/html").asFile.absolutePath + } + } + } +} + +fun annotatedAssignment() { + var b + @Suppress("UNCHECKED_CAST") b = f(1) as Int + @Suppress("UNCHECKED_CAST") + b = f(1) as Int +} + +fun whenAssignment() { + reports { + total { + htmlDirectoryValue = + when (someLongConditionValue) { + 1 -> firstValue + else -> secondValue + } + } + } +} + +fun scopingFunctionAssignment() { + reports { + total { + htmlDirectoryValue = conventionProjectValue.apply { someLongName = otherLongName } + } + } +} diff --git a/core/src/test/resources/cases/new_codestyle/BinaryExpressionExpressionBody.input b/core/src/test/resources/cases/new_codestyle/BinaryExpressionExpressionBody.input new file mode 100644 index 000000000..09050eac7 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/BinaryExpressionExpressionBody.input @@ -0,0 +1,4 @@ +private fun URI.isCachedOrLocal() = scheme == "file" || +host == "cache-redirector.jetbrains.com" || +host == "teamcity.jetbrains.com" || +host == "buildserver.labs.intellij.net" diff --git a/core/src/test/resources/cases/new_codestyle/BinaryExpressionExpressionBody.new.output b/core/src/test/resources/cases/new_codestyle/BinaryExpressionExpressionBody.new.output new file mode 100644 index 000000000..5cedfaf3e --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/BinaryExpressionExpressionBody.new.output @@ -0,0 +1,4 @@ +private fun URI.isCachedOrLocal() = scheme == "file" || + host == "cache-redirector.jetbrains.com" || + host == "teamcity.jetbrains.com" || + host == "buildserver.labs.intellij.net" diff --git a/core/src/test/resources/cases/new_codestyle/BinaryExpressionExpressionBody.output b/core/src/test/resources/cases/new_codestyle/BinaryExpressionExpressionBody.output new file mode 100644 index 000000000..328a37c31 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/BinaryExpressionExpressionBody.output @@ -0,0 +1,5 @@ +private fun URI.isCachedOrLocal() = + scheme == "file" || + host == "cache-redirector.jetbrains.com" || + host == "teamcity.jetbrains.com" || + host == "buildserver.labs.intellij.net" diff --git a/core/src/test/resources/cases/new_codestyle/BinaryExpressionInIf.input b/core/src/test/resources/cases/new_codestyle/BinaryExpressionInIf.input new file mode 100644 index 000000000..3055f3641 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/BinaryExpressionInIf.input @@ -0,0 +1,8 @@ +fun f() { + if (aaaaa == null || + aaaaa.bbbbb[0] == null || + aaaaa.bbbbb[0].cc == null || + aaaaa.bbbbb[0].dddd == null) { + println() + } +} diff --git a/core/src/test/resources/cases/new_codestyle/BinaryExpressionInIf.new.output b/core/src/test/resources/cases/new_codestyle/BinaryExpressionInIf.new.output new file mode 100644 index 000000000..f3f706b6c --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/BinaryExpressionInIf.new.output @@ -0,0 +1,8 @@ +fun f() { + if (aaaaa == null || + aaaaa.bbbbb[0] == null || + aaaaa.bbbbb[0].cc == null || + aaaaa.bbbbb[0].dddd == null) { + println() + } +} diff --git a/core/src/test/resources/cases/new_codestyle/BinaryExpressionInIf.output b/core/src/test/resources/cases/new_codestyle/BinaryExpressionInIf.output new file mode 100644 index 000000000..3055f3641 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/BinaryExpressionInIf.output @@ -0,0 +1,8 @@ +fun f() { + if (aaaaa == null || + aaaaa.bbbbb[0] == null || + aaaaa.bbbbb[0].cc == null || + aaaaa.bbbbb[0].dddd == null) { + println() + } +} diff --git a/core/src/test/resources/cases/new_codestyle/BinaryExpressionInNamedArgument.input b/core/src/test/resources/cases/new_codestyle/BinaryExpressionInNamedArgument.input new file mode 100644 index 000000000..6eefeff40 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/BinaryExpressionInNamedArgument.input @@ -0,0 +1,11 @@ +@Target(AnnotationTarget.CLASS) +@RequiresOptIn( + level = RequiresOptIn.Level.WARNING, + message = + "This is a kotlinx.coroutines API that is not intended to be inherited from, " + + "as the library may handle predefined instances of this in a special manner. " + + "This will be an error in a future release. " + + "If you need to inherit from this, please describe your use case in " + + "https://github.com/Kotlin/kotlinx.coroutines/issues, so that we can provide a stable API for inheritance. ", +) +public annotation class InternalForInheritanceCoroutinesApi \ No newline at end of file diff --git a/core/src/test/resources/cases/new_codestyle/BinaryExpressionInNamedArgument.new.output b/core/src/test/resources/cases/new_codestyle/BinaryExpressionInNamedArgument.new.output new file mode 100644 index 000000000..96dc434d6 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/BinaryExpressionInNamedArgument.new.output @@ -0,0 +1,10 @@ +@Target(AnnotationTarget.CLASS) +@RequiresOptIn( + level = RequiresOptIn.Level.WARNING, + message = "This is a kotlinx.coroutines API that is not intended to be inherited from, " + + "as the library may handle predefined instances of this in a special manner. " + + "This will be an error in a future release. " + + "If you need to inherit from this, please describe your use case in " + + "https://github.com/Kotlin/kotlinx.coroutines/issues, so that we can provide a stable API for inheritance. ", +) +public annotation class InternalForInheritanceCoroutinesApi diff --git a/core/src/test/resources/cases/new_codestyle/BinaryExpressionInNamedArgument.output b/core/src/test/resources/cases/new_codestyle/BinaryExpressionInNamedArgument.output new file mode 100644 index 000000000..071daf9c1 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/BinaryExpressionInNamedArgument.output @@ -0,0 +1,11 @@ +@Target(AnnotationTarget.CLASS) +@RequiresOptIn( + level = RequiresOptIn.Level.WARNING, + message = + "This is a kotlinx.coroutines API that is not intended to be inherited from, " + + "as the library may handle predefined instances of this in a special manner. " + + "This will be an error in a future release. " + + "If you need to inherit from this, please describe your use case in " + + "https://github.com/Kotlin/kotlinx.coroutines/issues, so that we can provide a stable API for inheritance. ", +) +public annotation class InternalForInheritanceCoroutinesApi diff --git a/core/src/test/resources/cases/new_codestyle/BlockLambdaAssignment.input b/core/src/test/resources/cases/new_codestyle/BlockLambdaAssignment.input new file mode 100644 index 000000000..75e58d030 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/BlockLambdaAssignment.input @@ -0,0 +1,5 @@ +val job = + GlobalScope.launch(dispatcher) { + ++capturedMutableState + expect(2) + } \ No newline at end of file diff --git a/core/src/test/resources/cases/new_codestyle/BlockLambdaAssignment.new.output b/core/src/test/resources/cases/new_codestyle/BlockLambdaAssignment.new.output new file mode 100644 index 000000000..006fe6391 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/BlockLambdaAssignment.new.output @@ -0,0 +1,4 @@ +val job = GlobalScope.launch(dispatcher) { + ++capturedMutableState + expect(2) +} diff --git a/core/src/test/resources/cases/new_codestyle/BlockLambdaAssignment.output b/core/src/test/resources/cases/new_codestyle/BlockLambdaAssignment.output new file mode 100644 index 000000000..30ae77d68 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/BlockLambdaAssignment.output @@ -0,0 +1,5 @@ +val job = + GlobalScope.launch(dispatcher) { + ++capturedMutableState + expect(2) + } diff --git a/core/src/test/resources/cases/new_codestyle/BooleanConditions.input b/core/src/test/resources/cases/new_codestyle/BooleanConditions.input new file mode 100644 index 000000000..789ff654f --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/BooleanConditions.input @@ -0,0 +1,16 @@ +fun f1() { + if ( + unwrapped !== rootCause && + unwrapped !== unwrappedCause && + unwrapped !is CancellationException && + seenExceptions.add(unwrapped) + ) { + rootCause.addSuppressed(unwrapped) + } +} + +fun f2() { + if (unwrapped !== rootCause) { + rootCause.addSuppressed(unwrapped) + } +} \ No newline at end of file diff --git a/core/src/test/resources/cases/new_codestyle/BooleanConditions.new.output b/core/src/test/resources/cases/new_codestyle/BooleanConditions.new.output new file mode 100644 index 000000000..728d8fcd3 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/BooleanConditions.new.output @@ -0,0 +1,14 @@ +fun f1() { + if (unwrapped !== rootCause && + unwrapped !== unwrappedCause && + unwrapped !is CancellationException && + seenExceptions.add(unwrapped)) { + rootCause.addSuppressed(unwrapped) + } +} + +fun f2() { + if (unwrapped !== rootCause) { + rootCause.addSuppressed(unwrapped) + } +} diff --git a/core/src/test/resources/cases/new_codestyle/BooleanConditions.output b/core/src/test/resources/cases/new_codestyle/BooleanConditions.output new file mode 100644 index 000000000..6648251fb --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/BooleanConditions.output @@ -0,0 +1,14 @@ +fun f1() { + if (unwrapped !== rootCause && + unwrapped !== unwrappedCause && + unwrapped !is CancellationException && + seenExceptions.add(unwrapped)) { + rootCause.addSuppressed(unwrapped) + } +} + +fun f2() { + if (unwrapped !== rootCause) { + rootCause.addSuppressed(unwrapped) + } +} diff --git a/core/src/test/resources/cases/new_codestyle/CallAfterBlockLambda.input b/core/src/test/resources/cases/new_codestyle/CallAfterBlockLambda.input new file mode 100644 index 000000000..7f50bd2dc --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/CallAfterBlockLambda.input @@ -0,0 +1,4 @@ +GlobalScope.launch(Dispatchers.Main) { + expect(2) + throw TestException() +}.join() diff --git a/core/src/test/resources/cases/new_codestyle/CallAfterBlockLambda.new.output b/core/src/test/resources/cases/new_codestyle/CallAfterBlockLambda.new.output new file mode 100644 index 000000000..7f50bd2dc --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/CallAfterBlockLambda.new.output @@ -0,0 +1,4 @@ +GlobalScope.launch(Dispatchers.Main) { + expect(2) + throw TestException() +}.join() diff --git a/core/src/test/resources/cases/new_codestyle/CallAfterBlockLambda.output b/core/src/test/resources/cases/new_codestyle/CallAfterBlockLambda.output new file mode 100644 index 000000000..e2c2eb7df --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/CallAfterBlockLambda.output @@ -0,0 +1,5 @@ +GlobalScope.launch(Dispatchers.Main) { + expect(2) + throw TestException() + } + .join() diff --git a/core/src/test/resources/cases/new_codestyle/CallArgumentWrapping.input b/core/src/test/resources/cases/new_codestyle/CallArgumentWrapping.input new file mode 100644 index 000000000..989d063a9 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/CallArgumentWrapping.input @@ -0,0 +1,13 @@ +fun f() { + add( + queue = + OverrideQueue( + queueSettings.waitTime, + queueSettings.firstToSolveWaitTime, + queueSettings.featuredRunWaitTime, + queueSettings.inProgressRunWaitTime, + queueSettings.maxQueueSize, + queueSettings.maxUntestedRun, + ) + ) +} \ No newline at end of file diff --git a/core/src/test/resources/cases/new_codestyle/CallArgumentWrapping.new.output b/core/src/test/resources/cases/new_codestyle/CallArgumentWrapping.new.output new file mode 100644 index 000000000..b0d7a4aad --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/CallArgumentWrapping.new.output @@ -0,0 +1,12 @@ +fun f() { + add( + queue = OverrideQueue( + queueSettings.waitTime, + queueSettings.firstToSolveWaitTime, + queueSettings.featuredRunWaitTime, + queueSettings.inProgressRunWaitTime, + queueSettings.maxQueueSize, + queueSettings.maxUntestedRun, + ), + ) +} diff --git a/core/src/test/resources/cases/new_codestyle/CallArgumentWrapping.output b/core/src/test/resources/cases/new_codestyle/CallArgumentWrapping.output new file mode 100644 index 000000000..2730c16f0 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/CallArgumentWrapping.output @@ -0,0 +1,13 @@ +fun f() { + add( + queue = + OverrideQueue( + queueSettings.waitTime, + queueSettings.firstToSolveWaitTime, + queueSettings.featuredRunWaitTime, + queueSettings.inProgressRunWaitTime, + queueSettings.maxQueueSize, + queueSettings.maxUntestedRun, + ) + ) +} diff --git a/core/src/test/resources/cases/new_codestyle/CallArgumentWrappingWithBreak.input b/core/src/test/resources/cases/new_codestyle/CallArgumentWrappingWithBreak.input new file mode 100644 index 000000000..57291823a --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/CallArgumentWrappingWithBreak.input @@ -0,0 +1,13 @@ +fun f() { + add( + queue = + AVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongOverrideQueue( + queueSettings.waitTime, + queueSettings.firstToSolveWaitTime, + queueSettings.featuredRunWaitTime, + queueSettings.inProgressRunWaitTime, + queueSettings.maxQueueSize, + queueSettings.maxUntestedRun, + ) + ) +} \ No newline at end of file diff --git a/core/src/test/resources/cases/new_codestyle/CallArgumentWrappingWithBreak.new.output b/core/src/test/resources/cases/new_codestyle/CallArgumentWrappingWithBreak.new.output new file mode 100644 index 000000000..53d91e8cc --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/CallArgumentWrappingWithBreak.new.output @@ -0,0 +1,13 @@ +fun f() { + add( + queue = + AVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongOverrideQueue( + queueSettings.waitTime, + queueSettings.firstToSolveWaitTime, + queueSettings.featuredRunWaitTime, + queueSettings.inProgressRunWaitTime, + queueSettings.maxQueueSize, + queueSettings.maxUntestedRun, + ), + ) +} diff --git a/core/src/test/resources/cases/new_codestyle/CallArgumentWrappingWithBreak.output b/core/src/test/resources/cases/new_codestyle/CallArgumentWrappingWithBreak.output new file mode 100644 index 000000000..a7fa132fe --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/CallArgumentWrappingWithBreak.output @@ -0,0 +1,13 @@ +fun f() { + add( + queue = + AVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongOverrideQueue( + queueSettings.waitTime, + queueSettings.firstToSolveWaitTime, + queueSettings.featuredRunWaitTime, + queueSettings.inProgressRunWaitTime, + queueSettings.maxQueueSize, + queueSettings.maxUntestedRun, + ) + ) +} diff --git a/core/src/test/resources/cases/new_codestyle/CallMultiArgumentWrapping.input b/core/src/test/resources/cases/new_codestyle/CallMultiArgumentWrapping.input new file mode 100644 index 000000000..de32c6b2c --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/CallMultiArgumentWrapping.input @@ -0,0 +1,22 @@ +fun f() { + add( + queue = + OverrideQueue( + queueSettings.waitTime, + queueSettings.firstToSolveWaitTime, + queueSettings.featuredRunWaitTime, + queueSettings.inProgressRunWaitTime, + queueSettings.maxQueueSize, + queueSettings.maxUntestedRun, + ), + foo = + OverrideQueue( + queueSettings.waitTime, + queueSettings.firstToSolveWaitTime, + queueSettings.featuredRunWaitTime, + queueSettings.inProgressRunWaitTime, + queueSettings.maxQueueSize, + queueSettings.maxUntestedRun, + ), + ) +} \ No newline at end of file diff --git a/core/src/test/resources/cases/new_codestyle/CallMultiArgumentWrapping.new.output b/core/src/test/resources/cases/new_codestyle/CallMultiArgumentWrapping.new.output new file mode 100644 index 000000000..068a14a0d --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/CallMultiArgumentWrapping.new.output @@ -0,0 +1,20 @@ +fun f() { + add( + queue = OverrideQueue( + queueSettings.waitTime, + queueSettings.firstToSolveWaitTime, + queueSettings.featuredRunWaitTime, + queueSettings.inProgressRunWaitTime, + queueSettings.maxQueueSize, + queueSettings.maxUntestedRun, + ), + foo = OverrideQueue( + queueSettings.waitTime, + queueSettings.firstToSolveWaitTime, + queueSettings.featuredRunWaitTime, + queueSettings.inProgressRunWaitTime, + queueSettings.maxQueueSize, + queueSettings.maxUntestedRun, + ), + ) +} diff --git a/core/src/test/resources/cases/new_codestyle/CallMultiArgumentWrapping.output b/core/src/test/resources/cases/new_codestyle/CallMultiArgumentWrapping.output new file mode 100644 index 000000000..f80ef3614 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/CallMultiArgumentWrapping.output @@ -0,0 +1,22 @@ +fun f() { + add( + queue = + OverrideQueue( + queueSettings.waitTime, + queueSettings.firstToSolveWaitTime, + queueSettings.featuredRunWaitTime, + queueSettings.inProgressRunWaitTime, + queueSettings.maxQueueSize, + queueSettings.maxUntestedRun, + ), + foo = + OverrideQueue( + queueSettings.waitTime, + queueSettings.firstToSolveWaitTime, + queueSettings.featuredRunWaitTime, + queueSettings.inProgressRunWaitTime, + queueSettings.maxQueueSize, + queueSettings.maxUntestedRun, + ), + ) +} diff --git a/core/src/test/resources/cases/new_codestyle/CommaSeparatedWhenConditions.input b/core/src/test/resources/cases/new_codestyle/CommaSeparatedWhenConditions.input new file mode 100644 index 000000000..5622d47c5 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/CommaSeparatedWhenConditions.input @@ -0,0 +1,14 @@ +fun f1(x: Int) { + when (x) { + 0, 1 -> println("a or b") + } +} + +fun f2() { + when (it.resolvedCall.resultingDescriptor) { + is LocalVariableDescriptor, + is ValueParameterDescriptor, + is ReceiverParameterDescriptor -> true + else -> false + } +} \ No newline at end of file diff --git a/core/src/test/resources/cases/new_codestyle/CommaSeparatedWhenConditions.new.output b/core/src/test/resources/cases/new_codestyle/CommaSeparatedWhenConditions.new.output new file mode 100644 index 000000000..2b4d8b63d --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/CommaSeparatedWhenConditions.new.output @@ -0,0 +1,14 @@ +fun f1(x: Int) { + when (x) { + 0, 1 -> println("a or b") + } +} + +fun f2() { + when (it.resolvedCall.resultingDescriptor) { + is LocalVariableDescriptor, + is ValueParameterDescriptor, + is ReceiverParameterDescriptor -> true + else -> false + } +} diff --git a/core/src/test/resources/cases/new_codestyle/CommaSeparatedWhenConditions.output b/core/src/test/resources/cases/new_codestyle/CommaSeparatedWhenConditions.output new file mode 100644 index 000000000..4dcf52483 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/CommaSeparatedWhenConditions.output @@ -0,0 +1,15 @@ +fun f1(x: Int) { + when (x) { + 0, + 1 -> println("a or b") + } +} + +fun f2() { + when (it.resolvedCall.resultingDescriptor) { + is LocalVariableDescriptor, + is ValueParameterDescriptor, + is ReceiverParameterDescriptor -> true + else -> false + } +} diff --git a/core/src/test/resources/cases/new_codestyle/DoWhileConditionWrapping.input b/core/src/test/resources/cases/new_codestyle/DoWhileConditionWrapping.input new file mode 100644 index 000000000..c2220a7fe --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/DoWhileConditionWrapping.input @@ -0,0 +1,5 @@ +fun f() { + do { + println() + } while (aaaaa == null || aaaaa.bbbbb[0] == null || aaaaa.bbbbb[0].cc == null || aaaaa.bbbbb[0].dddd == null) +} diff --git a/core/src/test/resources/cases/new_codestyle/DoWhileConditionWrapping.new.output b/core/src/test/resources/cases/new_codestyle/DoWhileConditionWrapping.new.output new file mode 100644 index 000000000..a0cc67e1e --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/DoWhileConditionWrapping.new.output @@ -0,0 +1,8 @@ +fun f() { + do { + println() + } while (aaaaa == null || + aaaaa.bbbbb[0] == null || + aaaaa.bbbbb[0].cc == null || + aaaaa.bbbbb[0].dddd == null) +} diff --git a/core/src/test/resources/cases/new_codestyle/DoWhileConditionWrapping.output b/core/src/test/resources/cases/new_codestyle/DoWhileConditionWrapping.output new file mode 100644 index 000000000..d41cf4705 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/DoWhileConditionWrapping.output @@ -0,0 +1,8 @@ +fun f() { + do { + println() + } while (aaaaa == null || + aaaaa.bbbbb[0] == null || + aaaaa.bbbbb[0].cc == null || + aaaaa.bbbbb[0].dddd == null) +} diff --git a/core/src/test/resources/cases/new_codestyle/ExpressionBodyHugging.input b/core/src/test/resources/cases/new_codestyle/ExpressionBodyHugging.input new file mode 100644 index 000000000..e61e0acd8 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/ExpressionBodyHugging.input @@ -0,0 +1,35 @@ +class Foo { + fun affectedTeams(event: TeamEvent) = when (event) { + is TeamUpdate -> emptyList() + is TeamDelete -> listOf(event.teamId) + else -> error("unexpected") + } + + val affectedTeamsProperty = when (event) { + is TeamUpdate -> emptyList() + is TeamDelete -> listOf(event.teamId) + else -> error("unexpected") + } + + val overrides get() = when (event) { + is TeamUpdate -> emptyList() + else -> error("unexpected") + } + + fun entry() = "clics" to DumpFileCommand( + name = "clics-archive", + waitTime = waitTime, + ) + + val entryProperty = "clics" to DumpFileCommand( + name = "clics-archive", + waitTime = waitTime, + ) + + fun callChain() = StressOptions() + .iterations(20 * stressTestMultiplierSqrt) + .invocationsPerIteration(1_000 * stressTestMultiplierSqrt) + .commonConfiguration() + .customize(isStressTest) + .check(this::class) +} diff --git a/core/src/test/resources/cases/new_codestyle/ExpressionBodyHugging.new.output b/core/src/test/resources/cases/new_codestyle/ExpressionBodyHugging.new.output new file mode 100644 index 000000000..68db7621d --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/ExpressionBodyHugging.new.output @@ -0,0 +1,36 @@ +class Foo { + fun affectedTeams(event: TeamEvent) = when (event) { + is TeamUpdate -> emptyList() + is TeamDelete -> listOf(event.teamId) + else -> error("unexpected") + } + + val affectedTeamsProperty = when (event) { + is TeamUpdate -> emptyList() + is TeamDelete -> listOf(event.teamId) + else -> error("unexpected") + } + + val overrides + get() = when (event) { + is TeamUpdate -> emptyList() + else -> error("unexpected") + } + + fun entry() = "clics" to DumpFileCommand( + name = "clics-archive", + waitTime = waitTime, + ) + + val entryProperty = "clics" to DumpFileCommand( + name = "clics-archive", + waitTime = waitTime, + ) + + fun callChain() = StressOptions() + .iterations(20 * stressTestMultiplierSqrt) + .invocationsPerIteration(1_000 * stressTestMultiplierSqrt) + .commonConfiguration() + .customize(isStressTest) + .check(this::class) +} diff --git a/core/src/test/resources/cases/new_codestyle/ExpressionBodyHugging.output b/core/src/test/resources/cases/new_codestyle/ExpressionBodyHugging.output new file mode 100644 index 000000000..2c324c01d --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/ExpressionBodyHugging.output @@ -0,0 +1,44 @@ +class Foo { + fun affectedTeams(event: TeamEvent) = + when (event) { + is TeamUpdate -> emptyList() + is TeamDelete -> listOf(event.teamId) + else -> error("unexpected") + } + + val affectedTeamsProperty = + when (event) { + is TeamUpdate -> emptyList() + is TeamDelete -> listOf(event.teamId) + else -> error("unexpected") + } + + val overrides + get() = + when (event) { + is TeamUpdate -> emptyList() + else -> error("unexpected") + } + + fun entry() = + "clics" to + DumpFileCommand( + name = "clics-archive", + waitTime = waitTime, + ) + + val entryProperty = + "clics" to + DumpFileCommand( + name = "clics-archive", + waitTime = waitTime, + ) + + fun callChain() = + StressOptions() + .iterations(20 * stressTestMultiplierSqrt) + .invocationsPerIteration(1_000 * stressTestMultiplierSqrt) + .commonConfiguration() + .customize(isStressTest) + .check(this::class) +} diff --git a/core/src/test/resources/cases/new_codestyle/Foo.input b/core/src/test/resources/cases/new_codestyle/Foo.input new file mode 100644 index 000000000..5ef98cd14 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/Foo.input @@ -0,0 +1,12 @@ +fun foo() { +extensions.configure("kover") { + reports { + total { + html { + htmlDir = + conventionProject.layout.buildDirectory.dir("kover/${project.name}/html") + } + } + } + } + } \ No newline at end of file diff --git a/core/src/test/resources/cases/new_codestyle/Foo.new.output b/core/src/test/resources/cases/new_codestyle/Foo.new.output new file mode 100644 index 000000000..e5a2659e4 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/Foo.new.output @@ -0,0 +1,12 @@ +fun foo() { + extensions.configure("kover") { + reports { + total { + html { + htmlDir = + conventionProject.layout.buildDirectory.dir("kover/${project.name}/html") + } + } + } + } +} diff --git a/core/src/test/resources/cases/new_codestyle/Foo.output b/core/src/test/resources/cases/new_codestyle/Foo.output new file mode 100644 index 000000000..74d46cbcb --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/Foo.output @@ -0,0 +1,11 @@ +fun foo() { + extensions.configure("kover") { + reports { + total { + html { + htmlDir = conventionProject.layout.buildDirectory.dir("kover/${project.name}/html") + } + } + } + } +} diff --git a/core/src/test/resources/cases/new_codestyle/InfixCall.input b/core/src/test/resources/cases/new_codestyle/InfixCall.input new file mode 100644 index 000000000..5bab06767 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/InfixCall.input @@ -0,0 +1,4 @@ +val pair = orgInfo.id to OverrideOrganizations.Override( + fullName = substituteRaw(fullName), + displayName = substituteRaw(displayName), +) diff --git a/core/src/test/resources/cases/new_codestyle/InfixCall.new.output b/core/src/test/resources/cases/new_codestyle/InfixCall.new.output new file mode 100644 index 000000000..5bab06767 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/InfixCall.new.output @@ -0,0 +1,4 @@ +val pair = orgInfo.id to OverrideOrganizations.Override( + fullName = substituteRaw(fullName), + displayName = substituteRaw(displayName), +) diff --git a/core/src/test/resources/cases/new_codestyle/InfixCall.output b/core/src/test/resources/cases/new_codestyle/InfixCall.output new file mode 100644 index 000000000..d872a0fe9 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/InfixCall.output @@ -0,0 +1,6 @@ +val pair = + orgInfo.id to + OverrideOrganizations.Override( + fullName = substituteRaw(fullName), + displayName = substituteRaw(displayName), + ) diff --git a/core/src/test/resources/cases/new_codestyle/InfixCallWithType.input b/core/src/test/resources/cases/new_codestyle/InfixCallWithType.input new file mode 100644 index 000000000..82149bb35 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/InfixCallWithType.input @@ -0,0 +1,4 @@ +val pair: Pair = orgInfo.id to OverrideOrganizations.Override( + fullName = substituteRaw(fullName), + displayName = substituteRaw(displayName), +) \ No newline at end of file diff --git a/core/src/test/resources/cases/new_codestyle/InfixCallWithType.new.output b/core/src/test/resources/cases/new_codestyle/InfixCallWithType.new.output new file mode 100644 index 000000000..77e5a277d --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/InfixCallWithType.new.output @@ -0,0 +1,5 @@ +val pair: Pair = + orgInfo.id to OverrideOrganizations.Override( + fullName = substituteRaw(fullName), + displayName = substituteRaw(displayName), + ) diff --git a/core/src/test/resources/cases/new_codestyle/InfixCallWithType.output b/core/src/test/resources/cases/new_codestyle/InfixCallWithType.output new file mode 100644 index 000000000..71dbc4722 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/InfixCallWithType.output @@ -0,0 +1,6 @@ +val pair: Pair = + orgInfo.id to + OverrideOrganizations.Override( + fullName = substituteRaw(fullName), + displayName = substituteRaw(displayName), + ) diff --git a/core/src/test/resources/cases/new_codestyle/InlineObject.input b/core/src/test/resources/cases/new_codestyle/InlineObject.input new file mode 100644 index 000000000..4d84a8b9c --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/InlineObject.input @@ -0,0 +1,8 @@ +class Tests { + fun testNotifications() = runTest { + val coroutine = + object : AVeryVeryVeryVeryVeryVeryLongSupertype(coroutineContext, true, false) { + fun foo(): String = "bar" + } + } +} diff --git a/core/src/test/resources/cases/new_codestyle/InlineObject.new.output b/core/src/test/resources/cases/new_codestyle/InlineObject.new.output new file mode 100644 index 000000000..e56ecc7c1 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/InlineObject.new.output @@ -0,0 +1,11 @@ +class Tests { + fun testNotifications() = runTest { + val coroutine = object : AVeryVeryVeryVeryVeryVeryLongSupertype( + coroutineContext, + true, + false, + ) { + fun foo(): String = "bar" + } + } +} diff --git a/core/src/test/resources/cases/new_codestyle/InlineObject.output b/core/src/test/resources/cases/new_codestyle/InlineObject.output new file mode 100644 index 000000000..772ceaedc --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/InlineObject.output @@ -0,0 +1,8 @@ +class Tests { + fun testNotifications() = runTest { + val coroutine = + object : AVeryVeryVeryVeryVeryVeryLongSupertype(coroutineContext, true, false) { + fun foo(): String = "bar" + } + } +} diff --git a/core/src/test/resources/cases/new_codestyle/Interfaces.input b/core/src/test/resources/cases/new_codestyle/Interfaces.input new file mode 100644 index 000000000..a1c567aec --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/Interfaces.input @@ -0,0 +1,7 @@ +object ClicsArchiveCommand : + Runnable, + Closeable, + KoinComponent, + AutoCloseable, + Comparable, + Iterable {} diff --git a/core/src/test/resources/cases/new_codestyle/Interfaces.new.output b/core/src/test/resources/cases/new_codestyle/Interfaces.new.output new file mode 100644 index 000000000..a1c567aec --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/Interfaces.new.output @@ -0,0 +1,7 @@ +object ClicsArchiveCommand : + Runnable, + Closeable, + KoinComponent, + AutoCloseable, + Comparable, + Iterable {} diff --git a/core/src/test/resources/cases/new_codestyle/Interfaces.output b/core/src/test/resources/cases/new_codestyle/Interfaces.output new file mode 100644 index 000000000..a1c567aec --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/Interfaces.output @@ -0,0 +1,7 @@ +object ClicsArchiveCommand : + Runnable, + Closeable, + KoinComponent, + AutoCloseable, + Comparable, + Iterable {} diff --git a/core/src/test/resources/cases/new_codestyle/LineBreaksBetweenClassMembers.input b/core/src/test/resources/cases/new_codestyle/LineBreaksBetweenClassMembers.input new file mode 100644 index 000000000..98a4171f8 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/LineBreaksBetweenClassMembers.input @@ -0,0 +1,11 @@ +internal abstract class AbstractTimeSource { + abstract fun currentTimeMillis(): Long + abstract fun nanoTime(): Long + abstract fun wrapTask(block: Runnable): Runnable + abstract fun trackTask() + abstract fun unTrackTask() + abstract fun registerTimeLoopThread() + abstract fun unregisterTimeLoopThread() + abstract fun parkNanos(blocker: Any, nanos: Long) // should return immediately when nanos <= 0 + abstract fun unpark(thread: Thread) +} \ No newline at end of file diff --git a/core/src/test/resources/cases/new_codestyle/LineBreaksBetweenClassMembers.new.output b/core/src/test/resources/cases/new_codestyle/LineBreaksBetweenClassMembers.new.output new file mode 100644 index 000000000..d54c39d61 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/LineBreaksBetweenClassMembers.new.output @@ -0,0 +1,11 @@ +internal abstract class AbstractTimeSource { + abstract fun currentTimeMillis(): Long + abstract fun nanoTime(): Long + abstract fun wrapTask(block: Runnable): Runnable + abstract fun trackTask() + abstract fun unTrackTask() + abstract fun registerTimeLoopThread() + abstract fun unregisterTimeLoopThread() + abstract fun parkNanos(blocker: Any, nanos: Long) // should return immediately when nanos <= 0 + abstract fun unpark(thread: Thread) +} diff --git a/core/src/test/resources/cases/new_codestyle/LineBreaksBetweenClassMembers.output b/core/src/test/resources/cases/new_codestyle/LineBreaksBetweenClassMembers.output new file mode 100644 index 000000000..cd8a39b9b --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/LineBreaksBetweenClassMembers.output @@ -0,0 +1,19 @@ +internal abstract class AbstractTimeSource { + abstract fun currentTimeMillis(): Long + + abstract fun nanoTime(): Long + + abstract fun wrapTask(block: Runnable): Runnable + + abstract fun trackTask() + + abstract fun unTrackTask() + + abstract fun registerTimeLoopThread() + + abstract fun unregisterTimeLoopThread() + + abstract fun parkNanos(blocker: Any, nanos: Long) // should return immediately when nanos <= 0 + + abstract fun unpark(thread: Thread) +} diff --git a/core/src/test/resources/cases/new_codestyle/LongCallChain.input b/core/src/test/resources/cases/new_codestyle/LongCallChain.input new file mode 100644 index 000000000..24902f043 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/LongCallChain.input @@ -0,0 +1,10 @@ +fun f() { + val testDataDir: Path = Path.of("") + .absolute() + .parent + .parent + .resolve("tests") + .resolve("testData") + .resolve("loaders") + .relativeTo(Path.of("").absolute()) +} diff --git a/core/src/test/resources/cases/new_codestyle/LongCallChain.new.output b/core/src/test/resources/cases/new_codestyle/LongCallChain.new.output new file mode 100644 index 000000000..a0881eaa2 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/LongCallChain.new.output @@ -0,0 +1,10 @@ +fun f() { + val testDataDir: Path = Path.of("") + .absolute() + .parent + .parent + .resolve("tests") + .resolve("testData") + .resolve("loaders") + .relativeTo(Path.of("").absolute()) +} diff --git a/core/src/test/resources/cases/new_codestyle/LongCallChain.output b/core/src/test/resources/cases/new_codestyle/LongCallChain.output new file mode 100644 index 000000000..a8e30eba6 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/LongCallChain.output @@ -0,0 +1,11 @@ +fun f() { + val testDataDir: Path = + Path.of("") + .absolute() + .parent + .parent + .resolve("tests") + .resolve("testData") + .resolve("loaders") + .relativeTo(Path.of("").absolute()) +} diff --git a/core/src/test/resources/cases/new_codestyle/LongCallChainWithLineBreak.input b/core/src/test/resources/cases/new_codestyle/LongCallChainWithLineBreak.input new file mode 100644 index 000000000..16b395b7a --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/LongCallChainWithLineBreak.input @@ -0,0 +1,10 @@ +fun f() { + val testDataDir: Path = Path.ofAVeryVeryVeryLongCallNameHereSoThatWeHaveToBreakAfterAssignment("") + .absolute() + .parent + .parent + .resolve("tests") + .resolve("testData") + .resolve("loaders") + .relativeTo(Path.of("").absolute()) +} diff --git a/core/src/test/resources/cases/new_codestyle/LongCallChainWithLineBreak.new.output b/core/src/test/resources/cases/new_codestyle/LongCallChainWithLineBreak.new.output new file mode 100644 index 000000000..e219df107 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/LongCallChainWithLineBreak.new.output @@ -0,0 +1,11 @@ +fun f() { + val testDataDir: Path = + Path.ofAVeryVeryVeryLongCallNameHereSoThatWeHaveToBreakAfterAssignment("") + .absolute() + .parent + .parent + .resolve("tests") + .resolve("testData") + .resolve("loaders") + .relativeTo(Path.of("").absolute()) +} diff --git a/core/src/test/resources/cases/new_codestyle/LongCallChainWithLineBreak.output b/core/src/test/resources/cases/new_codestyle/LongCallChainWithLineBreak.output new file mode 100644 index 000000000..f9f3969b9 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/LongCallChainWithLineBreak.output @@ -0,0 +1,11 @@ +fun f() { + val testDataDir: Path = + Path.ofAVeryVeryVeryLongCallNameHereSoThatWeHaveToBreakAfterAssignment("") + .absolute() + .parent + .parent + .resolve("tests") + .resolve("testData") + .resolve("loaders") + .relativeTo(Path.of("").absolute()) +} diff --git a/core/src/test/resources/cases/new_codestyle/PropertyAccessors.input b/core/src/test/resources/cases/new_codestyle/PropertyAccessors.input new file mode 100644 index 000000000..d5852fc87 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/PropertyAccessors.input @@ -0,0 +1,22 @@ +class Properties { + public final override val context: CoroutineContext = parentContext + this + + public override val coroutineContext: CoroutineContext + get() = context + + override val isActive: Boolean + get() = super.isActive + + val isStopped: Boolean + get() = super.isStopped + + public override val propertyWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName: CoroutineContext + get() = context + + public override val property: CoroutineContext + get() = aVeryVeryVeryVeryVeryVeryVeryVeryVeryLongInitializer + + val propertyAccessorWIthBlockBody: String get() = synchronized(this) { + "foo" + } +} \ No newline at end of file diff --git a/core/src/test/resources/cases/new_codestyle/PropertyAccessors.new.output b/core/src/test/resources/cases/new_codestyle/PropertyAccessors.new.output new file mode 100644 index 000000000..908a76bff --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/PropertyAccessors.new.output @@ -0,0 +1,20 @@ +class Properties { + public final override val context: CoroutineContext = parentContext + this + + public override val coroutineContext: CoroutineContext get() = context + + override val isActive: Boolean get() = super.isActive + + val isStopped: Boolean get() = super.isStopped + + public override val propertyWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName: CoroutineContext + get() = context + + public override val property: CoroutineContext + get() = aVeryVeryVeryVeryVeryVeryVeryVeryVeryLongInitializer + + val propertyAccessorWIthBlockBody: String + get() = synchronized(this) { + "foo" + } +} diff --git a/core/src/test/resources/cases/new_codestyle/PropertyAccessors.output b/core/src/test/resources/cases/new_codestyle/PropertyAccessors.output new file mode 100644 index 000000000..a034af97b --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/PropertyAccessors.output @@ -0,0 +1,24 @@ +class Properties { + public final override val context: CoroutineContext = parentContext + this + + public override val coroutineContext: CoroutineContext + get() = context + + override val isActive: Boolean + get() = super.isActive + + val isStopped: Boolean + get() = super.isStopped + + public override val propertyWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName: CoroutineContext + get() = context + + public override val property: CoroutineContext + get() = aVeryVeryVeryVeryVeryVeryVeryVeryVeryLongInitializer + + val propertyAccessorWIthBlockBody: String + get() = + synchronized(this) { + "foo" + } +} diff --git a/core/src/test/resources/cases/new_codestyle/PropertyAccessorsWithModifiers.input b/core/src/test/resources/cases/new_codestyle/PropertyAccessorsWithModifiers.input new file mode 100644 index 000000000..3a2fa9978 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/PropertyAccessorsWithModifiers.input @@ -0,0 +1,8 @@ +class Properties { + protected var slots: Array? = null + private set + + protected var nCollectors = 0 + protected get + private set +} \ No newline at end of file diff --git a/core/src/test/resources/cases/new_codestyle/PropertyAccessorsWithModifiers.new.output b/core/src/test/resources/cases/new_codestyle/PropertyAccessorsWithModifiers.new.output new file mode 100644 index 000000000..33a60c24e --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/PropertyAccessorsWithModifiers.new.output @@ -0,0 +1,8 @@ +class Properties { + protected var slots: Array? = null + private set + + protected var nCollectors = 0 + protected get + private set +} diff --git a/core/src/test/resources/cases/new_codestyle/PropertyAccessorsWithModifiers.output b/core/src/test/resources/cases/new_codestyle/PropertyAccessorsWithModifiers.output new file mode 100644 index 000000000..ae837e858 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/PropertyAccessorsWithModifiers.output @@ -0,0 +1,8 @@ +class Properties { + protected var slots: Array? = null + private set + + protected var nCollectors = 0 + protected get + private set +} diff --git a/core/src/test/resources/cases/new_codestyle/ScopingFunctionWithTypeArgument.input b/core/src/test/resources/cases/new_codestyle/ScopingFunctionWithTypeArgument.input new file mode 100644 index 000000000..ea40e4139 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/ScopingFunctionWithTypeArgument.input @@ -0,0 +1,7 @@ +val d = + async { + expect(3) + yield() // no effect, parent waiting + finish(4) + throw TestException() + } \ No newline at end of file diff --git a/core/src/test/resources/cases/new_codestyle/ScopingFunctionWithTypeArgument.new.output b/core/src/test/resources/cases/new_codestyle/ScopingFunctionWithTypeArgument.new.output new file mode 100644 index 000000000..0f5dad95e --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/ScopingFunctionWithTypeArgument.new.output @@ -0,0 +1,6 @@ +val d = async { + expect(3) + yield() // no effect, parent waiting + finish(4) + throw TestException() +} diff --git a/core/src/test/resources/cases/new_codestyle/ScopingFunctionWithTypeArgument.output b/core/src/test/resources/cases/new_codestyle/ScopingFunctionWithTypeArgument.output new file mode 100644 index 000000000..41eea3439 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/ScopingFunctionWithTypeArgument.output @@ -0,0 +1,6 @@ +val d = async { + expect(3) + yield() // no effect, parent waiting + finish(4) + throw TestException() +} diff --git a/core/src/test/resources/cases/new_codestyle/Supertype.input b/core/src/test/resources/cases/new_codestyle/Supertype.input new file mode 100644 index 000000000..c3f0563f0 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/Supertype.input @@ -0,0 +1,7 @@ +object ClicsArchiveCommand : + DumpFileCommand( + name = "clics-archive", + help = "Dump CLICS contest archive (zip)", + defaultFileName = "contest-archive.zip", + outputHelp = "Path to new zip file", + ) {} \ No newline at end of file diff --git a/core/src/test/resources/cases/new_codestyle/Supertype.new.output b/core/src/test/resources/cases/new_codestyle/Supertype.new.output new file mode 100644 index 000000000..8b035674d --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/Supertype.new.output @@ -0,0 +1,6 @@ +object ClicsArchiveCommand : DumpFileCommand( + name = "clics-archive", + help = "Dump CLICS contest archive (zip)", + defaultFileName = "contest-archive.zip", + outputHelp = "Path to new zip file", +) {} diff --git a/core/src/test/resources/cases/new_codestyle/Supertype.output b/core/src/test/resources/cases/new_codestyle/Supertype.output new file mode 100644 index 000000000..c2e1d0481 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/Supertype.output @@ -0,0 +1,7 @@ +object ClicsArchiveCommand : + DumpFileCommand( + name = "clics-archive", + help = "Dump CLICS contest archive (zip)", + defaultFileName = "contest-archive.zip", + outputHelp = "Path to new zip file", + ) {} diff --git a/core/src/test/resources/cases/new_codestyle/SupertypeAndInterfaces.input b/core/src/test/resources/cases/new_codestyle/SupertypeAndInterfaces.input new file mode 100644 index 000000000..f4076fcba --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/SupertypeAndInterfaces.input @@ -0,0 +1,7 @@ +object ClicsArchiveCommand : + DumpFileCommand( + name = "clics-archive", + help = "Dump CLICS contest archive (zip)", + defaultFileName = "contest-archive.zip", + outputHelp = "Path to new zip file", + ), Runnable, Closeable, KoinComponent {} \ No newline at end of file diff --git a/core/src/test/resources/cases/new_codestyle/SupertypeAndInterfaces.new.output b/core/src/test/resources/cases/new_codestyle/SupertypeAndInterfaces.new.output new file mode 100644 index 000000000..dbcc162fd --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/SupertypeAndInterfaces.new.output @@ -0,0 +1,6 @@ +object ClicsArchiveCommand : DumpFileCommand( + name = "clics-archive", + help = "Dump CLICS contest archive (zip)", + defaultFileName = "contest-archive.zip", + outputHelp = "Path to new zip file", +), Runnable, Closeable, KoinComponent {} diff --git a/core/src/test/resources/cases/new_codestyle/SupertypeAndInterfaces.output b/core/src/test/resources/cases/new_codestyle/SupertypeAndInterfaces.output new file mode 100644 index 000000000..2c384e9cc --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/SupertypeAndInterfaces.output @@ -0,0 +1,10 @@ +object ClicsArchiveCommand : + DumpFileCommand( + name = "clics-archive", + help = "Dump CLICS contest archive (zip)", + defaultFileName = "contest-archive.zip", + outputHelp = "Path to new zip file", + ), + Runnable, + Closeable, + KoinComponent {} diff --git a/core/src/test/resources/cases/new_codestyle/TryExpression.input b/core/src/test/resources/cases/new_codestyle/TryExpression.input new file mode 100644 index 000000000..ccac581cd --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/TryExpression.input @@ -0,0 +1,12 @@ +fun f(block: () -> T): T? { + var thrown: Throwable? = null + val result = try { + block() + } catch (t: Throwable) { + thrown = t + null + } finally { + cleanup() + } + return result +} \ No newline at end of file diff --git a/core/src/test/resources/cases/new_codestyle/TryExpression.new.output b/core/src/test/resources/cases/new_codestyle/TryExpression.new.output new file mode 100644 index 000000000..4ae7d4f97 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/TryExpression.new.output @@ -0,0 +1,12 @@ +fun f(block: () -> T): T? { + var thrown: Throwable? = null + val result = try { + block() + } catch (t: Throwable) { + thrown = t + null + } finally { + cleanup() + } + return result +} diff --git a/core/src/test/resources/cases/new_codestyle/TryExpression.output b/core/src/test/resources/cases/new_codestyle/TryExpression.output new file mode 100644 index 000000000..c2f70b394 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/TryExpression.output @@ -0,0 +1,13 @@ +fun f(block: () -> T): T? { + var thrown: Throwable? = null + val result = + try { + block() + } catch (t: Throwable) { + thrown = t + null + } finally { + cleanup() + } + return result +} diff --git a/core/src/test/resources/cases/new_codestyle/When.input b/core/src/test/resources/cases/new_codestyle/When.input new file mode 100644 index 000000000..248cd5de3 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/When.input @@ -0,0 +1,10 @@ +fun f() { + val teamsAffected = when (val event = state.lastEvent) { + is CommentaryMessagesUpdate -> emptyList() + is InfoUpdate -> info.teams.keys.toList() + is RunUpdate -> { + lastSubmissionTime = maxOf(lastSubmissionTime, event.newInfo.time) + runsByTeamId.applyEvent(state) + } + } +} \ No newline at end of file diff --git a/core/src/test/resources/cases/new_codestyle/When.new.output b/core/src/test/resources/cases/new_codestyle/When.new.output new file mode 100644 index 000000000..92c9b684e --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/When.new.output @@ -0,0 +1,10 @@ +fun f() { + val teamsAffected = when (val event = state.lastEvent) { + is CommentaryMessagesUpdate -> emptyList() + is InfoUpdate -> info.teams.keys.toList() + is RunUpdate -> { + lastSubmissionTime = maxOf(lastSubmissionTime, event.newInfo.time) + runsByTeamId.applyEvent(state) + } + } +} diff --git a/core/src/test/resources/cases/new_codestyle/When.output b/core/src/test/resources/cases/new_codestyle/When.output new file mode 100644 index 000000000..bce045a7c --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/When.output @@ -0,0 +1,11 @@ +fun f() { + val teamsAffected = + when (val event = state.lastEvent) { + is CommentaryMessagesUpdate -> emptyList() + is InfoUpdate -> info.teams.keys.toList() + is RunUpdate -> { + lastSubmissionTime = maxOf(lastSubmissionTime, event.newInfo.time) + runsByTeamId.applyEvent(state) + } + } +} diff --git a/core/src/test/resources/cases/new_codestyle/WhenBooleanSubject.input b/core/src/test/resources/cases/new_codestyle/WhenBooleanSubject.input new file mode 100644 index 000000000..b4cb2e1ef --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/WhenBooleanSubject.input @@ -0,0 +1,6 @@ +fun f() { + when (aaaaa == null || aaaaa.bbbbb[0] == null || aaaaa.bbbbb[0].cc == null || aaaaa.bbbbb[0].dddd == null) { + true -> println("yes") + false -> println("no") + } +} diff --git a/core/src/test/resources/cases/new_codestyle/WhenBooleanSubject.new.output b/core/src/test/resources/cases/new_codestyle/WhenBooleanSubject.new.output new file mode 100644 index 000000000..fa2602b77 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/WhenBooleanSubject.new.output @@ -0,0 +1,9 @@ +fun f() { + when (aaaaa == null || + aaaaa.bbbbb[0] == null || + aaaaa.bbbbb[0].cc == null || + aaaaa.bbbbb[0].dddd == null) { + true -> println("yes") + false -> println("no") + } +} diff --git a/core/src/test/resources/cases/new_codestyle/WhenBooleanSubject.output b/core/src/test/resources/cases/new_codestyle/WhenBooleanSubject.output new file mode 100644 index 000000000..dd4770601 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/WhenBooleanSubject.output @@ -0,0 +1,9 @@ +fun f() { + when (aaaaa == null || + aaaaa.bbbbb[0] == null || + aaaaa.bbbbb[0].cc == null || + aaaaa.bbbbb[0].dddd == null) { + true -> println("yes") + false -> println("no") + } +} diff --git a/core/src/test/resources/cases/new_codestyle/WhenGuard.input b/core/src/test/resources/cases/new_codestyle/WhenGuard.input new file mode 100644 index 000000000..acd34de52 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/WhenGuard.input @@ -0,0 +1,7 @@ +fun f(state: State) { + when (state) { + is Loaded if state.firstCondition && state.secondCondition && state.thirdCondition && state.fourthCondition -> handle() + is Empty if state.isReallyEmpty -> Unit + else -> Unit + } +} diff --git a/core/src/test/resources/cases/new_codestyle/WhenGuard.new.output b/core/src/test/resources/cases/new_codestyle/WhenGuard.new.output new file mode 100644 index 000000000..6972b2de6 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/WhenGuard.new.output @@ -0,0 +1,10 @@ +fun f(state: State) { + when (state) { + is Loaded if state.firstCondition && + state.secondCondition && + state.thirdCondition && + state.fourthCondition -> handle() + is Empty if state.isReallyEmpty -> Unit + else -> Unit + } +} diff --git a/core/src/test/resources/cases/new_codestyle/WhenGuard.output b/core/src/test/resources/cases/new_codestyle/WhenGuard.output new file mode 100644 index 000000000..7291cbd73 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/WhenGuard.output @@ -0,0 +1,10 @@ +fun f(state: State) { + when (state) { + is Loaded if state.firstCondition && + state.secondCondition && + state.thirdCondition && + state.fourthCondition -> handle() + is Empty if state.isReallyEmpty -> Unit + else -> Unit + } +} diff --git a/core/src/test/resources/cases/new_codestyle/WhenSubjectWrapping.input b/core/src/test/resources/cases/new_codestyle/WhenSubjectWrapping.input new file mode 100644 index 000000000..a426e2837 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/WhenSubjectWrapping.input @@ -0,0 +1,6 @@ +fun f() { + when (computeSomething(firstArgument, secondArgument, thirdArgument, fourthArgument, fifthArgument)) { + 0 -> println("zero") + else -> println("other") + } +} diff --git a/core/src/test/resources/cases/new_codestyle/WhenSubjectWrapping.new.output b/core/src/test/resources/cases/new_codestyle/WhenSubjectWrapping.new.output new file mode 100644 index 000000000..d2c2fb7a4 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/WhenSubjectWrapping.new.output @@ -0,0 +1,12 @@ +fun f() { + when (computeSomething( + firstArgument, + secondArgument, + thirdArgument, + fourthArgument, + fifthArgument, + )) { + 0 -> println("zero") + else -> println("other") + } +} diff --git a/core/src/test/resources/cases/new_codestyle/WhenSubjectWrapping.output b/core/src/test/resources/cases/new_codestyle/WhenSubjectWrapping.output new file mode 100644 index 000000000..d7624ba81 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/WhenSubjectWrapping.output @@ -0,0 +1,7 @@ +fun f() { + when (computeSomething( + firstArgument, secondArgument, thirdArgument, fourthArgument, fifthArgument)) { + 0 -> println("zero") + else -> println("other") + } +} diff --git a/core/src/test/resources/cases/new_codestyle/WhenWithType.input b/core/src/test/resources/cases/new_codestyle/WhenWithType.input new file mode 100644 index 000000000..1e9a206de --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/WhenWithType.input @@ -0,0 +1,11 @@ +fun f() { + val teamsAffected: List = + when (val event = state.lastEvent) { + is CommentaryMessagesUpdate -> emptyList() + is InfoUpdate -> info.teams.keys.toList() + is RunUpdate -> { + lastSubmissionTime = maxOf(lastSubmissionTime, event.newInfo.time) + runsByTeamId.applyEvent(state) + } + } +} \ No newline at end of file diff --git a/core/src/test/resources/cases/new_codestyle/WhenWithType.new.output b/core/src/test/resources/cases/new_codestyle/WhenWithType.new.output new file mode 100644 index 000000000..5d618419d --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/WhenWithType.new.output @@ -0,0 +1,11 @@ +fun f() { + val teamsAffected: List = + when (val event = state.lastEvent) { + is CommentaryMessagesUpdate -> emptyList() + is InfoUpdate -> info.teams.keys.toList() + is RunUpdate -> { + lastSubmissionTime = maxOf(lastSubmissionTime, event.newInfo.time) + runsByTeamId.applyEvent(state) + } + } +} diff --git a/core/src/test/resources/cases/new_codestyle/WhenWithType.output b/core/src/test/resources/cases/new_codestyle/WhenWithType.output new file mode 100644 index 000000000..25d34f16d --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/WhenWithType.output @@ -0,0 +1,11 @@ +fun f() { + val teamsAffected: List = + when (val event = state.lastEvent) { + is CommentaryMessagesUpdate -> emptyList() + is InfoUpdate -> info.teams.keys.toList() + is RunUpdate -> { + lastSubmissionTime = maxOf(lastSubmissionTime, event.newInfo.time) + runsByTeamId.applyEvent(state) + } + } +} diff --git a/core/src/test/resources/cases/new_codestyle/WhileCallCondition.input b/core/src/test/resources/cases/new_codestyle/WhileCallCondition.input new file mode 100644 index 000000000..d1c585260 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/WhileCallCondition.input @@ -0,0 +1,5 @@ +fun f() { + while (computeSomething(firstArgument, secondArgument, thirdArgument, fourthArgument, fifthArgument)) { + println() + } +} diff --git a/core/src/test/resources/cases/new_codestyle/WhileCallCondition.new.output b/core/src/test/resources/cases/new_codestyle/WhileCallCondition.new.output new file mode 100644 index 000000000..5f15a97ce --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/WhileCallCondition.new.output @@ -0,0 +1,11 @@ +fun f() { + while (computeSomething( + firstArgument, + secondArgument, + thirdArgument, + fourthArgument, + fifthArgument, + )) { + println() + } +} diff --git a/core/src/test/resources/cases/new_codestyle/WhileCallCondition.output b/core/src/test/resources/cases/new_codestyle/WhileCallCondition.output new file mode 100644 index 000000000..e8be5a5b0 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/WhileCallCondition.output @@ -0,0 +1,6 @@ +fun f() { + while (computeSomething( + firstArgument, secondArgument, thirdArgument, fourthArgument, fifthArgument)) { + println() + } +} diff --git a/core/src/test/resources/cases/new_codestyle/WhileConditionWrapping.input b/core/src/test/resources/cases/new_codestyle/WhileConditionWrapping.input new file mode 100644 index 000000000..905ef09c5 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/WhileConditionWrapping.input @@ -0,0 +1,5 @@ +fun f() { + while (aaaaa == null || aaaaa.bbbbb[0] == null || aaaaa.bbbbb[0].cc == null || aaaaa.bbbbb[0].dddd == null) { + println() + } +} diff --git a/core/src/test/resources/cases/new_codestyle/WhileConditionWrapping.new.output b/core/src/test/resources/cases/new_codestyle/WhileConditionWrapping.new.output new file mode 100644 index 000000000..faaba1b7b --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/WhileConditionWrapping.new.output @@ -0,0 +1,8 @@ +fun f() { + while (aaaaa == null || + aaaaa.bbbbb[0] == null || + aaaaa.bbbbb[0].cc == null || + aaaaa.bbbbb[0].dddd == null) { + println() + } +} diff --git a/core/src/test/resources/cases/new_codestyle/WhileConditionWrapping.output b/core/src/test/resources/cases/new_codestyle/WhileConditionWrapping.output new file mode 100644 index 000000000..093694764 --- /dev/null +++ b/core/src/test/resources/cases/new_codestyle/WhileConditionWrapping.output @@ -0,0 +1,8 @@ +fun f() { + while (aaaaa == null || + aaaaa.bbbbb[0] == null || + aaaaa.bbbbb[0].cc == null || + aaaaa.bbbbb[0].dddd == null) { + println() + } +}