diff options
Diffstat (limited to 'core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt')
-rw-r--r-- | core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt | 426 |
1 files changed, 259 insertions, 167 deletions
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 358fe50..898b70e 100644 --- a/core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt +++ b/core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt @@ -70,6 +70,7 @@ import org.jetbrains.kotlin.psi.KtFunctionType import org.jetbrains.kotlin.psi.KtIfExpression import org.jetbrains.kotlin.psi.KtImportDirective import org.jetbrains.kotlin.psi.KtImportList +import org.jetbrains.kotlin.psi.KtIntersectionType import org.jetbrains.kotlin.psi.KtIsExpression import org.jetbrains.kotlin.psi.KtLabelReferenceExpression import org.jetbrains.kotlin.psi.KtLabeledExpression @@ -231,18 +232,27 @@ class KotlinInputAstVisitor( } } + /** Example: `A & B`, */ + override fun visitIntersectionType(type: KtIntersectionType) { + builder.sync(type) + + // TODO(strulovich): Should this have the same indentation behaviour as `x && y`? + visit(type.getLeftTypeRef()) + builder.space() + builder.token("&") + builder.space() + visit(type.getRightTypeRef()) + } + /** Example `<Int, String>` in `List<Int, String>` */ override fun visitTypeArgumentList(typeArgumentList: KtTypeArgumentList) { builder.sync(typeArgumentList) - builder.block(ZERO) { - builder.token("<") - builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO) - builder.block(ZERO) { - emitParameterLikeList( - typeArgumentList.arguments, typeArgumentList.trailingComma != null, wrapInBlock = true) - } - } - builder.token(">") + visitEachCommaSeparated( + typeArgumentList.arguments, + typeArgumentList.trailingComma != null, + prefix = "<", + postfix = ">", + ) } override fun visitTypeProjection(typeProjection: KtTypeProjection) { @@ -456,7 +466,18 @@ class KotlinInputAstVisitor( visit(selectorExpression) } } - receiver is KtWhenExpression || receiver is KtStringTemplateExpression -> { + receiver is KtStringTemplateExpression -> { + val isMultiline = receiver.text.contains('\n') + builder.block(if (isMultiline) expressionBreakIndent else ZERO) { + visit(receiver) + if (isMultiline) { + builder.forcedBreak() + } + builder.token(expression.operationSign.value) + visit(expression.selectorExpression) + } + } + receiver is KtWhenExpression -> { builder.block(ZERO) { visit(receiver) builder.token(expression.operationSign.value) @@ -522,6 +543,7 @@ class KotlinInputAstVisitor( } val argsIndentElse = if (index == parts.size - 1) 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( @@ -531,6 +553,7 @@ class KotlinInputAstVisitor( selectorExpression.lambdaArguments, argumentsIndent = Indent.If.make(nameTag, expressionBreakIndent, argsIndentElse), lambdaIndent = Indent.If.make(nameTag, ZERO, lambdaIndentElse), + negativeLambdaIndent = Indent.If.make(nameTag, ZERO, negativeLambdaIndentElse), ) } } @@ -714,82 +737,107 @@ class KotlinInputAstVisitor( typeArgumentList, valueArgumentList, lambdaArguments, - lambdaIndent = ZERO) + ) } } - /** Examples `foo<T>(a, b)`, `foo(a)`, `boo()`, `super(a)` */ + /** + * Examples `foo<T>(a, b)`, `foo(a)`, `boo()`, `super(a)` + * + * @param lambdaIndent how to indent [lambdaArguments], if present + * @param negativeLambdaIndent the negative indentation of [lambdaIndent] + */ private fun visitCallElement( callee: KtExpression?, typeArgumentList: KtTypeArgumentList?, argumentList: KtValueArgumentList?, lambdaArguments: List<KtLambdaArgument>, argumentsIndent: Indent = expressionBreakIndent, - lambdaIndent: Indent = ZERO + lambdaIndent: Indent = ZERO, + negativeLambdaIndent: Indent = ZERO, ) { - builder.block(ZERO) { - visit(callee) - val arguments = argumentList?.arguments.orEmpty() - builder.block(argumentsIndent) { visit(typeArgumentList) } - builder.block(argumentsIndent) { - if (argumentList != null) { - builder.token("(") - } - if (arguments.isNotEmpty()) { - if (isGoogleStyle) { - visit(argumentList) - val first = arguments.first() - if (arguments.size != 1 || - first?.isNamed() != false || - first.getArgumentExpression() !is KtLambdaExpression) { - builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakNegativeIndent) - } - } else { - builder.block(ZERO) { visit(argumentList) } + // Apply the lambda indent to the callee, type args, value args, and the lambda. + // This is undone for the first three by the negative lambda indent. + // This way they're in one block, and breaks in the argument list cause a break in the lambda. + builder.block(lambdaIndent) { + + // Used to keep track of whether or not we need to indent the lambda + // This is based on if there is a break in the argument list + var brokeBeforeBrace: BreakTag? = null + + builder.block(negativeLambdaIndent) { + visit(callee) + builder.block(argumentsIndent) { + builder.block(ZERO) { visit(typeArgumentList) } + if (argumentList != null) { + brokeBeforeBrace = visitValueArgumentListInternal(argumentList) } } - if (argumentList != null) { - builder.token(")") - } } - val hasTrailingComma = argumentList?.trailingComma != null if (lambdaArguments.isNotEmpty()) { builder.space() - builder.block(lambdaIndent) { - lambdaArguments.forEach { - visitArgumentInternal(it, forceBreakLambdaBody = hasTrailingComma) - } - } + visitArgumentInternal( + lambdaArguments.single(), + wrapInBlock = false, + brokeBeforeBrace = brokeBeforeBrace, + ) } } } /** Example (`1, "hi"`) in a function call */ override fun visitValueArgumentList(list: KtValueArgumentList) { + visitValueArgumentListInternal(list) + } + + /** + * Example (`1, "hi"`) in a function call + * + * @return a [BreakTag] which can tell you if a break was taken, but only when the list doesn't + * terminate in a negative closing indent. See [visitEachCommaSeparated] for examples. + */ + private fun visitValueArgumentListInternal(list: KtValueArgumentList): BreakTag? { builder.sync(list) + val arguments = list.arguments val isSingleUnnamedLambda = arguments.size == 1 && arguments.first().getArgumentExpression() is KtLambdaExpression && arguments.first().getArgumentName() == null + val hasTrailingComma = list.trailingComma != null + + val wrapInBlock: Boolean + val breakBeforePostfix: Boolean + val leadingBreak: Boolean + val breakAfterPrefix: Boolean + if (isSingleUnnamedLambda) { - builder.block(expressionBreakNegativeIndent) { - visit(arguments.first()) - if (list.trailingComma != null) { - builder.token(",") - } - } + wrapInBlock = true + breakBeforePostfix = false + leadingBreak = arguments.isNotEmpty() && hasTrailingComma + breakAfterPrefix = false } else { - // Break before args. - builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO) - emitParameterLikeList( - list.arguments, list.trailingComma != null, wrapInBlock = !isGoogleStyle) - } + wrapInBlock = !isGoogleStyle + breakBeforePostfix = isGoogleStyle && arguments.isNotEmpty() + leadingBreak = arguments.isNotEmpty() + breakAfterPrefix = arguments.isNotEmpty() + } + + return visitEachCommaSeparated( + list.arguments, + hasTrailingComma, + wrapInBlock = wrapInBlock, + breakBeforePostfix = breakBeforePostfix, + leadingBreak = leadingBreak, + prefix = "(", + postfix = ")", + breakAfterPrefix = breakAfterPrefix, + ) } /** Example `{ 1 + 1 }` (as lambda) or `{ (x, y) -> x + y }` */ override fun visitLambdaExpression(lambdaExpression: KtLambdaExpression) { - visitLambdaExpressionInternal(lambdaExpression, brokeBeforeBrace = null, forceBreakBody = false) + visitLambdaExpressionInternal(lambdaExpression, brokeBeforeBrace = null) } /** @@ -806,21 +854,10 @@ class KotlinInputAstVisitor( * car() * } * ``` - * @param forceBreakBody if true, forces the lambda to be multi-line. Useful for call expressions - * where it would look weird for the lambda to be on one-line. For example, here we avoid - * one-lining `{ x = 0 }` since the parameters have a trailing comma: - * ``` - * foo.bar( - * trailingComma, - * ) { - * x = 0 - * } - * ``` */ private fun visitLambdaExpressionInternal( lambdaExpression: KtLambdaExpression, brokeBeforeBrace: BreakTag?, - forceBreakBody: Boolean, ) { builder.sync(lambdaExpression) @@ -855,9 +892,7 @@ class KotlinInputAstVisitor( if (hasParams || hasArrow) { builder.space() - builder.block(bracePlusExpressionIndent) { - forEachCommaSeparated(valueParams) { it.accept(this) } - } + builder.block(bracePlusExpressionIndent) { visitEachCommaSeparated(valueParams) } builder.block(bracePlusBlockIndent) { if (lambdaExpression.functionLiteral.valueParameterList?.trailingComma != null) { builder.token(",") @@ -870,10 +905,6 @@ class KotlinInputAstVisitor( builder.breakOp(Doc.FillMode.UNIFIED, "", bracePlusZeroIndent) } - if (forceBreakBody) { - builder.forcedBreak() - } - if (hasStatements) { builder.breakOp(Doc.FillMode.UNIFIED, " ", bracePlusBlockIndent) builder.block(bracePlusBlockIndent) { @@ -931,32 +962,11 @@ class KotlinInputAstVisitor( /** e.g., `a: Int, b: Int, c: Int` in `fun foo(a: Int, b: Int, c: Int) { ... }`. */ override fun visitParameterList(list: KtParameterList) { - emitParameterLikeList(list.parameters, list.trailingComma != null, wrapInBlock = false) - } - - /** - * Emit a list of elements that look like function parameters or arguments, e.g., `a, b, c` in - * `foo(a, b, c)` - */ - private fun <T : PsiElement> emitParameterLikeList( - list: List<T>?, - hasTrailingComma: Boolean, - wrapInBlock: Boolean - ) { - if (list.isNullOrEmpty()) { - return - } - - forEachCommaSeparated(list, hasTrailingComma, wrapInBlock, trailingBreak = isGoogleStyle) { - visit(it) - } - if (hasTrailingComma) { - builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakNegativeIndent) - } + visitEachCommaSeparated(list.parameters, list.trailingComma != null, wrapInBlock = false) } /** - * Call `function` for each element in `list`, with comma (,) tokens inbetween. + * Visit each element in [list], with comma (,) tokens in-between. * * Example: * ``` @@ -973,6 +983,15 @@ class KotlinInputAstVisitor( * 5 * ``` * + * Optionally include a prefix and postfix: + * ``` + * ( + * a, + * b, + * c, + * ) + * ``` + * * @param hasTrailingComma if true, each element is placed on its own line (even if they could've * fit in a single line), and a trailing comma is emitted. * @@ -981,88 +1000,160 @@ class KotlinInputAstVisitor( * a, * b, * ``` + * + * @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. + * @param leadingBreak if true, break before the first element. + * @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 + * block. + * @param breakBeforePostfix if true, place a break after the last element. Redundant when + * [hasTrailingComma] is true. + * @return a [BreakTag] which can tell you if a break was taken, but only when the list doesn't + * terminate in a negative closing indent. + * + * Example 1, this returns a BreakTag which tells you a break wasn't taken: + * ``` + * (arg1, arg2) + * ``` + * + * Example 2, this returns a BreakTag which tells you a break WAS taken: + * ``` + * ( + * arg1, + * arg2) + * ``` + * + * Example 3, this returns null: + * ``` + * ( + * arg1, + * arg2, + * ) + * ``` + * + * Example 4, this also returns null (similar to example 2, but Google style): + * ``` + * ( + * arg1, + * arg2 + * ) + * ``` */ - private fun <T> forEachCommaSeparated( - list: Iterable<T>, + private fun visitEachCommaSeparated( + list: Iterable<PsiElement>, hasTrailingComma: Boolean = false, wrapInBlock: Boolean = true, - trailingBreak: Boolean = false, - function: (T) -> Unit - ) { - if (hasTrailingComma) { - builder.block(ZERO) { - builder.forcedBreak() - for (value in list) { - function(value) - builder.token(",") - builder.forcedBreak() - } + leadingBreak: Boolean = true, + prefix: String? = null, + postfix: String? = null, + breakAfterPrefix: Boolean = true, + breakBeforePostfix: Boolean = isGoogleStyle, + ): BreakTag? { + val breakAfterLastElement = hasTrailingComma || (postfix != null && breakBeforePostfix) + val nameTag = if (breakAfterLastElement) null else genSym() + + if (prefix != null) { + builder.token(prefix) + if (breakAfterPrefix) { + builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO, Optional.ofNullable(nameTag)) } - return } - builder.block(ZERO, isEnabled = wrapInBlock) { + val breakType = if (hasTrailingComma) Doc.FillMode.FORCED else Doc.FillMode.UNIFIED + fun emitComma() { + builder.token(",") + builder.breakOp(breakType, " ", ZERO) + } + + val indent = if (leadingBreak) ZERO else expressionBreakNegativeIndent + builder.block(indent, isEnabled = wrapInBlock) { + if (leadingBreak) { + builder.breakOp(breakType, "", ZERO) + } + var first = true - builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO) for (value in list) { - if (!first) { - builder.token(",") - builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO) - } + if (!first) emitComma() first = false + visit(value) + } - function(value) + if (hasTrailingComma) { + emitComma() } } - if (trailingBreak) { - builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakNegativeIndent) + + if (breakAfterLastElement) { + // a negative closing indent places the postfix to the left of the elements + // see examples 2 and 4 in the docstring + builder.breakOp(breakType, "", expressionBreakNegativeIndent) } + + if (postfix != null) { + if (breakAfterLastElement) { + // Indent trailing comments to the same depth as list items. We really have to fight + // googlejavaformat here for some reason. + builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO) + builder.block(expressionBreakNegativeIndent) { + builder.breakOp(breakType, "", ZERO) + builder.token(postfix, expressionBreakIndent) + } + } else { + builder.token(postfix) + } + } + + return nameTag } /** Example `a` in `foo(a)`, or `*a`, or `limit = 50` */ override fun visitArgument(argument: KtValueArgument) { - visitArgumentInternal(argument, forceBreakLambdaBody = false) + visitArgumentInternal( + argument, + wrapInBlock = true, + brokeBeforeBrace = null, + ) } /** * The internal version of [visitArgument]. * - * @param forceBreakLambdaBody if true (and [argument] is of type [KtLambdaExpression]), forces - * the lambda to be multi-line. See documentation of [visitLambdaExpressionInternal] for an - * example. + * @param wrapInBlock if true places the argument expression in a block. */ private fun visitArgumentInternal( argument: KtValueArgument, - forceBreakLambdaBody: Boolean, + wrapInBlock: Boolean, + brokeBeforeBrace: BreakTag?, ) { builder.sync(argument) val hasArgName = argument.getArgumentName() != null val isLambda = argument.getArgumentExpression() is KtLambdaExpression - builder.block(ZERO) { - if (hasArgName) { - visit(argument.getArgumentName()) + if (hasArgName) { + visit(argument.getArgumentName()) + builder.space() + builder.token("=") + if (isLambda) { builder.space() - builder.token("=") - if (isLambda) { - builder.space() - } } - builder.block(if (hasArgName && !isLambda) expressionBreakIndent else ZERO) { - if (hasArgName && !isLambda) { - builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO) - } - if (argument.isSpread) { - builder.token("*") - } - if (isLambda) { - visitLambdaExpressionInternal( - argument.getArgumentExpression() as KtLambdaExpression, - brokeBeforeBrace = null, - forceBreakBody = forceBreakLambdaBody, - ) - } else { - visit(argument.getArgumentExpression()) - } + } + val indent = if (hasArgName && !isLambda) expressionBreakIndent else ZERO + builder.block(indent, isEnabled = wrapInBlock) { + if (hasArgName && !isLambda) { + builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO) + } + if (argument.isSpread) { + builder.token("*") + } + if (isLambda) { + visitLambdaExpressionInternal( + argument.getArgumentExpression() as KtLambdaExpression, + brokeBeforeBrace = brokeBeforeBrace, + ) + } else { + visit(argument.getArgumentExpression()) } } } @@ -1357,11 +1448,7 @@ class KotlinInputAstVisitor( else -> throw AssertionError(expr) } - visitLambdaExpressionInternal( - lambdaExpression, - brokeBeforeBrace = breakToExpr, - forceBreakBody = false, - ) + visitLambdaExpressionInternal(lambdaExpression, brokeBeforeBrace = breakToExpr) } override fun visitClassOrObject(classOrObject: KtClassOrObject) { @@ -1510,7 +1597,7 @@ class KotlinInputAstVisitor( call.typeArgumentList, call.valueArgumentList, call.lambdaArguments, - lambdaIndent = ZERO) + ) } } @@ -1744,7 +1831,7 @@ class KotlinInputAstVisitor( override fun visitSuperTypeList(list: KtSuperTypeList) { builder.sync(list) - builder.block(expressionBreakIndent) { forEachCommaSeparated(list.entries) { visit(it) } } + builder.block(expressionBreakIndent) { visitEachCommaSeparated(list.entries) } } override fun visitSuperTypeCallEntry(call: KtSuperTypeCallEntry) { @@ -1891,7 +1978,7 @@ class KotlinInputAstVisitor( builder.token("[") builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakIndent) builder.block(expressionBreakIndent) { - emitParameterLikeList( + visitEachCommaSeparated( expression.indexExpressions, expression.trailingComma != null, wrapInBlock = true) } } @@ -1911,7 +1998,7 @@ class KotlinInputAstVisitor( builder.token("(") builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakIndent) builder.block(expressionBreakIndent) { - emitParameterLikeList( + visitEachCommaSeparated( destructuringDeclaration.entries, hasTrailingComma, wrapInBlock = true) } } @@ -1973,7 +2060,7 @@ class KotlinInputAstVisitor( // Break before args. builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakIndent) builder.block(expressionBreakIndent) { - emitParameterLikeList(list.parameters, list.trailingComma != null, wrapInBlock = true) + visitEachCommaSeparated(list.parameters, list.trailingComma != null, wrapInBlock = true) } } builder.token(">") @@ -1998,7 +2085,7 @@ class KotlinInputAstVisitor( builder.token("where") builder.space() builder.sync(list) - forEachCommaSeparated(list.constraints) { visit(it) } + visitEachCommaSeparated(list.constraints) } /** Example `T : Foo` */ @@ -2137,10 +2224,16 @@ class KotlinInputAstVisitor( builder.token(".") } builder.block(expressionBreakIndent) { - builder.token("(") - visit(type.parameterList) + val parameterList = type.parameterList + if (parameterList != null) { + visitEachCommaSeparated( + parameterList.parameters, + prefix = "(", + postfix = ")", + hasTrailingComma = parameterList.trailingComma != null, + ) + } } - builder.token(")") builder.space() builder.token("->") builder.space() @@ -2192,15 +2285,14 @@ class KotlinInputAstVisitor( */ override fun visitCollectionLiteralExpression(expression: KtCollectionLiteralExpression) { builder.sync(expression) - builder.block(ZERO) { - builder.token("[") - builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakIndent) - builder.block(expressionBreakIndent) { - emitParameterLikeList( - expression.getInnerExpressions(), expression.trailingComma != null, wrapInBlock = true) - } + builder.block(expressionBreakIndent) { + visitEachCommaSeparated( + expression.getInnerExpressions(), + expression.trailingComma != null, + prefix = "[", + postfix = "]", + wrapInBlock = true) } - builder.token("]") } override fun visitTryExpression(expression: KtTryExpression) { |