From 582c58a7a243a2637a343f112e2dd9c1df8b900e Mon Sep 17 00:00:00 2001
From: Jordan Demeulenaere
Date: Thu, 23 Feb 2023 10:29:01 +0100
Subject: Revert "Revert "Merge tag 'v0.42' into update""
This reverts commit 0c522a74a45613a7831f8abf423e83a38d409886.
Test: m ktfmt
Bug: 266197805
Change-Id: I87b9b9b9e70acabfd4a26a628cb8f7cf5a1f956e
---
core/pom.xml | 2 +-
core/src/main/java/com/facebook/ktfmt/cli/Main.kt | 9 +-
.../main/java/com/facebook/ktfmt/cli/ParsedArgs.kt | 16 +-
.../java/com/facebook/ktfmt/format/Formatter.kt | 8 +-
.../java/com/facebook/ktfmt/format/KotlinInput.kt | 32 +-
.../facebook/ktfmt/format/KotlinInputAstVisitor.kt | 426 +-
.../java/com/facebook/ktfmt/format/KotlinTok.kt | 6 +-
.../ktfmt/format/RedundantElementRemover.kt | 10 +-
.../java/com/facebook/ktfmt/kdoc/CommentType.kt | 64 +
.../java/com/facebook/ktfmt/kdoc/FormattingTask.kt | 58 +
.../com/facebook/ktfmt/kdoc/KDocCommentsHelper.kt | 14 +-
.../java/com/facebook/ktfmt/kdoc/KDocFormatter.kt | 305 +-
.../facebook/ktfmt/kdoc/KDocFormattingOptions.kt | 134 +
.../main/java/com/facebook/ktfmt/kdoc/Paragraph.kt | 617 +++
.../java/com/facebook/ktfmt/kdoc/ParagraphList.kt | 28 +
.../facebook/ktfmt/kdoc/ParagraphListBuilder.kt | 830 ++++
.../src/main/java/com/facebook/ktfmt/kdoc/Table.kt | 270 ++
.../main/java/com/facebook/ktfmt/kdoc/Utilities.kt | 329 ++
.../test/java/com/facebook/ktfmt/cli/MainTest.kt | 41 +-
.../java/com/facebook/ktfmt/cli/ParsedArgsTest.kt | 81 +-
.../com/facebook/ktfmt/format/FormatterTest.kt | 1494 +++++--
.../ktfmt/format/GoogleStyleFormatterKtTest.kt | 383 +-
.../java/com/facebook/ktfmt/kdoc/DokkaVerifier.kt | 197 +
.../com/facebook/ktfmt/kdoc/KDocFormatterTest.kt | 4706 +++++++++++++++++++-
.../java/com/facebook/ktfmt/kdoc/UtilitiesTest.kt | 105 +
pom.xml | 2 +-
version.txt | 2 +-
website/package-lock.json | 66 +-
28 files changed, 9244 insertions(+), 991 deletions(-)
create mode 100644 core/src/main/java/com/facebook/ktfmt/kdoc/CommentType.kt
create mode 100644 core/src/main/java/com/facebook/ktfmt/kdoc/FormattingTask.kt
create mode 100644 core/src/main/java/com/facebook/ktfmt/kdoc/KDocFormattingOptions.kt
create mode 100644 core/src/main/java/com/facebook/ktfmt/kdoc/Paragraph.kt
create mode 100644 core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphList.kt
create mode 100644 core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphListBuilder.kt
create mode 100644 core/src/main/java/com/facebook/ktfmt/kdoc/Table.kt
create mode 100644 core/src/main/java/com/facebook/ktfmt/kdoc/Utilities.kt
create mode 100644 core/src/test/java/com/facebook/ktfmt/kdoc/DokkaVerifier.kt
create mode 100644 core/src/test/java/com/facebook/ktfmt/kdoc/UtilitiesTest.kt
diff --git a/core/pom.xml b/core/pom.xml
index c7e8df4..765c8f1 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -11,7 +11,7 @@
com.facebook
ktfmt-parent
- 0.39
+ 0.42
diff --git a/core/src/main/java/com/facebook/ktfmt/cli/Main.kt b/core/src/main/java/com/facebook/ktfmt/cli/Main.kt
index 4c965bb..0a08944 100644
--- a/core/src/main/java/com/facebook/ktfmt/cli/Main.kt
+++ b/core/src/main/java/com/facebook/ktfmt/cli/Main.kt
@@ -70,7 +70,7 @@ class Main(
fun run(): Int {
if (parsedArgs.fileNames.isEmpty()) {
err.println(
- "Usage: ktfmt [--dropbox-style | --google-style | --kotlinlang-style] [--dry-run] [--set-exit-if-changed] File1.kt File2.kt ...")
+ "Usage: ktfmt [--dropbox-style | --google-style | --kotlinlang-style] [--dry-run] [--set-exit-if-changed] [--stdin-name=] File1.kt File2.kt ...")
err.println("Or: ktfmt @file")
return 1
}
@@ -82,6 +82,9 @@ class Main(
} catch (e: Exception) {
1
}
+ } else if (parsedArgs.stdinName != null) {
+ err.println("Error: --stdin-name can only be used with stdin")
+ return 1
}
val files: List
@@ -120,7 +123,7 @@ class Main(
* @return true iff input is valid and already formatted.
*/
private fun format(file: File?): Boolean {
- val fileName = file?.toString() ?: ""
+ val fileName = file?.toString() ?: parsedArgs.stdinName ?: ""
try {
val code = file?.readText() ?: BufferedReader(InputStreamReader(input)).readText()
val formattedCode = Formatter.format(parsedArgs.formattingOptions, code)
@@ -130,7 +133,7 @@ class Main(
if (file == null) {
if (parsedArgs.dryRun) {
if (!alreadyFormatted) {
- out.println("")
+ out.println(fileName)
}
} else {
out.print(formattedCode)
diff --git a/core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt b/core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt
index 503cb89..4c66efd 100644
--- a/core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt
+++ b/core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt
@@ -33,6 +33,8 @@ data class ParsedArgs(
/** Return exit code 1 if any formatting changes are detected. */
val setExitIfChanged: Boolean,
+ /** File name to report when formating code from stdin */
+ val stdinName: String?,
) {
companion object {
@@ -50,6 +52,7 @@ data class ParsedArgs(
var formattingOptions = FormattingOptions()
var dryRun = false
var setExitIfChanged = false
+ var stdinName: String? = null
for (arg in args) {
when {
@@ -58,12 +61,23 @@ data class ParsedArgs(
arg == "--kotlinlang-style" -> formattingOptions = Formatter.KOTLINLANG_FORMAT
arg == "--dry-run" || arg == "-n" -> dryRun = true
arg == "--set-exit-if-changed" -> setExitIfChanged = true
+ arg.startsWith("--stdin-name") -> stdinName = parseKeyValueArg(err, "--stdin-name", arg)
arg.startsWith("--") -> err.println("Unexpected option: $arg")
arg.startsWith("@") -> err.println("Unexpected option: $arg")
else -> fileNames.add(arg)
}
}
- return ParsedArgs(fileNames, formattingOptions, dryRun, setExitIfChanged)
+
+ return ParsedArgs(fileNames, formattingOptions, dryRun, setExitIfChanged, stdinName)
+ }
+
+ private fun parseKeyValueArg(err: PrintStream, key: String, arg: String): String? {
+ val parts = arg.split('=', limit = 2)
+ if (parts[0] != key || parts.size != 2) {
+ err.println("Found option '${arg}', expected '${key}='")
+ return null
+ }
+ return parts[1]
}
}
}
diff --git a/core/src/main/java/com/facebook/ktfmt/format/Formatter.kt b/core/src/main/java/com/facebook/ktfmt/format/Formatter.kt
index 1fa90f7..4cdb589 100644
--- a/core/src/main/java/com/facebook/ktfmt/format/Formatter.kt
+++ b/core/src/main/java/com/facebook/ktfmt/format/Formatter.kt
@@ -88,13 +88,7 @@ object Formatter {
val lfCode = StringUtilRt.convertLineSeparators(kotlinCode)
val sortedImports = sortedAndDistinctImports(lfCode)
- val pretty = prettyPrint(sortedImports, options, "\n")
- val noRedundantElements =
- try {
- dropRedundantElements(pretty, options)
- } catch (e: ParseError) {
- throw IllegalStateException("Failed to re-parse code after pretty printing:\n $pretty", e)
- }
+ val noRedundantElements = dropRedundantElements(sortedImports, options)
val prettyCode =
prettyPrint(noRedundantElements, options, Newlines.guessLineSeparator(kotlinCode)!!)
return if (shebang.isNotEmpty()) shebang + "\n" + prettyCode else prettyCode
diff --git a/core/src/main/java/com/facebook/ktfmt/format/KotlinInput.kt b/core/src/main/java/com/facebook/ktfmt/format/KotlinInput.kt
index 52b01a4..f1efe5d 100644
--- a/core/src/main/java/com/facebook/ktfmt/format/KotlinInput.kt
+++ b/core/src/main/java/com/facebook/ktfmt/format/KotlinInput.kt
@@ -28,7 +28,6 @@ import com.google.googlejavaformat.Input
import com.google.googlejavaformat.Newlines
import com.google.googlejavaformat.java.FormatterException
import com.google.googlejavaformat.java.JavaOutput
-import java.util.LinkedHashMap
import org.jetbrains.kotlin.com.intellij.openapi.util.text.StringUtil
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtFile
@@ -55,16 +54,7 @@ class KotlinInput(private val text: String, file: KtFile) : Input() {
val toks = buildToks(file, text)
positionToColumnMap = makePositionToColumnMap(toks)
tokens = buildTokens(toks)
- val tokenLocations = ImmutableRangeMap.builder()
- for (token in tokens) {
- val end = JavaOutput.endTok(token)
- var upper = end.position
- if (end.text.isNotEmpty()) {
- upper += end.length() - 1
- }
- tokenLocations.put(Range.closed(JavaOutput.startTok(token).position, upper), token)
- }
- positionTokenMap = tokenLocations.build()
+ positionTokenMap = buildTokenPositionsMap(tokens)
// adjust kN for EOF
kToToken = arrayOfNulls(kN + 1)
@@ -134,13 +124,8 @@ class KotlinInput(private val text: String, file: KtFile) : Input() {
enclosed.iterator().next().tok.index, getLast(enclosed).getTok().getIndex() + 1)
}
- private fun makePositionToColumnMap(toks: List): ImmutableMap {
- val builder = LinkedHashMap()
- for (tok in toks) {
- builder.put(tok.position, tok.column)
- }
- return ImmutableMap.copyOf(builder)
- }
+ private fun makePositionToColumnMap(toks: List) =
+ ImmutableMap.copyOf(toks.map { it.position to it.column }.toMap())
private fun buildToks(file: KtFile, fileText: String): ImmutableList {
val tokenizer = Tokenizer(fileText, file)
@@ -207,6 +192,17 @@ class KotlinInput(private val text: String, file: KtFile) : Input() {
return tokens.build()
}
+ private fun buildTokenPositionsMap(tokens: ImmutableList): ImmutableRangeMap {
+ val tokenLocations = ImmutableRangeMap.builder()
+ for (token in tokens) {
+ val end = JavaOutput.endTok(token)
+ val endPosition = end.position + (if (end.text.isNotEmpty()) end.length() - 1 else 0)
+ tokenLocations.put(Range.closed(JavaOutput.startTok(token).position, endPosition), token)
+ }
+
+ return tokenLocations.build()
+ }
+
private fun isParamComment(tok: Tok): Boolean {
return tok.isSlashStarComment && tok.text.matches("/\\*[A-Za-z0-9\\s_\\-]+=\\s*\\*/".toRegex())
}
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 `` in `List` */
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(a, b)`, `foo(a)`, `boo()`, `super(a)` */
+ /**
+ * Examples `foo(a, b)`, `foo(a)`, `boo()`, `super(a)`
+ *
+ * @param lambdaIndent how to indent [lambdaArguments], if present
+ * @param negativeLambdaIndent the negative indentation of [lambdaIndent]
+ */
private fun visitCallElement(
callee: KtExpression?,
typeArgumentList: KtTypeArgumentList?,
argumentList: KtValueArgumentList?,
lambdaArguments: List,
argumentsIndent: Indent = expressionBreakIndent,
- lambdaIndent: Indent = ZERO
+ 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 emitParameterLikeList(
- list: List?,
- 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 forEachCommaSeparated(
- list: Iterable,
+ private fun visitEachCommaSeparated(
+ list: Iterable,
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) {
diff --git a/core/src/main/java/com/facebook/ktfmt/format/KotlinTok.kt b/core/src/main/java/com/facebook/ktfmt/format/KotlinTok.kt
index d03a7e4..5db843e 100644
--- a/core/src/main/java/com/facebook/ktfmt/format/KotlinTok.kt
+++ b/core/src/main/java/com/facebook/ktfmt/format/KotlinTok.kt
@@ -26,7 +26,7 @@ class KotlinTok(
private val originalText: String,
private val text: String,
private val position: Int,
- private val columnI: Int,
+ private val column: Int,
val isToken: Boolean,
private val kind: KtToken
) : Input.Tok {
@@ -41,7 +41,7 @@ class KotlinTok(
override fun getPosition(): Int = position
- override fun getColumn(): Int = columnI
+ override fun getColumn(): Int = column
override fun isNewline(): Boolean = Newlines.isNewline(text)
@@ -60,7 +60,7 @@ class KotlinTok(
.add("index", index)
.add("text", text)
.add("position", position)
- .add("columnI", columnI)
+ .add("column", column)
.add("isToken", isToken)
.toString()
}
diff --git a/core/src/main/java/com/facebook/ktfmt/format/RedundantElementRemover.kt b/core/src/main/java/com/facebook/ktfmt/format/RedundantElementRemover.kt
index a073438..1c090fe 100644
--- a/core/src/main/java/com/facebook/ktfmt/format/RedundantElementRemover.kt
+++ b/core/src/main/java/com/facebook/ktfmt/format/RedundantElementRemover.kt
@@ -17,6 +17,7 @@
package com.facebook.ktfmt.format
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
+import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace
import org.jetbrains.kotlin.kdoc.psi.impl.KDocImpl
import org.jetbrains.kotlin.psi.KtImportList
import org.jetbrains.kotlin.psi.KtPackageDirective
@@ -65,9 +66,16 @@ object RedundantElementRemover {
redundantImportDetector.getRedundantImportElements()
for (element in elementsToRemove.sortedByDescending(PsiElement::endOffset)) {
- result.replace(element.startOffset, element.endOffset, "")
+ // Don't insert extra newlines when the semicolon is already a line terminator
+ val replacement = if (element.nextSibling.containsNewline()) "" else "\n"
+ result.replace(element.startOffset, element.endOffset, replacement)
}
return result.toString()
}
+
+ private fun PsiElement?.containsNewline(): Boolean {
+ if (this !is PsiWhiteSpace) return false
+ return this.text.contains('\n')
+ }
}
diff --git a/core/src/main/java/com/facebook/ktfmt/kdoc/CommentType.kt b/core/src/main/java/com/facebook/ktfmt/kdoc/CommentType.kt
new file mode 100644
index 0000000..aba6176
--- /dev/null
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/CommentType.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) Tor Norbye.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.facebook.ktfmt.kdoc
+
+enum class CommentType(
+ /** The opening string of the comment. */
+ val prefix: String,
+ /** The closing string of the comment. */
+ val suffix: String,
+ /** For multi line comments, the prefix at each comment line after the first one. */
+ val linePrefix: String
+) {
+ KDOC("/**", "*/", " * "),
+ BLOCK("/*", "*/", ""),
+ LINE("//", "", "// ");
+
+ /**
+ * The number of characters needed to fit a comment on a line: the prefix, suffix and a single
+ * space padding inside these.
+ */
+ fun singleLineOverhead(): Int {
+ return prefix.length + suffix.length + 1 + if (suffix.isEmpty()) 0 else 1
+ }
+
+ /**
+ * The number of characters required in addition to the line comment for each line in a multi line
+ * comment.
+ */
+ fun lineOverhead(): Int {
+ return linePrefix.length
+ }
+}
+
+fun String.isKDocComment(): Boolean = startsWith("/**")
+
+fun String.isBlockComment(): Boolean = startsWith("/*") && !startsWith("/**")
+
+fun String.isLineComment(): Boolean = startsWith("//")
+
+fun String.commentType(): CommentType {
+ return if (isKDocComment()) {
+ CommentType.KDOC
+ } else if (isBlockComment()) {
+ CommentType.BLOCK
+ } else if (isLineComment()) {
+ CommentType.LINE
+ } else {
+ error("Not a comment: $this")
+ }
+}
diff --git a/core/src/main/java/com/facebook/ktfmt/kdoc/FormattingTask.kt b/core/src/main/java/com/facebook/ktfmt/kdoc/FormattingTask.kt
new file mode 100644
index 0000000..4f195db
--- /dev/null
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/FormattingTask.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) Tor Norbye.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.facebook.ktfmt.kdoc
+
+class FormattingTask(
+ /** Options to format with */
+ var options: KDocFormattingOptions,
+
+ /** The original comment to be formatted */
+ var comment: String,
+
+ /**
+ * The initial indentation on the first line of the KDoc. The reformatted comment will prefix
+ * each subsequent line with this string.
+ */
+ var initialIndent: String,
+
+ /**
+ * Indent to use after the first line.
+ *
+ * This is useful when the comment starts the end of an existing code line. For example,
+ * something like this:
+ * ```
+ * if (foo.bar.baz()) { // This comment started at column 25
+ * // but the second and subsequent lines are indented 8 spaces
+ * // ...
+ * ```
+ *
+ * (This doesn't matter much for KDoc comments, since the formatter will always push these into
+ * their own lines so the indents will match, but for line and block comments it can matter.)
+ */
+ var secondaryIndent: String = initialIndent,
+
+ /**
+ * Optional list of parameters associated with this doc; if set, and if
+ * [KDocFormattingOptions.orderDocTags] is set, parameter doc tags will be sorted to match this
+ * order. (The intent is for the tool invoking KDocFormatter to pass in the parameter names in
+ * signature order here.)
+ */
+ var orderedParameterNames: List = emptyList(),
+
+ /** The type of comment being formatted. */
+ val type: CommentType = comment.commentType()
+)
diff --git a/core/src/main/java/com/facebook/ktfmt/kdoc/KDocCommentsHelper.kt b/core/src/main/java/com/facebook/ktfmt/kdoc/KDocCommentsHelper.kt
index a63a332..3cf3c64 100644
--- a/core/src/main/java/com/facebook/ktfmt/kdoc/KDocCommentsHelper.kt
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/KDocCommentsHelper.kt
@@ -29,8 +29,16 @@ import java.util.ArrayList
import java.util.regex.Pattern
/** `KDocCommentsHelper` extends [CommentsHelper] to rewrite KDoc comments. */
-class KDocCommentsHelper(private val lineSeparator: String, private val maxLineLength: Int) :
- CommentsHelper {
+class KDocCommentsHelper(private val lineSeparator: String, maxLineLength: Int) : CommentsHelper {
+
+ private val kdocFormatter =
+ KDocFormatter(
+ KDocFormattingOptions(maxLineLength, maxLineLength).also {
+ it.allowParamBrackets = true // TODO Do we want this?
+ it.convertMarkup = false
+ it.nestedListIndent = 4
+ it.optimal = false // Use greedy line breaking for predictability.
+ })
override fun rewrite(tok: Tok, maxWidth: Int, column0: Int): String {
if (!tok.isComment) {
@@ -38,7 +46,7 @@ class KDocCommentsHelper(private val lineSeparator: String, private val maxLineL
}
var text = tok.originalText
if (tok.isJavadocComment) {
- text = KDocFormatter.formatKDoc(text, column0, maxLineLength)
+ text = kdocFormatter.reformatComment(text, " ".repeat(column0))
}
val lines = ArrayList()
val it = Newlines.lineIterator(text)
diff --git a/core/src/main/java/com/facebook/ktfmt/kdoc/KDocFormatter.kt b/core/src/main/java/com/facebook/ktfmt/kdoc/KDocFormatter.kt
index 8c0af07..d44561c 100644
--- a/core/src/main/java/com/facebook/ktfmt/kdoc/KDocFormatter.kt
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/KDocFormatter.kt
@@ -1,211 +1,160 @@
/*
- * Copyright 2016 Google Inc.
+ * Copyright (c) Tor Norbye.
*
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-
-/*
- * This was copied from https://github.com/google/google-java-format and modified extensively to
- * work for Kotlin formatting
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*/
package com.facebook.ktfmt.kdoc
-import com.facebook.ktfmt.kdoc.KDocToken.Type.BEGIN_KDOC
-import com.facebook.ktfmt.kdoc.KDocToken.Type.BLANK_LINE
-import com.facebook.ktfmt.kdoc.KDocToken.Type.CODE
-import com.facebook.ktfmt.kdoc.KDocToken.Type.CODE_BLOCK_MARKER
-import com.facebook.ktfmt.kdoc.KDocToken.Type.CODE_CLOSE_TAG
-import com.facebook.ktfmt.kdoc.KDocToken.Type.CODE_OPEN_TAG
-import com.facebook.ktfmt.kdoc.KDocToken.Type.END_KDOC
-import com.facebook.ktfmt.kdoc.KDocToken.Type.LIST_ITEM_OPEN_TAG
-import com.facebook.ktfmt.kdoc.KDocToken.Type.LITERAL
-import com.facebook.ktfmt.kdoc.KDocToken.Type.MARKDOWN_LINK
-import com.facebook.ktfmt.kdoc.KDocToken.Type.PRE_CLOSE_TAG
-import com.facebook.ktfmt.kdoc.KDocToken.Type.PRE_OPEN_TAG
-import com.facebook.ktfmt.kdoc.KDocToken.Type.TABLE_CLOSE_TAG
-import com.facebook.ktfmt.kdoc.KDocToken.Type.TABLE_OPEN_TAG
-import com.facebook.ktfmt.kdoc.KDocToken.Type.TAG
-import com.facebook.ktfmt.kdoc.KDocToken.Type.WHITESPACE
-import java.util.regex.Pattern.compile
-import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType
-import org.jetbrains.kotlin.kdoc.lexer.KDocLexer
-import org.jetbrains.kotlin.kdoc.lexer.KDocTokens
-import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE
-
-/**
- * Entry point for formatting KDoc.
- *
- * This stateless class reads tokens from the stateful lexer and translates them to "requests" and
- * "writes" to the stateful writer. It also munges tokens into "standardized" forms. Finally, it
- * performs postprocessing to convert the written KDoc to a one-liner if possible or to leave a
- * single blank line if it's empty.
- */
-object KDocFormatter {
+import kotlin.math.min
- private val ONE_CONTENT_LINE_PATTERN = compile(" */[*][*]\n *[*] (.*)\n *[*]/")
+/** Formatter which can reformat KDoc comments. */
+class KDocFormatter(private val options: KDocFormattingOptions) {
+ /** Reformats the [comment], which follows the given [initialIndent] string. */
+ fun reformatComment(comment: String, initialIndent: String): String {
+ return reformatComment(FormattingTask(options, comment, initialIndent))
+ }
- private val NUMBERED_LIST_PATTERN = "[0-9]+\\.".toRegex()
+ fun reformatComment(task: FormattingTask): String {
+ val indent = task.secondaryIndent
+ val indentSize = getIndentSize(indent, options)
+ val firstIndentSize = getIndentSize(task.initialIndent, options)
+ val comment = task.comment
+ val lineComment = comment.isLineComment()
+ val blockComment = comment.isBlockComment()
+ val paragraphs = ParagraphListBuilder(comment, options, task).scan(indentSize)
+ val commentType = task.type
+ val lineSeparator = "\n$indent${commentType.linePrefix}"
+ val prefix = commentType.prefix
- /**
- * Formats the given Javadoc comment, which must start with ∕✱✱ and end with ✱∕. The output will
- * start and end with the same characters.
- */
- fun formatKDoc(input: String, blockIndent: Int, maxLineLength: Int): String {
- val escapedInput = Escaping.escapeKDoc(input)
- val kDocLexer = KDocLexer()
- kDocLexer.start(escapedInput)
- val tokens = mutableListOf()
- var previousType: IElementType? = null
- while (kDocLexer.tokenType != null) {
- val tokenType = kDocLexer.tokenType
- val tokenText =
- with(kDocLexer.tokenText) {
- if (previousType == KDocTokens.LEADING_ASTERISK && first() == ' ') substring(1)
- else this
- }
+ // Collapse single line? If alternate is turned on, use the opposite of the
+ // setting
+ val collapseLine = options.collapseSingleLine.let { if (options.alternate) !it else it }
+ if (paragraphs.isSingleParagraph() && collapseLine && !lineComment) {
+ // Does the text fit on a single line?
+ val trimmed = paragraphs.firstOrNull()?.text?.trim() ?: ""
+ // Subtract out space for "/** " and " */" and the indent:
+ val width =
+ min(
+ options.maxLineWidth - firstIndentSize - commentType.singleLineOverhead(),
+ options.maxCommentWidth)
+ val suffix = if (commentType.suffix.isEmpty()) "" else " ${commentType.suffix}"
+ if (trimmed.length <= width) {
+ return "$prefix $trimmed$suffix"
+ }
+ if (indentSize < firstIndentSize) {
+ val nextLineWidth =
+ min(
+ options.maxLineWidth - indentSize - commentType.singleLineOverhead(),
+ options.maxCommentWidth)
+ if (trimmed.length <= nextLineWidth) {
+ return "$prefix $trimmed$suffix"
+ }
+ }
+ }
- processToken(tokenType, tokens, tokenText, previousType)
+ val sb = StringBuilder()
- previousType = tokenType
- kDocLexer.advance()
+ sb.append(prefix)
+ if (lineComment) {
+ sb.append(' ')
+ } else {
+ sb.append(lineSeparator)
}
- val result = render(tokens, blockIndent, maxLineLength)
- return makeSingleLineIfPossible(blockIndent, result, maxLineLength)
- }
- private fun processToken(
- tokenType: IElementType?,
- tokens: MutableList,
- tokenText: String,
- previousType: IElementType?
- ) {
- when (tokenType) {
- KDocTokens.START -> tokens.add(KDocToken(BEGIN_KDOC, tokenText))
- KDocTokens.END -> tokens.add(KDocToken(END_KDOC, tokenText))
- KDocTokens.LEADING_ASTERISK -> Unit // Ignore, no need to output anything
- KDocTokens.TAG_NAME -> tokens.add(KDocToken(TAG, tokenText))
- KDocTokens.CODE_BLOCK_TEXT -> tokens.add(KDocToken(CODE, tokenText))
- KDocTokens.MARKDOWN_INLINE_LINK,
- KDocTokens.MARKDOWN_LINK -> {
- tokens.add(KDocToken(MARKDOWN_LINK, tokenText))
+ for (paragraph in paragraphs) {
+ if (paragraph.separate) {
+ // Remove trailing spaces which can happen when we have a paragraph
+ // separator
+ stripTrailingSpaces(lineComment, sb)
+ sb.append(lineSeparator)
}
- KDocTokens.MARKDOWN_ESCAPED_CHAR,
- KDocTokens.TEXT -> {
- var first = true
- for (word in tokenizeKdocText(tokenText)) {
- if (word.first().isWhitespace()) {
- tokens.add(KDocToken(WHITESPACE, " "))
- continue
- }
- if (first) {
- if (word == "-" || word == "*" || word.matches(NUMBERED_LIST_PATTERN)) {
- tokens.add(KDocToken(LIST_ITEM_OPEN_TAG, ""))
- }
- first = false
- }
- // If the KDoc is malformed (e.g. unclosed code block) KDocLexer doesn't report an
- // END_KDOC properly. We want to recover in such cases
- if (word == "*/") {
- tokens.add(KDocToken(END_KDOC, word))
- } else if (word.startsWith("```")) {
- tokens.add(KDocToken(CODE_BLOCK_MARKER, word))
+ val text = paragraph.text
+ if (paragraph.preformatted || paragraph.table) {
+ sb.append(text)
+ // Remove trailing spaces which can happen when we have an empty line in a
+ // preformatted paragraph.
+ stripTrailingSpaces(lineComment, sb)
+ sb.append(lineSeparator)
+ continue
+ }
+
+ val lineWithoutIndent = options.maxLineWidth - commentType.lineOverhead()
+ val quoteAdjustment = if (paragraph.quoted) 2 else 0
+ val maxLineWidth =
+ min(options.maxCommentWidth, lineWithoutIndent - indentSize) - quoteAdjustment
+ val firstMaxLineWidth =
+ if (sb.indexOf('\n') == -1) {
+ min(options.maxCommentWidth, lineWithoutIndent - firstIndentSize) - quoteAdjustment
} else {
- tokens.add(KDocToken(LITERAL, word))
+ maxLineWidth
}
+
+ val lines = paragraph.reflow(firstMaxLineWidth, maxLineWidth)
+ var first = true
+ val hangingIndent = paragraph.hangingIndent
+ for (line in lines) {
+ sb.append(paragraph.indent)
+ if (first && !paragraph.continuation) {
+ first = false
+ } else {
+ sb.append(hangingIndent)
}
- }
- WHITE_SPACE -> {
- if (previousType == KDocTokens.LEADING_ASTERISK || tokenText.count { it == '\n' } >= 2) {
- tokens.add(KDocToken(BLANK_LINE, ""))
+ if (paragraph.quoted) {
+ sb.append("> ")
+ }
+ if (line.isEmpty()) {
+ // Remove trailing spaces which can happen when we have a paragraph
+ // separator
+ stripTrailingSpaces(lineComment, sb)
} else {
- tokens.add(KDocToken(WHITESPACE, " "))
+ sb.append(line)
}
+ sb.append(lineSeparator)
}
- else -> throw RuntimeException("Unexpected: $tokenType")
}
- }
- private fun render(input: List, blockIndent: Int, maxLineLength: Int): String {
- val output = KDocWriter(blockIndent, maxLineLength)
- for (token in input) {
- when (token.type) {
- BEGIN_KDOC -> output.writeBeginJavadoc()
- END_KDOC -> {
- output.writeEndJavadoc()
- return Escaping.unescapeKDoc(output.toString())
- }
- LIST_ITEM_OPEN_TAG -> output.writeListItemOpen(token)
- PRE_OPEN_TAG -> output.writePreOpen(token)
- PRE_CLOSE_TAG -> output.writePreClose(token)
- CODE_OPEN_TAG -> output.writeCodeOpen(token)
- CODE_CLOSE_TAG -> output.writeCodeClose(token)
- TABLE_OPEN_TAG -> output.writeTableOpen(token)
- TABLE_CLOSE_TAG -> output.writeTableClose(token)
- TAG -> output.writeTag(token)
- CODE -> output.writeCodeLine(token)
- CODE_BLOCK_MARKER -> output.writeExplicitCodeBlockMarker(token)
- BLANK_LINE -> output.requestBlankLine()
- WHITESPACE -> output.requestWhitespace()
- LITERAL -> output.writeLiteral(token)
- MARKDOWN_LINK -> output.writeMarkdownLink(token)
- else -> throw AssertionError(token.type)
+ if (!lineComment) {
+ if (sb.endsWith("* ")) {
+ sb.setLength(sb.length - 2)
}
+ sb.append("*/")
+ } else if (sb.endsWith(lineSeparator)) {
+ @Suppress("ReturnValueIgnored") sb.removeSuffix(lineSeparator)
}
- throw AssertionError()
- }
- /**
- * Returns the given string or a one-line version of it (e.g., "∕✱✱ Tests for foos. ✱∕") if it
- * fits on one line.
- */
- private fun makeSingleLineIfPossible(
- blockIndent: Int,
- input: String,
- maxLineLength: Int
- ): String {
- val oneLinerContentLength = maxLineLength - "/** */".length - blockIndent
- val matcher = ONE_CONTENT_LINE_PATTERN.matcher(input)
- if (matcher.matches() && matcher.group(1).isEmpty()) {
- return "/** */"
- } else if (matcher.matches() && matcher.group(1).length <= oneLinerContentLength) {
- return "/** " + matcher.group(1) + " */"
+ val formatted =
+ if (lineComment) {
+ sb.trim().removeSuffix("//").trim().toString()
+ } else if (blockComment) {
+ sb.toString().replace(lineSeparator + "\n", "\n\n")
+ } else {
+ sb.toString()
+ }
+
+ val separatorIndex = comment.indexOf('\n')
+ return if (separatorIndex > 0 && comment[separatorIndex - 1] == '\r') {
+ // CRLF separator
+ formatted.replace("\n", "\r\n")
+ } else {
+ formatted
}
- return input
}
- /**
- * tokenizeKdocText splits 's' by whitespace, and returns both whitespace and non-whitespace
- * parts.
- *
- * Multiple adjacent whitespace characters are collapsed into one. Trailing and leading spaces are
- * included in the result.
- *
- * Example: `" one two three "` becomes `[" ", "one", " ", "two", " ", "three", " "]`. See tests
- * for more examples.
- */
- fun tokenizeKdocText(s: String) = sequence {
- if (s.isEmpty()) {
- return@sequence
- }
- var mark = 0
- var inWhitespace = s[0].isWhitespace()
- for (i in 1..s.lastIndex) {
- if (inWhitespace == s[i].isWhitespace()) {
- continue
- }
- val result = if (inWhitespace) " " else s.substring(mark, i)
- inWhitespace = s[i].isWhitespace()
- mark = i
- yield(result)
+ private fun stripTrailingSpaces(lineComment: Boolean, sb: StringBuilder) {
+ if (!lineComment && sb.endsWith("* ")) {
+ sb.setLength(sb.length - 1)
+ } else if (lineComment && sb.endsWith("// ")) {
+ sb.setLength(sb.length - 1)
}
- yield(if (inWhitespace) " " else s.substring(mark, s.length))
}
}
diff --git a/core/src/main/java/com/facebook/ktfmt/kdoc/KDocFormattingOptions.kt b/core/src/main/java/com/facebook/ktfmt/kdoc/KDocFormattingOptions.kt
new file mode 100644
index 0000000..bfe80ea
--- /dev/null
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/KDocFormattingOptions.kt
@@ -0,0 +1,134 @@
+/*
+ * Portions Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Copyright (c) Tor Norbye.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.facebook.ktfmt.kdoc
+
+import kotlin.math.min
+
+/** Options controlling how the [KDocFormatter] will behave. */
+class KDocFormattingOptions(
+ /** Right hand side margin to write lines at. */
+ var maxLineWidth: Int = 72,
+ /**
+ * Limit comment to be at most [maxCommentWidth] characters even if more would fit on the line.
+ */
+ var maxCommentWidth: Int = min(maxLineWidth, 72)
+) {
+ /** Whether to collapse multi-line comments that would fit on a single line into a single line. */
+ var collapseSingleLine: Boolean = true
+
+ /** Whether to collapse repeated spaces. */
+ var collapseSpaces: Boolean = true
+
+ /** Whether to convert basic markup like **bold** into **bold**, < into <, etc. */
+ var convertMarkup: Boolean = true
+
+ /**
+ * Whether to add punctuation where missing, such as ending sentences with a period. (TODO: Make
+ * sure the FIRST sentence ends with one too! Especially if the subsequent sentence is separated.)
+ */
+ var addPunctuation: Boolean = false
+
+ /**
+ * How many spaces to use for hanging indents in numbered lists and after block tags. Using 4 or
+ * more here will result in subsequent lines being interpreted as block formatted.
+ */
+ var hangingIndent: Int = 2
+
+ /** When there are nested lists etc, how many spaces to indent by. */
+ var nestedListIndent: Int = 3
+ set(value) {
+ if (value < 3) {
+ error(
+ "Nested list indent must be at least 3; if list items are only indented 2 spaces they " +
+ "will not be rendered as list items")
+ }
+ field = value
+ }
+
+ /**
+ * Don't format with tabs! (See
+ * https://kotlinlang.org/docs/reference/coding-conventions.html#formatting)
+ *
+ * But if you do, this is the tab width.
+ */
+ var tabWidth: Int = 8
+
+ /** Whether to perform optimal line breaking instead of greeding. */
+ var optimal: Boolean = true
+
+ /**
+ * If true, reformat markdown tables such that the column markers line up. When false, markdown
+ * tables are left alone (except for left hand side cleanup.)
+ */
+ var alignTableColumns: Boolean = true
+
+ /**
+ * If true, moves any kdoc tags to the end of the comment and `@return` tags after `@param` tags.
+ */
+ var orderDocTags: Boolean = true
+
+ /**
+ * If true, perform "alternative" formatting. This is only relevant in the IDE. You can invoke the
+ * action repeatedly and it will jump between normal formatting an alternative formatting. For
+ * single-line comments it will alternate between single and multiple lines. For longer comments
+ * it will alternate between optimal line breaking and greedy line breaking.
+ */
+ var alternate: Boolean = false
+
+ /**
+ * KDoc allows param tag to be specified using an alternate bracket syntax. KDoc formatter ties to
+ * unify the format of comments, so it will rewrite them into the canonical syntax unless this
+ * option is true.
+ */
+ var allowParamBrackets: Boolean = false
+
+ /** Creates a copy of this formatting object. */
+ fun copy(): KDocFormattingOptions {
+ val copy = KDocFormattingOptions()
+ copy.maxLineWidth = maxLineWidth
+ copy.maxCommentWidth = maxCommentWidth
+ copy.collapseSingleLine = collapseSingleLine
+ copy.collapseSpaces = collapseSpaces
+ copy.hangingIndent = hangingIndent
+ copy.tabWidth = tabWidth
+ copy.alignTableColumns = alignTableColumns
+ copy.orderDocTags = orderDocTags
+ copy.addPunctuation = addPunctuation
+ copy.convertMarkup = convertMarkup
+ copy.nestedListIndent = nestedListIndent
+ copy.optimal = optimal
+ copy.alternate = alternate
+
+ return copy
+ }
+}
diff --git a/core/src/main/java/com/facebook/ktfmt/kdoc/Paragraph.kt b/core/src/main/java/com/facebook/ktfmt/kdoc/Paragraph.kt
new file mode 100644
index 0000000..93e98e5
--- /dev/null
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/Paragraph.kt
@@ -0,0 +1,617 @@
+/*
+ * Portions Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Copyright (c) Tor Norbye.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.facebook.ktfmt.kdoc
+
+import kotlin.math.min
+
+class Paragraph(private val task: FormattingTask) {
+ private val options: KDocFormattingOptions
+ get() = task.options
+ var content = StringBuilder()
+ val text
+ get() = content.toString()
+ var prev: Paragraph? = null
+ var next: Paragraph? = null
+
+ /** If true, this paragraph should be preceded by a blank line. */
+ var separate = false
+
+ /**
+ * If true, this paragraph is a continuation of the previous paragraph (so should be indented with
+ * the hanging indent, including line 1)
+ */
+ var continuation = false
+
+ /**
+ * Whether this paragraph is allowed to be empty. Paragraphs are normally merged if this is not
+ * set. This allows the line breaker to call [ParagraphListBuilder.newParagraph] repeatedly
+ * without introducing more than one new paragraph. But for preformatted text we do want to be
+ * able to express repeated blank lines.
+ */
+ var allowEmpty = false
+
+ /** Is this paragraph preformatted? */
+ var preformatted = false
+
+ /** Is this paragraph a block paragraph? If so, it must start on its own line. */
+ var block = false
+
+ /** Is this paragraph specifying a kdoc tag like @param? */
+ var doc = false
+
+ /**
+ * Is this line quoted? (In the future make this an int such that we can support additional
+ * levels.)
+ */
+ var quoted = false
+
+ /** Is this line part of a table? */
+ var table = false
+
+ /** Is this a separator line? */
+ var separator = false
+
+ /** Should this paragraph use a hanging indent? (Implies [block] as well). */
+ var hanging = false
+ set(value) {
+ block = true
+ field = value
+ }
+
+ var originalIndent = 0
+
+ // The indent to use for all lines in the paragraph.
+ var indent = ""
+
+ // The indent to use for all lines in the paragraph if [hanging] is true,
+ // or the second and subsequent lines if [hanging] is false
+ var hangingIndent = ""
+
+ fun isEmpty(): Boolean {
+ return content.isEmpty()
+ }
+
+ private fun hasClosingPre(): Boolean {
+ return content.contains("", ignoreCase = false) || next?.hasClosingPre() ?: false
+ }
+
+ fun cleanup() {
+ val original = text
+
+ if (preformatted) {
+ return
+ }
+
+ var s = original
+ if (options.convertMarkup) {
+ s = convertMarkup(text)
+ }
+ if (!options.allowParamBrackets) {
+ s = rewriteParams(s)
+ }
+
+ if (s != original) {
+ content.clear()
+ content.append(s)
+ }
+ }
+
+ private fun rewriteParams(s: String): String {
+ var start = 0
+ val length = s.length
+ while (start < length && s[start].isWhitespace()) {
+ start++
+ }
+ if (s.startsWith("@param", start)) {
+ start += "@param".length
+ while (start < length && s[start].isWhitespace()) {
+ start++
+ }
+ if (start < length && s[start++] == '[') {
+ while (start < length && s[start].isWhitespace()) {
+ start++
+ }
+ var end = start
+ while (end < length && s[end].isJavaIdentifierPart()) {
+ end++
+ }
+ if (end > start) {
+ val name = s.substring(start, end)
+ while (end < length && s[end].isWhitespace()) {
+ end++
+ }
+ if (end < length && s[end++] == ']') {
+ while (end < length && s[end].isWhitespace()) {
+ end++
+ }
+ return "@param $name ${s.substring(end)}"
+ }
+ }
+ }
+ }
+
+ return s
+ }
+
+ private fun convertMarkup(s: String): String {
+ if (s.none { it == '<' || it == '&' || it == '{' }) return s
+
+ val sb = StringBuilder(s.length)
+ var i = 0
+ val n = s.length
+ var code = false
+ var brackets = 0
+ while (i < n) {
+ val c = s[i++]
+ if (c == '\\') {
+ sb.append(c)
+ if (i < n - 1) {
+ sb.append(s[i++])
+ }
+ continue
+ } else if (c == '`') {
+ code = !code
+ sb.append(c)
+ continue
+ } else if (c == '[') {
+ brackets++
+ sb.append(c)
+ continue
+ } else if (c == ']') {
+ brackets--
+ sb.append(c)
+ continue
+ } else if (code || brackets > 0) {
+ sb.append(c)
+ continue
+ } else if (c == '<') {
+ if (s.startsWith("b>", i, false) || s.startsWith("/b>", i, false)) {
+ // "" or -> "**"
+ sb.append('*').append('*')
+ if (s[i] == '/') i++
+ i += 2
+ continue
+ }
+ if (s.startsWith("i>", i, false) || s.startsWith("/i>", i, false)) {
+ // "" or -> "*"
+ sb.append('*')
+ if (s[i] == '/') i++
+ i += 2
+ continue
+ }
+ if (s.startsWith("em>", i, false) || s.startsWith("/em>", i, false)) {
+ // "" or -> "_"
+ sb.append('_')
+ if (s[i] == '/') i++
+ i += 3
+ continue
+ }
+ // (We don't convert here because those tags appear in paragraphs
+ // marked preformatted, and preformatted paragraphs are never passed to
+ // convertTags)
+ } else if (c == '&') {
+ if (s.startsWith("lt;", i, true)) { // "<" -> "<"
+ sb.append('<')
+ i += 3
+ continue
+ }
+ if (s.startsWith("gt;", i, true)) { // ">" -> ">"
+ sb.append('>')
+ i += 3
+ continue
+ }
+ } else if (c == '{') {
+ if (s.startsWith("@param", i, true)) {
+ val curr = i + 6
+ var end = s.indexOf('}', curr)
+ if (end == -1) {
+ end = n
+ }
+ sb.append('[')
+ sb.append(s.substring(curr, end).trim())
+ sb.append(']')
+ i = end + 1
+ continue
+ } else if (s.startsWith("@link", i, true)
+ // @linkplain is similar to @link, but kdoc does *not* render a [symbol]
+ // into a {@linkplain} in HTML, so converting these would change the output.
+ && !s.startsWith("@linkplain", i, true)) {
+ // {@link} or {@linkplain}
+ sb.append('[')
+ var curr = i + 5
+ while (curr < n) {
+ val ch = s[curr++]
+ if (ch.isWhitespace()) {
+ break
+ }
+ if (ch == '}') {
+ curr--
+ break
+ }
+ }
+ var skip = false
+ while (curr < n) {
+ val ch = s[curr]
+ if (ch == '}') {
+ sb.append(']')
+ curr++
+ break
+ } else if (ch == '(') {
+ skip = true
+ } else if (!skip) {
+ if (ch == '#') {
+ if (!sb.endsWith('[')) {
+ sb.append('.')
+ }
+ } else {
+ sb.append(ch)
+ }
+ }
+ curr++
+ }
+ i = curr
+ continue
+ }
+ }
+ sb.append(c)
+ }
+
+ return sb.toString()
+ }
+
+ fun reflow(firstLineMaxWidth: Int, maxLineWidth: Int): List {
+ val lineWidth = maxLineWidth - getIndentSize(indent, options)
+ val hangingIndentSize = getIndentSize(hangingIndent, options) - if (quoted) 2 else 0 // "> "
+ if (text.length < (firstLineMaxWidth - hangingIndentSize)) {
+ return listOf(text.collapseSpaces())
+ }
+ // Split text into words
+ val words: List = computeWords()
+
+ // See divide & conquer algorithm listed here: https://xxyxyz.org/line-breaking/
+ if (words.size == 1) {
+ return listOf(words[0])
+ }
+
+ if (firstLineMaxWidth < maxLineWidth) {
+ // We have ragged text. We'll just greedily place the first
+ // words on the first line, and then optimize the rest.
+ val line = StringBuilder()
+ val firstLineWidth = firstLineMaxWidth - getIndentSize(indent, options)
+ for (i in words.indices) {
+ val word = words[i]
+ if (line.isEmpty()) {
+ if (word.length + task.type.lineOverhead() > firstLineMaxWidth) {
+ // can't fit anything on the first line: just flow to
+ // full width and caller will need to insert comment on
+ // the next line.
+ return reflow(words, lineWidth, hangingIndentSize)
+ }
+ line.append(word)
+ } else if (line.length + word.length + 1 <= firstLineWidth) {
+ line.append(' ')
+ line.append(word)
+ } else {
+ // Break the rest
+ val remainingWords = words.subList(i, words.size)
+ val reflownRemaining = reflow(remainingWords, lineWidth, hangingIndentSize)
+ return listOf(line.toString()) + reflownRemaining
+ }
+ }
+ // We fit everything on the first line
+ return listOf(line.toString())
+ }
+
+ return reflow(words, lineWidth, hangingIndentSize)
+ }
+
+ fun reflow(words: List, lineWidth: Int, hangingIndentSize: Int): List {
+ if (options.alternate || !options.optimal || hanging && hangingIndentSize > 0) {
+ // Switch to greedy if explicitly turned on, and for hanging indent
+ // paragraphs, since the current implementation doesn't have support
+ // for a different maximum length on the first line from the rest
+ // and there were various cases where this ended up with bad results.
+ // This is typically used in list items (and kdoc sections) which tend
+ // to be short -- and for 2-3 lines the gains of optimal line breaking
+ // isn't worth the cases where we have really unbalanced looking text
+ return reflowGreedy(lineWidth, options, words)
+ }
+
+ val lines = reflowOptimal(lineWidth - hangingIndentSize, words)
+ if (lines.size <= 2) {
+ // Just 2 lines? We prefer long+short instead of half+half.
+ return reflowGreedy(lineWidth, options, words)
+ } else {
+ // We could just return [lines] here, but the straightforward algorithm
+ // doesn't do a great job with short paragraphs where the last line is
+ // short; it over-corrects and shortens everything else in order to balance
+ // out the last line.
+
+ val maxLine: (String) -> Int = {
+ // Ignore lines that are unbreakable
+ if (it.indexOf(' ') == -1) {
+ 0
+ } else {
+ it.length
+ }
+ }
+ val longestLine = lines.maxOf(maxLine)
+ var lastWord = words.size - 1
+ while (lastWord > 0) {
+ // We can afford to do this because we're only repeating it for a single
+ // line's worth of words and because comments tend to be relatively short
+ // anyway
+ val newLines = reflowOptimal(lineWidth - hangingIndentSize, words.subList(0, lastWord))
+ if (newLines.size < lines.size) {
+ val newLongestLine = newLines.maxOf(maxLine)
+ if (newLongestLine > longestLine &&
+ newLines.subList(0, newLines.size - 1).any { it.length > longestLine }) {
+ return newLines +
+ reflowGreedy(
+ lineWidth - hangingIndentSize, options, words.subList(lastWord, words.size))
+ }
+ break
+ }
+ lastWord--
+ }
+
+ return lines
+ }
+ }
+
+ /**
+ * Returns true if it's okay to break at the current word.
+ *
+ * We need to check for this, because a word can have a different meaning at the beginning of a
+ * line than in the middle somewhere, so if it just so happens to be at the break boundary, we
+ * need to make sure we don't make it the first word on the next line since that would change the
+ * documentation.
+ */
+ private fun canBreakAt(word: String): Boolean {
+ // Can we start a new line with this without interpreting it in a special
+ // way?
+
+ if (word.startsWith("#") ||
+ word.startsWith("```") ||
+ word.isDirectiveMarker() ||
+ word.startsWith("@") || // interpreted as a tag
+ word.isTodo()) {
+ return false
+ }
+
+ if (!word.first().isLetter()) {
+ val wordWithSpace = "$word " // for regex matching in below checks
+ if (wordWithSpace.isListItem() && !word.equals("", true) || wordWithSpace.isQuoted()) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ private fun computeWords(): List {
+ val words = text.split(Regex("\\s+")).filter { it.isNotBlank() }.map { it.trim() }
+ if (words.size == 1) {
+ return words
+ }
+
+ if (task.type != CommentType.KDOC) {
+ // In block comments and line comments we feel free to break anywhere
+ // between words; there isn't a special meaning assigned to certain words
+ // if they appear first on a line like there is in KDoc/Markdown.
+ return words
+ }
+
+ // See if any of the words should never be broken up. We do that for list
+ // separators and a few others. We never want to put "1." at the beginning
+ // of a line as an overflow.
+
+ val combined = ArrayList(words.size)
+
+ // If this paragraph is a list item or a quoted line, merge the first word
+ // with this item such that we never split them apart.
+ var start = 0
+ var first = words[start++]
+ if (quoted || hanging && !text.isKDocTag()) {
+ first = first + " " + words[start++]
+ }
+
+ combined.add(first)
+ var prev = first
+ var insideSquareBrackets = words[start - 1].startsWith("[")
+ for (i in start until words.size) {
+ val word = words[i]
+
+ // We also cannot break up a URL text across lines, which will alter the
+ // rendering of the docs.
+ if (prev.startsWith("[")) insideSquareBrackets = true
+ if (prev.contains("]")) insideSquareBrackets = false
+
+ // Can we start a new line with this without interpreting it in a special
+ // way?
+ if (!canBreakAt(word) || insideSquareBrackets) {
+ // Combine with previous word with a single space; the line breaking
+ // algorithm won't know that it's more than one word.
+ val joined = "$prev $word"
+ combined.removeLast()
+ combined.add(joined)
+ prev = joined
+ } else {
+ combined.add(word)
+ prev = word
+ }
+ }
+ return combined
+ }
+
+ private data class Quadruple(val i0: Int, val j0: Int, val i1: Int, val j1: Int)
+
+ private fun reflowOptimal(maxLineWidth: Int, words: List): List {
+ val count = words.size
+ val lines = ArrayList()
+
+ val offsets = ArrayList()
+ offsets.add(0)
+
+ for (boxWidth in words.map { it.length }.toList()) {
+ offsets.add(offsets.last() + min(boxWidth, maxLineWidth))
+ }
+
+ val big = 10 shl 20
+ val minimum = IntArray(count + 1) { big }
+ val breaks = IntArray(count + 1)
+ minimum[0] = 0
+
+ fun cost(i: Int, j: Int): Int {
+ val width = offsets[j] - offsets[i] + j - i - 1
+ return if (width <= maxLineWidth) {
+ val squared = (maxLineWidth - width) * (maxLineWidth - width)
+ minimum[i] + squared
+ } else {
+ big
+ }
+ }
+
+ fun search(pi0: Int, pj0: Int, pi1: Int, pj1: Int) {
+ val stack = java.util.ArrayDeque()
+ stack.add(Quadruple(pi0, pj0, pi1, pj1))
+
+ while (stack.isNotEmpty()) {
+ val (i0, j0, i1, j1) = stack.removeLast()
+ if (j0 < j1) {
+ val j = (j0 + j1) / 2
+
+ for (i in i0 until i1) {
+ val c = cost(i, j)
+ if (c <= minimum[j]) {
+ minimum[j] = c
+ breaks[j] = i
+ }
+ }
+ stack.add(Quadruple(breaks[j], j + 1, i1, j1))
+ stack.add(Quadruple(i0, j0, breaks[j] + 1, j))
+ }
+ }
+ }
+
+ var n = count + 1
+ var i = 0
+ var offset = 0
+
+ while (true) {
+ val r = min(n, 1 shl (i + 1))
+ val edge = (1 shl i) + offset
+ search(0 + offset, edge, edge, r + offset)
+ val x = minimum[r - 1 + offset]
+ var flag = true
+ for (j in (1 shl i) until (r - 1)) {
+ val y = cost(j + offset, r - 1 + offset)
+ if (y <= x) {
+ n -= j
+ i = 0
+ offset += j
+ flag = false
+ break
+ }
+ }
+ if (flag) {
+ if (r == n) break
+ i++
+ }
+ }
+
+ var j = count
+ while (j > 0) {
+ i = breaks[j]
+ val sb = StringBuilder()
+ for (w in i until j) {
+ sb.append(words[w])
+ if (w < j - 1) {
+ sb.append(' ')
+ }
+ }
+ lines.add(sb.toString())
+ j = i
+ }
+
+ lines.reverse()
+ return lines
+ }
+
+ private fun reflowGreedy(
+ lineWidth: Int,
+ options: KDocFormattingOptions,
+ words: List
+ ): List {
+ // Greedy implementation
+
+ var width = lineWidth
+ if (options.hangingIndent > 0 && hanging && continuation) {
+ width -= getIndentSize(hangingIndent, options)
+ }
+
+ val lines = mutableListOf()
+ var column = 0
+ val sb = StringBuilder()
+ for (word in words) {
+ when {
+ sb.isEmpty() -> {
+ sb.append(word)
+ column += word.length
+ }
+ column + word.length + 1 <= width -> {
+ sb.append(' ').append(word)
+ column += word.length + 1
+ }
+ else -> {
+ width = lineWidth
+ if (options.hangingIndent > 0 && hanging) {
+ width -= getIndentSize(hangingIndent, options)
+ }
+ lines.add(sb.toString())
+ sb.setLength(0)
+ sb.append(word)
+ column = sb.length
+ }
+ }
+ }
+ if (sb.isNotEmpty()) {
+ lines.add(sb.toString())
+ }
+ return lines
+ }
+
+ override fun toString(): String {
+ return "$content, separate=$separate, block=$block, hanging=$hanging, preformatted=$preformatted, quoted=$quoted, continuation=$continuation, allowempty=$allowEmpty, separator=$separator"
+ }
+}
diff --git a/core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphList.kt b/core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphList.kt
new file mode 100644
index 0000000..5130824
--- /dev/null
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphList.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) Tor Norbye.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.facebook.ktfmt.kdoc
+
+/**
+ * A list of paragraphs. Each paragraph should start on a new line and end with a newline. In
+ * addition, if a paragraph is marked with "separate=true", we'll insert an extra blank line in
+ * front of it.
+ */
+class ParagraphList(private val paragraphs: List) : Iterable {
+ fun isSingleParagraph() = paragraphs.size <= 1
+ override fun iterator(): Iterator = paragraphs.iterator()
+ override fun toString(): String = paragraphs.joinToString { it.content }
+}
diff --git a/core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphListBuilder.kt b/core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphListBuilder.kt
new file mode 100644
index 0000000..cb0891e
--- /dev/null
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphListBuilder.kt
@@ -0,0 +1,830 @@
+/*
+ * Copyright (c) Tor Norbye.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.facebook.ktfmt.kdoc
+
+class ParagraphListBuilder(
+ comment: String,
+ private val options: KDocFormattingOptions,
+ private val task: FormattingTask
+) {
+ private val lineComment: Boolean = comment.isLineComment()
+ private val commentPrefix: String =
+ if (lineComment) "//" else if (comment.isKDocComment()) "/**" else "/*"
+ private val paragraphs: MutableList = mutableListOf()
+ private val lines =
+ if (lineComment) {
+ comment.split("\n").map { it.trimStart() }
+ } else if (!comment.contains("\n")) {
+ listOf("* ${comment.removePrefix(commentPrefix).removeSuffix("*/").trim()}")
+ } else {
+ comment.removePrefix(commentPrefix).removeSuffix("*/").trim().split("\n")
+ }
+
+ private fun lineContent(line: String): String {
+ val trimmed = line.trim()
+ return when {
+ lineComment && trimmed.startsWith("// ") -> trimmed.substring(3)
+ lineComment && trimmed.startsWith("//") -> trimmed.substring(2)
+ trimmed.startsWith("* ") -> trimmed.substring(2)
+ trimmed.startsWith("*") -> trimmed.substring(1)
+ else -> trimmed
+ }
+ }
+
+ private fun closeParagraph(): Paragraph {
+ val text = paragraph.text
+ when {
+ text.isKDocTag() -> {
+ paragraph.doc = true
+ paragraph.hanging = true
+ }
+ text.isTodo() -> {
+ paragraph.hanging = true
+ }
+ text.isListItem() -> paragraph.hanging = true
+ text.isDirectiveMarker() -> {
+ paragraph.block = true
+ paragraph.preformatted = true
+ }
+ }
+ if (!paragraph.isEmpty() || paragraph.allowEmpty) {
+ paragraphs.add(paragraph)
+ }
+ return paragraph
+ }
+
+ private fun newParagraph(): Paragraph {
+ closeParagraph()
+ val prev = paragraph
+ paragraph = Paragraph(task)
+ prev.next = paragraph
+ paragraph.prev = prev
+ return paragraph
+ }
+
+ private var paragraph = Paragraph(task)
+
+ private fun appendText(s: String): ParagraphListBuilder {
+ paragraph.content.append(s)
+ return this
+ }
+
+ private fun addLines(
+ i: Int,
+ includeEnd: Boolean = true,
+ until: (Int, String, String) -> Boolean = { _, _, _ -> true },
+ customize: (Int, Paragraph) -> Unit = { _, _ -> },
+ shouldBreak: (String, String) -> Boolean = { _, _ -> false },
+ separator: String = " "
+ ): Int {
+ var j = i
+ while (j < lines.size) {
+ val l = lines[j]
+ val lineWithIndentation = lineContent(l)
+ val lineWithoutIndentation = lineWithIndentation.trim()
+
+ if (!includeEnd) {
+ if (j > i && until(j, lineWithoutIndentation, lineWithIndentation)) {
+ stripTrailingBlankLines()
+ return j
+ }
+ }
+
+ if (shouldBreak(lineWithoutIndentation, lineWithIndentation)) {
+ newParagraph()
+ }
+
+ if (lineWithIndentation.isQuoted()) {
+ appendText(lineWithoutIndentation.substring(2).collapseSpaces())
+ } else {
+ appendText(lineWithoutIndentation.collapseSpaces())
+ }
+ appendText(separator)
+ customize(j, paragraph)
+ if (includeEnd) {
+ if (j > i && until(j, lineWithoutIndentation, lineWithIndentation)) {
+ stripTrailingBlankLines()
+ return j + 1
+ }
+ }
+
+ j++
+ }
+
+ stripTrailingBlankLines()
+ newParagraph()
+
+ return j
+ }
+
+ private fun addPreformatted(
+ i: Int,
+ includeStart: Boolean = false,
+ includeEnd: Boolean = true,
+ expectClose: Boolean = false,
+ customize: (Int, Paragraph) -> Unit = { _, _ -> },
+ until: (String) -> Boolean = { true },
+ ): Int {
+ newParagraph()
+ var j = i
+ var foundClose = false
+ var customize = true
+ while (j < lines.size) {
+ val l = lines[j]
+ val lineWithIndentation = lineContent(l)
+ if (lineWithIndentation.contains("```") &&
+ lineWithIndentation.trimStart().startsWith("```")) {
+ // Don't convert tags if we already have nested ``` content; that will lead to trouble
+ customize = false
+ }
+ val done = (includeStart || j > i) && until(lineWithIndentation)
+ if (!includeEnd && done) {
+ foundClose = true
+ break
+ }
+ j++
+ if (includeEnd && done) {
+ foundClose = true
+ break
+ }
+ }
+
+ // Don't convert if there's already a mixture
+
+ // We ran out of lines. This means we had an unterminated preformatted
+ // block. This is unexpected(unless it was an indented block) and most
+ // likely a documentation error (even Dokka will start formatting return
+ // value documentation in preformatted text if you have an opening
+ // without a closing
before a @return comment), but try to backpedal
+ // a bit such that we don't apply full preformatted treatment everywhere to
+ // things like line breaking.
+ if (!foundClose && expectClose) {
+ // Just add a single line as preformatted and then treat the rest in the
+ // normal way
+ customize = false
+ j = lines.size
+ }
+
+ for (index in i until j) {
+ val l = lines[index]
+ val lineWithIndentation = lineContent(l)
+ appendText(lineWithIndentation)
+ paragraph.preformatted = true
+ paragraph.allowEmpty = true
+ if (customize) {
+ customize(index, paragraph)
+ }
+ newParagraph()
+ }
+ stripTrailingBlankLines()
+ newParagraph()
+
+ return j
+ }
+
+ private fun stripTrailingBlankLines() {
+ for (p in paragraphs.size - 1 downTo 0) {
+ val paragraph = paragraphs[p]
+ if (!paragraph.isEmpty()) {
+ break
+ }
+ paragraphs.removeAt(p)
+ }
+ }
+
+ fun scan(indentSize: Int): ParagraphList {
+ var i = 0
+ while (i < lines.size) {
+ val l = lines[i++]
+ val lineWithIndentation = lineContent(l)
+ val lineWithoutIndentation = lineWithIndentation.trim()
+
+ fun newParagraph(i: Int): Paragraph {
+ val paragraph = this.newParagraph()
+
+ if (i >= 0 && i < lines.size) {
+ if (lines[i] == l) {
+ paragraph.originalIndent = lineWithIndentation.length - lineWithoutIndentation.length
+ } else {
+ // We've looked ahead, e.g. when adding lists etc
+ val line = lineContent(lines[i])
+ val trimmed = line.trim()
+ paragraph.originalIndent = line.length - trimmed.length
+ }
+ }
+ return paragraph
+ }
+
+ if (lineWithIndentation.startsWith(" ") && // markdown preformatted text
+ (i == 1 || lineContent(lines[i - 2]).isBlank()) && // we've already ++'ed i above
+ // Make sure it's not just deeply indented inside a different block
+ (paragraph.prev == null ||
+ lineWithIndentation.length - lineWithoutIndentation.length >=
+ paragraph.prev!!.originalIndent + 4)) {
+ i = addPreformatted(i - 1, includeEnd = false, expectClose = false) { !it.startsWith(" ") }
+ } else if (lineWithoutIndentation.startsWith("-") &&
+ lineWithoutIndentation.containsOnly('-', '|', ' ')) {
+ val paragraph = newParagraph(i - 1)
+ appendText(lineWithoutIndentation)
+ newParagraph(i).block = true
+ // Dividers must be surrounded by blank lines
+ if (lineWithIndentation.isLine() &&
+ (i < 2 || lineContent(lines[i - 2]).isBlank()) &&
+ (i > lines.size - 1 || lineContent(lines[i]).isBlank())) {
+ paragraph.separator = true
+ }
+ } else if (lineWithoutIndentation.startsWith("=") &&
+ lineWithoutIndentation.containsOnly('=', ' ')) {
+ // Header
+ // ======
+ newParagraph(i - 1).block = true
+ appendText(lineWithoutIndentation)
+ newParagraph(i).block = true
+ } else if (lineWithoutIndentation.startsWith(
+ "#")) { // not isHeader() because is handled separately
+ // ## Header
+ newParagraph(i - 1).block = true
+ appendText(lineWithoutIndentation)
+ newParagraph(i).block = true
+ } else if (lineWithoutIndentation.startsWith("*") &&
+ lineWithoutIndentation.containsOnly('*', ' ')) {
+ // Horizontal rule:
+ // *******
+ // * * *
+ // Unlike --- lines, these aren't required to be preceded by or followed by
+ // blank lines.
+ newParagraph(i - 1).block = true
+ appendText(lineWithoutIndentation)
+ newParagraph(i).block = true
+ } else if (lineWithoutIndentation.startsWith("```")) {
+ i = addPreformatted(i - 1, expectClose = true) { it.trimStart().startsWith("```") }
+ } else if (lineWithoutIndentation.startsWith("", ignoreCase = true)) {
+ i =
+ addPreformatted(
+ i - 1,
+ includeStart = true,
+ expectClose = true,
+ customize = { _, _ ->
+ if (options.convertMarkup) {
+ fun handleTag(tag: String) {
+ val text = paragraph.text
+ val trimmed = text.trim()
+
+ val index = text.indexOf(tag, ignoreCase = true)
+ if (index == -1) {
+ return
+ }
+ paragraph.content.clear()
+ if (trimmed.equals(tag, ignoreCase = true)) {
+ paragraph.content.append("```")
+ return
+ }
+
+ // Split paragraphs; these things have to be on their own line
+ // in the ``` form (unless both are in the middle)
+ val before = text.substring(0, index).replace("", "", true).trim()
+ if (before.isNotBlank()) {
+ paragraph.content.append(before)
+ newParagraph()
+ paragraph.preformatted = true
+ paragraph.allowEmpty = true
+ }
+ appendText("```")
+ val after =
+ text.substring(index + tag.length).replace("", "", true).trim()
+ if (after.isNotBlank()) {
+ newParagraph()
+ appendText(after)
+ paragraph.preformatted = true
+ paragraph.allowEmpty = true
+ }
+ }
+
+ handleTag("")
+ handleTag("
")
+ }
+ },
+ until = { it.contains("
", ignoreCase = true) })
+ } else if (lineWithoutIndentation.isQuoted()) {
+ i--
+ val paragraph = newParagraph(i)
+ paragraph.quoted = true
+ paragraph.block = false
+ i =
+ addLines(
+ i,
+ until = { _, w, _ ->
+ w.isBlank() ||
+ w.isListItem() ||
+ w.isKDocTag() ||
+ w.isTodo() ||
+ w.isDirectiveMarker() ||
+ w.isHeader()
+ },
+ customize = { _, p -> p.quoted = true },
+ includeEnd = false)
+ newParagraph(i)
+ } else if (lineWithoutIndentation.equals("", true) ||
+ lineWithoutIndentation.equals("", true)) {
+ newParagraph(i - 1).block = true
+ appendText(lineWithoutIndentation)
+ newParagraph(i).hanging = true
+ i =
+ addLines(
+ i,
+ includeEnd = true,
+ until = { _, w, _ -> w.equals("
", true) || w.equals("", true) },
+ customize = { _, p -> p.block = true },
+ shouldBreak = { w, _ ->
+ w.startsWith("", true) ||
+ w.startsWith("", true) ||
+ w.startsWith("", true)
+ })
+ newParagraph(i)
+ } else if (lineWithoutIndentation.isListItem() ||
+ lineWithoutIndentation.isKDocTag() && task.type == CommentType.KDOC ||
+ lineWithoutIndentation.isTodo()) {
+ i--
+ newParagraph(i).hanging = true
+ val start = i
+ i =
+ addLines(
+ i,
+ includeEnd = false,
+ until = { j: Int, w: String, s: String ->
+ // See if it's a line continuation
+ if (s.isBlank() &&
+ j < lines.size - 1 &&
+ lineContent(lines[j + 1]).startsWith(" ")) {
+ false
+ } else {
+ s.isBlank() ||
+ w.isListItem() ||
+ w.isQuoted() ||
+ w.isKDocTag() ||
+ w.isTodo() ||
+ s.startsWith("```") ||
+ w.startsWith("") ||
+ w.isDirectiveMarker() ||
+ w.isLine() ||
+ w.isHeader() ||
+ // Not indented by at least two spaces following a blank line?
+ s.length > 2 &&
+ (!s[0].isWhitespace() || !s[1].isWhitespace()) &&
+ j < lines.size - 1 &&
+ lineContent(lines[j - 1]).isBlank()
+ }
+ },
+ shouldBreak = { w, _ -> w.isBlank() },
+ customize = { j, p ->
+ if (lineContent(lines[j]).isBlank() && j >= start) {
+ p.hanging = true
+ p.continuation = true
+ }
+ })
+ newParagraph(i)
+ } else if (lineWithoutIndentation.isEmpty()) {
+ newParagraph(i).separate = true
+ } else if (lineWithoutIndentation.isDirectiveMarker()) {
+ newParagraph(i - 1)
+ appendText(lineWithoutIndentation)
+ newParagraph(i).block = true
+ } else {
+ if (lineWithoutIndentation.indexOf('|') != -1 &&
+ paragraph.isEmpty() &&
+ (i < 2 || !lines[i - 2].contains("---"))) {
+ val result = Table.getTable(lines, i - 1, ::lineContent)
+ if (result != null) {
+ val (table, nextRow) = result
+ val content =
+ if (options.alignTableColumns) {
+ // Only considering maxLineWidth here, not maxCommentWidth; we
+ // cannot break table lines, only adjust tabbing, and a padded table
+ // seems more readable (maxCommentWidth < maxLineWidth is there to
+ // prevent long lines for readability)
+ table.format(options.maxLineWidth - indentSize - 3)
+ } else {
+ table.original()
+ }
+ for (index in content.indices) {
+ val line = content[index]
+ appendText(line)
+ paragraph.separate = index == 0
+ paragraph.block = true
+ paragraph.table = true
+ newParagraph(-1)
+ }
+ i = nextRow
+ newParagraph(i)
+ continue
+ }
+ }
+
+ // Some common HTML block tags
+ if (lineWithoutIndentation.startsWith("<") &&
+ (lineWithoutIndentation.startsWith("", true) ||
+ lineWithoutIndentation.startsWith("
", true) ||
+ lineWithoutIndentation.startsWith("", true) ||
+ lineWithoutIndentation.equals("
", true) ||
+ options.convertMarkup && lineWithoutIndentation.equals("
", true)) {
+ if (options.convertMarkup) {
+ // Replace with a blank line
+ paragraph.separate = true
+ } else {
+ appendText(lineWithoutIndentation)
+ newParagraph(i).block = true
+ }
+ continue
+ } else if (lineWithoutIndentation.endsWith("", true) ||
+ lineWithoutIndentation.endsWith("", true) ||
+ lineWithoutIndentation.endsWith("", true) ||
+ lineWithoutIndentation.endsWith("", true)) {
+ if (lineWithoutIndentation.startsWith("", true) || text.startsWith("", true))) {
+ paragraph.separate = true
+ text.substring(text.indexOf('>') + 1).trim()
+ } else {
+ text
+ }
+ .let { if (options.collapseSpaces) it.collapseSpaces() else it }
+
+ appendText(s)
+ appendText(" ")
+
+ if (braceBalance > 0) {
+ val end = s.indexOf('}')
+ if (end == -1 && i < lines.size) {
+ val next = lineContent(lines[i]).trim()
+ if (breakOutOfTag(next)) {
+ return i
+ }
+ return addPlainText(i + 1, next, 1)
+ }
+ }
+
+ val index = s.indexOf("{@")
+ if (index != -1) {
+ // find end
+ val end = s.indexOf('}', index)
+ if (end == -1 && i < lines.size) {
+ val next = lineContent(lines[i]).trim()
+ if (breakOutOfTag(next)) {
+ return i
+ }
+ return addPlainText(i + 1, next, 1)
+ }
+ }
+
+ return i
+ }
+
+ private fun breakOutOfTag(next: String): Boolean {
+ if (next.isBlank() || next.startsWith("```")) {
+ // See https://github.com/tnorbye/kdoc-formatter/issues/77
+ // There may be comments which look unusual from a formatting
+ // perspective where it looks like you have embedded markup
+ // or blank lines; if so, just give up on trying to turn
+ // this into paragraph text
+ return true
+ }
+ return false
+ }
+
+ private fun docTagRank(tag: String): Int {
+ // Canonical kdoc order -- https://kotlinlang.org/docs/kotlin-doc.html#block-tags
+ // Full list in Dokka's sources: plugins/base/src/main/kotlin/parsers/Parser.kt
+ return when {
+ tag.startsWith("@param") -> 0
+ tag.startsWith("@return") -> 1
+ tag.startsWith("@constructor") -> 2
+ tag.startsWith("@receiver") -> 3
+ tag.startsWith("@property") -> 4
+ tag.startsWith("@throws") -> 5
+ tag.startsWith("@exception") -> 6
+ tag.startsWith("@sample") -> 7
+ tag.startsWith("@see") -> 8
+ tag.startsWith("@author") -> 9
+ tag.startsWith("@since") -> 10
+ tag.startsWith("@suppress") -> 11
+ tag.startsWith("@deprecated") -> 12
+ else -> 100 // custom tags
+ }
+ }
+
+ /**
+ * Make a pass over the paragraphs and make sure that we (for example) place blank lines around
+ * preformatted text.
+ */
+ private fun arrange() {
+ if (paragraphs.isEmpty()) {
+ return
+ }
+
+ sortDocTags()
+ adjustParagraphSeparators()
+ adjustIndentation()
+ removeBlankParagraphs()
+ stripTrailingBlankLines()
+ }
+
+ private fun sortDocTags() {
+ if (options.orderDocTags && paragraphs.any { it.doc }) {
+ val order = paragraphs.mapIndexed { index, paragraph -> paragraph to index }.toMap()
+ val comparator =
+ object : Comparator> {
+ override fun compare(l1: List, l2: List): Int {
+ val p1 = l1.first()
+ val p2 = l2.first()
+ val o1 = order[p1]!!
+ val o2 = order[p2]!!
+
+ // Sort TODOs to the end
+ if (p1.text.isTodo() != p2.text.isTodo()) {
+ return if (p1.text.isTodo()) 1 else -1
+ }
+
+ if (p1.doc == p2.doc) {
+ if (p1.doc) {
+ // Sort @return after @param etc
+ val r1 = docTagRank(p1.text)
+ val r2 = docTagRank(p2.text)
+ if (r1 != r2) {
+ return r1 - r2
+ }
+ // Within identical tags, preserve current order, except for
+ // parameter names which are sorted by signature order.
+ val orderedParameterNames = task.orderedParameterNames
+ if (orderedParameterNames.isNotEmpty()) {
+ fun Paragraph.parameterRank(): Int {
+ val name = text.getParamName()
+ if (name != null) {
+ val index = orderedParameterNames.indexOf(name)
+ if (index != -1) {
+ return index
+ }
+ }
+ return 1000
+ }
+
+ val i1 = p1.parameterRank()
+ val i2 = p2.parameterRank()
+
+ // If the parameter names are not matching, ignore.
+ if (i1 != i2) {
+ return i1 - i2
+ }
+ }
+ }
+ return o1 - o2
+ }
+ return if (p1.doc) 1 else -1
+ }
+ }
+
+ // We don't sort the paragraphs list directly; we have to tie all the
+ // paragraphs following a KDoc parameter to that paragraph (until the
+ // next KDoc tag). So instead we create a list of lists -- consisting of
+ // one list for each paragraph, though with a KDoc parameter it's a list
+ // containing first the KDoc parameter paragraph and then all following
+ // parameters. We then sort by just the first item in this list of list,
+ // and then restore the paragraph list from the result.
+ val units = mutableListOf>()
+ var tag: MutableList? = null
+ for (paragraph in paragraphs) {
+ if (paragraph.doc) {
+ tag = mutableListOf()
+ units.add(tag)
+ }
+ if (tag != null && !paragraph.text.isTodo()) {
+ tag.add(paragraph)
+ } else {
+ units.add(listOf(paragraph))
+ }
+ }
+ units.sortWith(comparator)
+
+ var prev: Paragraph? = null
+ paragraphs.clear()
+ for (paragraph in units.flatten()) {
+ paragraphs.add(paragraph)
+ prev?.next = paragraph
+ paragraph.prev = prev
+ prev = paragraph
+ }
+ }
+ }
+
+ private fun adjustParagraphSeparators() {
+ var prev: Paragraph? = null
+
+ for (paragraph in paragraphs) {
+ paragraph.cleanup()
+ val text = paragraph.text
+ paragraph.separate =
+ when {
+ prev == null -> false
+ paragraph.preformatted && prev.preformatted -> false
+ paragraph.table ->
+ paragraph.separate && (!prev.block || prev.text.isKDocTag() || prev.table)
+ paragraph.separator || prev.separator -> true
+ text.isLine(1) || prev.text.isLine(1) -> false
+ paragraph.separate && paragraph.text.isListItem() -> false
+ paragraph.separate -> true
+ // Don't separate kdoc tags, except for the first one
+ paragraph.doc -> !prev.doc
+ text.isDirectiveMarker() -> false
+ text.isTodo() && !prev.text.isTodo() -> true
+ text.isHeader() -> true
+ // Set preformatted paragraphs off (but not tags where it's implicit)
+ paragraph.preformatted ->
+ !prev.preformatted &&
+ !text.startsWith("", true) -> false
+ paragraph.continuation -> true
+ paragraph.hanging -> false
+ paragraph.quoted -> prev.quoted
+ text.isHeader() -> true
+ text.startsWith("", true) || text.startsWith("
", true) -> true
+ else -> !paragraph.block && !paragraph.isEmpty()
+ }
+
+ if (paragraph.hanging) {
+ if (paragraph.doc || text.startsWith("
", true) || text.isTodo()) {
+ paragraph.hangingIndent = getIndent(options.hangingIndent)
+ } else if (paragraph.continuation && paragraph.prev != null) {
+ paragraph.hangingIndent = paragraph.prev!!.hangingIndent
+ // Dedent to match hanging indent
+ val s = paragraph.text.trimStart()
+ paragraph.content.clear()
+ paragraph.content.append(s)
+ } else {
+ paragraph.hangingIndent = getIndent(text.indexOf(' ') + 1)
+ }
+ }
+ prev = paragraph
+ }
+ }
+
+ private fun adjustIndentation() {
+ val firstIndent = paragraphs[0].originalIndent
+ if (firstIndent > 0) {
+ for (paragraph in paragraphs) {
+ if (paragraph.originalIndent <= firstIndent) {
+ paragraph.originalIndent = 0
+ }
+ }
+ }
+
+ // Handle nested lists
+ var inList = paragraphs.firstOrNull()?.hanging ?: false
+ var startIndent = 0
+ var levels: MutableSet? = null
+ for (i in 1 until paragraphs.size) {
+ val paragraph = paragraphs[i]
+ if (!inList) {
+ if (paragraph.hanging) {
+ inList = true
+ startIndent = paragraph.originalIndent
+ }
+ } else {
+ if (!paragraph.hanging) {
+ inList = false
+ } else {
+ if (paragraph.originalIndent == startIndent) {
+ paragraph.originalIndent = 0
+ } else if (paragraph.originalIndent > 0) {
+ (levels ?: mutableSetOf().also { levels = it }).add(paragraph.originalIndent)
+ }
+ }
+ }
+ }
+
+ levels?.sorted()?.let { sorted ->
+ val assignments = mutableMapOf()
+ for (i in sorted.indices) {
+ assignments[sorted[i]] = (i + 1) * options.nestedListIndent
+ }
+ for (paragraph in paragraphs) {
+ if (paragraph.originalIndent > 0) {
+ val assigned = assignments[paragraph.originalIndent] ?: continue
+ paragraph.originalIndent = assigned
+ paragraph.indent = getIndent(paragraph.originalIndent)
+ }
+ }
+ }
+ }
+
+ private fun removeBlankParagraphs() {
+ // Remove blank lines between list items and from the end as well as around
+ // separators
+ for (i in paragraphs.size - 2 downTo 0) {
+ if (paragraphs[i].isEmpty() && (!paragraphs[i].preformatted || i == paragraphs.size - 1)) {
+ paragraphs.removeAt(i)
+ if (i > 0) {
+ paragraphs[i - 1].next = null
+ }
+ }
+ }
+ }
+
+ private fun punctuate() {
+ if (!options.addPunctuation || paragraphs.isEmpty()) {
+ return
+ }
+ val last = paragraphs.last()
+ if (last.preformatted || last.doc || last.hanging && !last.continuation || last.isEmpty()) {
+ return
+ }
+
+ val text = last.content
+ if (!text.startsWithUpperCaseLetter()) {
+ return
+ }
+
+ for (i in text.length - 1 downTo 0) {
+ val c = text[i]
+ if (c.isWhitespace()) {
+ continue
+ }
+ if (c.isLetterOrDigit() || c.isCloseSquareBracket()) {
+ text.setLength(i + 1)
+ text.append('.')
+ }
+ break
+ }
+ }
+}
+
+fun String.containsOnly(vararg s: Char): Boolean {
+ for (c in this) {
+ if (s.none { it == c }) {
+ return false
+ }
+ }
+ return true
+}
+
+fun StringBuilder.startsWithUpperCaseLetter() =
+ this.isNotEmpty() && this[0].isUpperCase() && this[0].isLetter()
+
+fun Char.isCloseSquareBracket() = this == ']'
diff --git a/core/src/main/java/com/facebook/ktfmt/kdoc/Table.kt b/core/src/main/java/com/facebook/ktfmt/kdoc/Table.kt
new file mode 100644
index 0000000..a4c837c
--- /dev/null
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/Table.kt
@@ -0,0 +1,270 @@
+/*
+ * Copyright (c) Tor Norbye.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.facebook.ktfmt.kdoc
+
+import kotlin.math.max
+
+class Table(
+ private val columns: Int,
+ private val widths: List,
+ private val rows: List,
+ private val align: List,
+ private val original: List
+) {
+ fun original(): List {
+ return original
+ }
+
+ /**
+ * Format the table. Note that table rows cannot be broken into multiple lines in Markdown tables,
+ * so the [maxWidth] here is used to decide whether to add padding around the table only, and it's
+ * quite possible for the table to format to wider lengths than [maxWidth].
+ */
+ fun format(maxWidth: Int = Integer.MAX_VALUE): List {
+ val tableMaxWidth =
+ 2 + widths.sumOf { it + 2 } // +2: "| " in each cell and final " |" on the right
+
+ val pad = tableMaxWidth <= maxWidth
+ val lines = mutableListOf()
+ for (i in rows.indices) {
+ val sb = StringBuilder()
+ val row = rows[i]
+ for (column in 0 until row.cells.size) {
+ sb.append('|')
+ if (pad) {
+ sb.append(' ')
+ }
+ val cell = row.cells[column]
+ val width = widths[column]
+ val s =
+ if (align[column] == Align.CENTER && i > 0) {
+ String.format(
+ "%-${width}s",
+ String.format("%${cell.length + (width - cell.length) / 2}s", cell))
+ } else if (align[column] == Align.RIGHT && i > 0) {
+ String.format("%${width}s", cell)
+ } else {
+ String.format("%-${width}s", cell)
+ }
+ sb.append(s)
+ if (pad) {
+ sb.append(' ')
+ }
+ }
+ sb.append('|')
+ lines.add(sb.toString())
+ sb.clear()
+
+ if (i == 0) {
+ for (column in 0 until row.cells.size) {
+ sb.append('|')
+ var width = widths[column]
+ if (align[column] != Align.LEFT) {
+ width--
+ if (align[column] == Align.CENTER) {
+ sb.append(':')
+ width--
+ }
+ }
+ if (pad) {
+ sb.append('-')
+ }
+ val s = "-".repeat(width)
+ sb.append(s)
+ if (pad) {
+ sb.append('-')
+ }
+ if (align[column] != Align.LEFT) {
+ sb.append(':')
+ }
+ }
+ sb.append('|')
+ lines.add(sb.toString())
+ sb.clear()
+ }
+ }
+
+ return lines
+ }
+
+ companion object {
+ /**
+ * If the line starting at index [start] begins a table, return that table as well as the index
+ * of the first line after the table.
+ */
+ fun getTable(
+ lines: List,
+ start: Int,
+ lineContent: (String) -> String
+ ): Pair? {
+ if (start > lines.size - 2) {
+ return null
+ }
+ val headerLine = lineContent(lines[start])
+ val separatorLine = lineContent(lines[start + 1])
+ val barCount = countSeparators(headerLine)
+ if (!isHeaderDivider(barCount, separatorLine.trim())) {
+ return null
+ }
+ val header = getRow(headerLine) ?: return null
+ val rows = mutableListOf()
+ rows.add(header)
+
+ val dividerRow = getRow(separatorLine) ?: return null
+
+ var i = start + 2
+ while (i < lines.size) {
+ val line = lineContent(lines[i])
+ if (!line.contains("|")) {
+ break
+ }
+ val row = getRow(line) ?: break
+ rows.add(row)
+ i++
+ }
+
+ val rowsAndDivider = rows + dividerRow
+ if (rowsAndDivider.all {
+ val first = it.cells.firstOrNull()
+ first != null && first.isBlank()
+ }) {
+ rowsAndDivider.forEach { if (it.cells.isNotEmpty()) it.cells.removeAt(0) }
+ }
+
+ // val columns = rows.maxOf { it.cells.size }
+ val columns = dividerRow.cells.size
+ val maxColumns = rows.maxOf { it.cells.size }
+ val widths = mutableListOf()
+ for (column in 0 until maxColumns) {
+ widths.add(3)
+ }
+ for (row in rows) {
+ for (column in 0 until row.cells.size) {
+ widths[column] = max(widths[column], row.cells[column].length)
+ }
+ for (column in row.cells.size until columns) {
+ row.cells.add("")
+ }
+ }
+
+ val align = mutableListOf()
+ for (cell in dividerRow.cells) {
+ val direction =
+ if (cell.endsWith(":")) {
+ if (cell.startsWith(":-")) {
+ Align.CENTER
+ } else {
+ Align.RIGHT
+ }
+ } else {
+ Align.LEFT
+ }
+ align.add(direction)
+ }
+ for (column in align.size until maxColumns) {
+ align.add(Align.LEFT)
+ }
+ val table =
+ Table(columns, widths, rows, align, lines.subList(start, i).map { lineContent(it) })
+ return Pair(table, i)
+ }
+
+ /** Returns true if the given String looks like a markdown table header divider. */
+ private fun isHeaderDivider(barCount: Int, s: String): Boolean {
+ var i = 0
+ var count = 0
+ while (i < s.length) {
+ val c = s[i++]
+ if (c == '\\') {
+ i++
+ } else if (c == '|') {
+ count++
+ } else if (c.isWhitespace() || c == ':') {
+ continue
+ } else if (c == '-' &&
+ (s.startsWith("--", i) ||
+ s.startsWith("-:", i) ||
+ i > 1 && s.startsWith(":-:", i - 2) ||
+ i > 1 && s.startsWith(":--", i - 2))) {
+ while (i < s.length && s[i] == '-') {
+ i++
+ }
+ } else {
+ return false
+ }
+ }
+
+ return barCount == count
+ }
+
+ private fun getRow(s: String): Row? {
+ // Can't just use String.split('|') because that would not handle escaped |'s
+ if (s.indexOf('|') == -1) {
+ return null
+ }
+ val row = Row()
+ var i = 0
+ var end = 0
+ while (end < s.length) {
+ val c = s[end]
+ if (c == '\\') {
+ end++
+ } else if (c == '|') {
+ val cell = s.substring(i, end).trim()
+ if (end > 0) {
+ row.cells.add(cell.trim())
+ }
+ i = end + 1
+ }
+ end++
+ }
+ if (end > i) {
+ val cell = s.substring(i, end).trim()
+ if (cell.isNotEmpty()) {
+ row.cells.add(cell.trim())
+ }
+ }
+
+ return row
+ }
+
+ private fun countSeparators(s: String): Int {
+ var i = 0
+ var count = 0
+ while (i < s.length) {
+ val c = s[i]
+ if (c == '|') {
+ count++
+ } else if (c == '\\') {
+ i++
+ }
+ i++
+ }
+ return count
+ }
+ }
+
+ enum class Align {
+ LEFT,
+ RIGHT,
+ CENTER
+ }
+
+ class Row {
+ val cells = mutableListOf()
+ }
+}
diff --git a/core/src/main/java/com/facebook/ktfmt/kdoc/Utilities.kt b/core/src/main/java/com/facebook/ktfmt/kdoc/Utilities.kt
new file mode 100644
index 0000000..e034bbe
--- /dev/null
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/Utilities.kt
@@ -0,0 +1,329 @@
+/*
+ * Copyright (c) Tor Norbye.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.facebook.ktfmt.kdoc
+
+import java.util.regex.Pattern
+import kotlin.math.min
+
+fun getIndent(width: Int): String {
+ val sb = StringBuilder()
+ for (i in 0 until width) {
+ sb.append(' ')
+ }
+ return sb.toString()
+}
+
+fun getIndentSize(indent: String, options: KDocFormattingOptions): Int {
+ var size = 0
+ for (c in indent) {
+ if (c == '\t') {
+ size += options.tabWidth
+ } else {
+ size++
+ }
+ }
+ return size
+}
+
+/** Returns line number (1-based) */
+fun getLineNumber(source: String, offset: Int, startLine: Int = 1, startOffset: Int = 0): Int {
+ var line = startLine
+ for (i in startOffset until offset) {
+ val c = source[i]
+ if (c == '\n') {
+ line++
+ }
+ }
+ return line
+}
+
+private val numberPattern = Pattern.compile("^\\d+([.)]) ")
+
+fun String.isListItem(): Boolean {
+ return startsWith("- ") ||
+ startsWith("* ") ||
+ startsWith("+ ") ||
+ firstOrNull()?.isDigit() == true && numberPattern.matcher(this).find() ||
+ startsWith("", ignoreCase = true)
+}
+
+fun String.collapseSpaces(): String {
+ if (indexOf(" ") == -1) {
+ return this.trimEnd()
+ }
+ val sb = StringBuilder()
+ var prev: Char = this[0]
+ for (i in indices) {
+ if (prev == ' ') {
+ if (this[i] == ' ') {
+ continue
+ }
+ }
+ sb.append(this[i])
+ prev = this[i]
+ }
+ return sb.trimEnd().toString()
+}
+
+fun String.isTodo(): Boolean {
+ return startsWith("TODO:") || startsWith("TODO(")
+}
+
+fun String.isHeader(): Boolean {
+ return startsWith("#") || startsWith(" ")
+}
+
+fun String.isDirectiveMarker(): Boolean {
+ return startsWith("")
+}
+
+/**
+ * Returns true if the string ends with a symbol that implies more text is coming, e.g. ":" or ","
+ */
+fun String.isExpectingMore(): Boolean {
+ val last = lastOrNull { !it.isWhitespace() } ?: return false
+ return last == ':' || last == ','
+}
+
+/**
+ * Does this String represent a divider line? (Markdown also requires it to be surrounded by empty
+ * lines which has to be checked by the caller)
+ */
+fun String.isLine(minCount: Int = 3): Boolean {
+ return startsWith('-') && containsOnly('-', ' ') && count { it == '-' } >= minCount ||
+ startsWith('_') && containsOnly('_', ' ') && count { it == '_' } >= minCount
+}
+
+fun String.isKDocTag(): Boolean {
+ // Not using a hardcoded list here since tags can change over time
+ if (startsWith("@")) {
+ for (i in 1 until length) {
+ val c = this[i]
+ if (c.isWhitespace()) {
+ return i > 2
+ } else if (!c.isLetter() || !c.isLowerCase()) {
+ if (c == '[' && startsWith("@param")) {
+ // @param is allowed to use brackets -- see
+ // https://kotlinlang.org/docs/kotlin-doc.html#param-name
+ // Example: @param[foo] The description of foo
+ return true
+ }
+ return false
+ }
+ }
+ return true
+ }
+ return false
+}
+
+/**
+ * If this String represents a KDoc `@param` tag, returns the corresponding parameter name,
+ * otherwise null.
+ */
+fun String.getParamName(): String? {
+ val length = this.length
+ var start = 0
+ while (start < length && this[start].isWhitespace()) {
+ start++
+ }
+ if (!this.startsWith("@param", start)) {
+ return null
+ }
+ start += "@param".length
+
+ while (start < length) {
+ if (this[start].isWhitespace()) {
+ start++
+ } else {
+ break
+ }
+ }
+
+ if (start < length && this[start] == '[') {
+ start++
+ while (start < length) {
+ if (this[start].isWhitespace()) {
+ start++
+ } else {
+ break
+ }
+ }
+ }
+
+ var end = start
+ while (end < length) {
+ if (!this[end].isJavaIdentifierPart()) {
+ break
+ }
+ end++
+ }
+
+ if (end > start) {
+ return this.substring(start, end)
+ }
+
+ return null
+}
+
+private fun getIndent(start: Int, lookup: (Int) -> Char): String {
+ var i = start - 1
+ while (i >= 0 && lookup(i) != '\n') {
+ i--
+ }
+ val sb = StringBuilder()
+ for (j in i + 1 until start) {
+ sb.append(lookup(j))
+ }
+ return sb.toString()
+}
+
+/**
+ * Given a character [lookup] function in a document of [max] characters, for a comment starting at
+ * offset [start], compute the effective indent on the first line and on subsequent lines.
+ *
+ * For a comment starting on its own line, the two will be the same. But for a comment that is at
+ * the end of a line containing code, the first line indent will not be the indentation of the
+ * earlier code, it will be the full indent as if all the code characters were whitespace characters
+ * (which lets the formatter figure out how much space is available on the first line).
+ */
+fun computeIndents(start: Int, lookup: (Int) -> Char, max: Int): Pair {
+ val originalIndent = getIndent(start, lookup)
+ val suffix = !originalIndent.all { it.isWhitespace() }
+ val indent =
+ if (suffix) {
+ originalIndent.map { if (it.isWhitespace()) it else ' ' }.joinToString(separator = "")
+ } else {
+ originalIndent
+ }
+
+ val secondaryIndent =
+ if (suffix) {
+ // We don't have great heuristics to figure out what the indent should be
+ // following a source line -- e.g. it can be implied by things like whether
+ // the line ends with '{' or an operator, but it's more complicated than
+ // that. So we'll cheat and just look to see what the existing code does!
+ var offset = start
+ while (offset < max && lookup(offset) != '\n') {
+ offset++
+ }
+ offset++
+ val sb = StringBuilder()
+ while (offset < max) {
+ if (lookup(offset) == '\n') {
+ sb.clear()
+ } else {
+ val c = lookup(offset)
+ if (c.isWhitespace()) {
+ sb.append(c)
+ } else {
+ if (c == '*') {
+ // in a comment, the * is often one space indented
+ // to line up with the first * in the opening /** and
+ // the actual indent should be aligned with the /
+ sb.setLength(sb.length - 1)
+ }
+ break
+ }
+ }
+ offset++
+ }
+ sb.toString()
+ } else {
+ originalIndent
+ }
+
+ return Pair(indent, secondaryIndent)
+}
+
+/**
+ * Attempt to preserve the caret position across reformatting. Returns the delta in the new comment.
+ */
+fun findSamePosition(comment: String, delta: Int, reformattedComment: String): Int {
+ // First see if the two comments are identical up to the delta; if so, same
+ // new position
+ for (i in 0 until min(comment.length, reformattedComment.length)) {
+ if (i == delta) {
+ return delta
+ } else if (comment[i] != reformattedComment[i]) {
+ break
+ }
+ }
+
+ var i = comment.length - 1
+ var j = reformattedComment.length - 1
+ if (delta == i + 1) {
+ return j + 1
+ }
+ while (i >= 0 && j >= 0) {
+ if (i == delta) {
+ return j
+ }
+ if (comment[i] != reformattedComment[j]) {
+ break
+ }
+ i--
+ j--
+ }
+
+ fun isSignificantChar(c: Char): Boolean = c.isWhitespace() || c == '*'
+
+ // Finally it's somewhere in the middle; search by character skipping over
+ // insignificant characters (space, *, etc)
+ fun nextSignificantChar(s: String, from: Int): Int {
+ var curr = from
+ while (curr < s.length) {
+ val c = s[curr]
+ if (isSignificantChar(c)) {
+ curr++
+ } else {
+ break
+ }
+ }
+ return curr
+ }
+
+ var offset = 0
+ var reformattedOffset = 0
+ while (offset < delta && reformattedOffset < reformattedComment.length) {
+ offset = nextSignificantChar(comment, offset)
+ reformattedOffset = nextSignificantChar(reformattedComment, reformattedOffset)
+ if (offset == delta) {
+ return reformattedOffset
+ }
+ offset++
+ reformattedOffset++
+ }
+ return reformattedOffset
+}
+
+// Until stdlib version is no longer experimental
+fun > Iterable.maxOf(selector: (T) -> R): R {
+ val iterator = iterator()
+ if (!iterator.hasNext()) throw NoSuchElementException()
+ var maxValue = selector(iterator.next())
+ while (iterator.hasNext()) {
+ val v = selector(iterator.next())
+ if (maxValue < v) {
+ maxValue = v
+ }
+ }
+ return maxValue
+}
diff --git a/core/src/test/java/com/facebook/ktfmt/cli/MainTest.kt b/core/src/test/java/com/facebook/ktfmt/cli/MainTest.kt
index c02aca6..3697d72 100644
--- a/core/src/test/java/com/facebook/ktfmt/cli/MainTest.kt
+++ b/core/src/test/java/com/facebook/ktfmt/cli/MainTest.kt
@@ -122,6 +122,21 @@ class MainTest {
assertThat(err.toString("UTF-8")).startsWith(":1:14: error: ")
}
+ @Test
+ fun `Parsing errors are reported (stdin-name)`() {
+ val code = "fun f1 ( "
+ val returnValue =
+ Main(
+ code.byteInputStream(),
+ PrintStream(out),
+ PrintStream(err),
+ arrayOf("--stdin-name=file/Foo.kt", "-"))
+ .run()
+
+ assertThat(returnValue).isEqualTo(1)
+ assertThat(err.toString("UTF-8")).startsWith("file/Foo.kt:1:14: error: ")
+ }
+
@Test
fun `Parsing errors are reported (file)`() {
val fooBar = root.resolve("foo.kt")
@@ -231,7 +246,8 @@ class MainTest {
|println(child)
|}
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
val formatted =
"""fun f() {
| for (child in
@@ -239,7 +255,8 @@ class MainTest {
| println(child)
| }
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
Main(
code.byteInputStream(),
PrintStream(out),
@@ -429,4 +446,24 @@ class MainTest {
assertThat(out.toString("UTF-8")).isEqualTo("\n")
assertThat(exitCode).isEqualTo(1)
}
+
+ @Test
+ fun `--stdin-name can only be used with stdin`() {
+ val code = """fun f () = println( "hello, world" )"""
+ val file = root.resolve("foo.kt")
+ file.writeText(code)
+
+ val exitCode =
+ Main(
+ emptyInput,
+ PrintStream(out),
+ PrintStream(err),
+ arrayOf("--stdin-name=bar.kt", file.toString()))
+ .run()
+
+ assertThat(file.readText()).isEqualTo(code)
+ assertThat(out.toString("UTF-8")).isEmpty()
+ assertThat(err.toString("UTF-8")).isEqualTo("Error: --stdin-name can only be used with stdin\n")
+ assertThat(exitCode).isEqualTo(1)
+ }
}
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 40f3646..37cbf57 100644
--- a/core/src/test/java/com/facebook/ktfmt/cli/ParsedArgsTest.kt
+++ b/core/src/test/java/com/facebook/ktfmt/cli/ParsedArgsTest.kt
@@ -42,29 +42,23 @@ class ParsedArgsTest {
@Test
fun `files to format are returned and unknown flags are reported`() {
- val out = ByteArrayOutputStream()
-
- val (fileNames, _) = ParsedArgs.parseOptions(PrintStream(out), arrayOf("foo.kt", "--unknown"))
+ val (parsed, out) = parseTestOptions("foo.kt", "--unknown")
- assertThat(fileNames).containsExactly("foo.kt")
+ assertThat(parsed.fileNames).containsExactly("foo.kt")
assertThat(out.toString()).isEqualTo("Unexpected option: --unknown\n")
}
@Test
fun `files to format are returned and flags starting with @ are reported`() {
- val out = ByteArrayOutputStream()
+ val (parsed, out) = parseTestOptions("foo.kt", "@unknown")
- val (fileNames, _) = ParsedArgs.parseOptions(PrintStream(out), arrayOf("foo.kt", "@unknown"))
-
- assertThat(fileNames).containsExactly("foo.kt")
+ assertThat(parsed.fileNames).containsExactly("foo.kt")
assertThat(out.toString()).isEqualTo("Unexpected option: @unknown\n")
}
@Test
fun `parseOptions uses default values when args are empty`() {
- val out = ByteArrayOutputStream()
-
- val parsed = ParsedArgs.parseOptions(PrintStream(out), arrayOf("foo.kt"))
+ val (parsed, _) = parseTestOptions("foo.kt")
val formattingOptions = parsed.formattingOptions
assertThat(formattingOptions.style).isEqualTo(FormattingOptions.Style.FACEBOOK)
@@ -76,57 +70,67 @@ class ParsedArgsTest {
assertThat(parsed.dryRun).isFalse()
assertThat(parsed.setExitIfChanged).isFalse()
+ assertThat(parsed.stdinName).isNull()
}
@Test
fun `parseOptions recognizes --dropbox-style and rejects unknown flags`() {
- val out = ByteArrayOutputStream()
+ val (parsed, out) = parseTestOptions("--dropbox-style", "foo.kt", "--unknown")
- val (fileNames, formattingOptions) =
- ParsedArgs.parseOptions(PrintStream(out), arrayOf("--dropbox-style", "foo.kt", "--unknown"))
-
- assertThat(fileNames).containsExactly("foo.kt")
- assertThat(formattingOptions.blockIndent).isEqualTo(4)
- assertThat(formattingOptions.continuationIndent).isEqualTo(4)
+ assertThat(parsed.fileNames).containsExactly("foo.kt")
+ assertThat(parsed.formattingOptions.blockIndent).isEqualTo(4)
+ assertThat(parsed.formattingOptions.continuationIndent).isEqualTo(4)
assertThat(out.toString()).isEqualTo("Unexpected option: --unknown\n")
}
@Test
fun `parseOptions recognizes --google-style`() {
- val out = ByteArrayOutputStream()
-
- val (_, formattingOptions) =
- ParsedArgs.parseOptions(PrintStream(out), arrayOf("--google-style", "foo.kt"))
-
- assertThat(formattingOptions).isEqualTo(Formatter.GOOGLE_FORMAT)
+ val (parsed, _) = parseTestOptions("--google-style", "foo.kt")
+ assertThat(parsed.formattingOptions).isEqualTo(Formatter.GOOGLE_FORMAT)
}
@Test
fun `parseOptions recognizes --dry-run`() {
- val out = ByteArrayOutputStream()
-
- val parsed = ParsedArgs.parseOptions(PrintStream(out), arrayOf("--dry-run", "foo.kt"))
-
+ val (parsed, _) = parseTestOptions("--dry-run", "foo.kt")
assertThat(parsed.dryRun).isTrue()
}
@Test
fun `parseOptions recognizes -n as --dry-run`() {
- val out = ByteArrayOutputStream()
-
- val parsed = ParsedArgs.parseOptions(PrintStream(out), arrayOf("-n", "foo.kt"))
-
+ val (parsed, _) = parseTestOptions("-n", "foo.kt")
assertThat(parsed.dryRun).isTrue()
}
@Test
fun `parseOptions recognizes --set-exit-if-changed`() {
- val out = ByteArrayOutputStream()
+ val (parsed, _) = parseTestOptions("--set-exit-if-changed", "foo.kt")
+ assertThat(parsed.setExitIfChanged).isTrue()
+ }
- val parsed =
- ParsedArgs.parseOptions(PrintStream(out), arrayOf("--set-exit-if-changed", "foo.kt"))
+ @Test
+ fun `parseOptions --stdin-name`() {
+ val (parsed, _) = parseTestOptions("--stdin-name=my/foo.kt")
+ assertThat(parsed.stdinName).isEqualTo("my/foo.kt")
+ }
- assertThat(parsed.setExitIfChanged).isTrue()
+ @Test
+ fun `parseOptions --stdin-name with empty value`() {
+ val (parsed, _) = parseTestOptions("--stdin-name=")
+ assertThat(parsed.stdinName).isEqualTo("")
+ }
+
+ @Test
+ fun `parseOptions --stdin-name without value`() {
+ val (parsed, out) = parseTestOptions("--stdin-name")
+ assertThat(out).isEqualTo("Found option '--stdin-name', expected '--stdin-name='\n")
+ assertThat(parsed.stdinName).isNull()
+ }
+
+ @Test
+ fun `parseOptions --stdin-name prefix`() {
+ val (parsed, out) = parseTestOptions("--stdin-namea")
+ assertThat(out).isEqualTo("Found option '--stdin-namea', expected '--stdin-name='\n")
+ assertThat(parsed.stdinName).isNull()
}
@Test
@@ -153,4 +157,9 @@ class ParsedArgsTest {
assertThat(parsed.setExitIfChanged).isTrue()
assertThat(parsed.fileNames).containsExactlyElementsIn(listOf("File1.kt", "File2.kt"))
}
+
+ private fun parseTestOptions(vararg args: String): Pair {
+ val out = ByteArrayOutputStream()
+ return Pair(ParsedArgs.parseOptions(PrintStream(out), arrayOf(*args)), out.toString())
+ }
}
diff --git a/core/src/test/java/com/facebook/ktfmt/format/FormatterTest.kt b/core/src/test/java/com/facebook/ktfmt/format/FormatterTest.kt
index 2096f50..88b16fe 100644
--- a/core/src/test/java/com/facebook/ktfmt/format/FormatterTest.kt
+++ b/core/src/test/java/com/facebook/ktfmt/format/FormatterTest.kt
@@ -47,7 +47,8 @@ class FormatterTest {
|println("Called with args:")
|
|args.forEach { println(File + "-") }
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `support script (kts) files with a shebang`() =
@@ -57,7 +58,8 @@ class FormatterTest {
|package foo
|
|println("Called")
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `call chains`() =
@@ -94,7 +96,8 @@ class FormatterTest {
| .add(1)
| .build()
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -116,7 +119,8 @@ class FormatterTest {
| doc.computeBreaks(
| output.commentsHelper, maxWidth, State(0))
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -139,7 +143,8 @@ class FormatterTest {
|) {
| val a = 0
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -170,7 +175,8 @@ class FormatterTest {
|
| ImmutableList.newBuilder().add(1).add(1).add(1).add(1).add(1).add(1).add(1).add(1).add(1).add(1).build()
| }
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
@@ -211,7 +217,8 @@ class FormatterTest {
| .add(1)
| .build()
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
// Don't add more tests here
@@ -225,7 +232,8 @@ class FormatterTest {
| var x: Int = 4
| val y = 0
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test fun `class without a body nor properties`() = assertFormatted("class Foo\n")
@@ -239,7 +247,8 @@ class FormatterTest {
"""fun interface MyRunnable {
| fun runIt()
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle complex fun interface without body`() =
@@ -258,7 +267,8 @@ class FormatterTest {
| fun method() {}
| class Bar
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `properties and fields with modifiers`() =
@@ -270,7 +280,8 @@ class FormatterTest {
| open var f3 = 0
| final var f4 = 0
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `properties with multiple modifiers`() =
@@ -279,7 +290,8 @@ class FormatterTest {
|class Foo(public open inner val p1: Int) {
| public open inner var f2 = 0
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `spaces around binary operations`() =
@@ -289,7 +301,8 @@ class FormatterTest {
| a = 5
| x + 1
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `breaking long binary operations`() =
@@ -310,7 +323,8 @@ class FormatterTest {
| value8) +
| value9
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -322,7 +336,8 @@ class FormatterTest {
| return expression1 != expression2 ||
| expression2 != expression1
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -341,7 +356,8 @@ class FormatterTest {
| "lazy" +
| "dog"
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -358,7 +374,8 @@ class FormatterTest {
| "over" +
| "the".."lazy" + "dog"
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -413,7 +430,8 @@ class FormatterTest {
| //
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -424,7 +442,8 @@ class FormatterTest {
|// a
|
|/* Another comment */
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `properties with accessors`() =
@@ -445,7 +464,8 @@ class FormatterTest {
| var zz = false
| private set
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `properties with accessors and semicolons on same line`() {
@@ -456,7 +476,8 @@ class FormatterTest {
| internal val a by lazy { 5 }; internal get
| var foo: Int; get() = 6; set(x) {};
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
@@ -469,7 +490,8 @@ class FormatterTest {
| get() = 6
| set(x) {}
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -485,7 +507,8 @@ class FormatterTest {
| "Hello there this is long"
| get() = field
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -498,7 +521,8 @@ class FormatterTest {
| a++
| a === b
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `package names stay in one line`() {
@@ -507,13 +531,15 @@ class FormatterTest {
| package com .example. subexample
|
|fun f() = 1
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
|package com.example.subexample
|
|fun f() = 1
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -527,15 +553,18 @@ class FormatterTest {
|import `nothing stops`.`us`.`from doing this`
|
|fun f() = `from doing this`()
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `safe dot operator expression`() =
- assertFormatted("""
+ assertFormatted(
+ """
|fun f() {
| node?.name
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `safe dot operator expression with normal`() =
@@ -544,7 +573,8 @@ class FormatterTest {
|fun f() {
| node?.name.hello
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `safe dot operator expression chain in expression function`() =
@@ -553,7 +583,8 @@ class FormatterTest {
|--------------------------------------------------
|fun f(number: Int) =
| Something.doStuff(number)?.size
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -575,7 +606,8 @@ class FormatterTest {
| foo.facebook.Foo
| .format()
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -626,7 +658,8 @@ class FormatterTest {
| .methodName4()
| .abcdefghijkl()
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -703,7 +736,8 @@ class FormatterTest {
| foo
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -725,27 +759,27 @@ class FormatterTest {
| }
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
- fun `don't one-line lambdas following parameter breaks`() =
+ fun `don't one-line lambdas following argument breaks`() =
assertFormatted(
"""
|------------------------------------------------------------------------
|class Foo : Bar() {
| fun doIt() {
- | // don't break in lambda, no parameter breaks found
+ | // don't break in lambda, no argument breaks found
| fruit.forEach { eat(it) }
|
- | // break in the lambda because the closing paren gets attached
- | // to the last argument
+ | // break in the lambda, without comma
| fruit.forEach(
| someVeryLongParameterNameThatWillCauseABreak,
| evenWithoutATrailingCommaOnTheParameterListSoLetsSeeIt) {
- | eat(it)
- | }
+ | eat(it)
+ | }
|
- | // break in the lambda
+ | // break in the lambda, with comma
| fruit.forEach(
| fromTheVine = true,
| ) {
@@ -782,7 +816,8 @@ class FormatterTest {
| }
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -800,26 +835,106 @@ class FormatterTest {
| }
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
- fun `no break between multi-line strings and their selectors`() =
+ fun `no forward propagation of breaks in call expressions (at trailing lambda)`() =
+ assertFormatted(
+ """
+ |--------------------------
+ |fun test() {
+ | foo_bar_baz__zip(b) {
+ | c
+ | }
+ | foo.bar(baz).zip(b) {
+ | c
+ | }
+ |}
+ |"""
+ .trimMargin(),
+ deduceMaxWidth = true)
+
+ @Test
+ fun `forward propagation of breaks in call expressions (at value args)`() =
+ assertFormatted(
+ """
+ |----------------------
+ |fun test() {
+ | foo_bar_baz__zip(
+ | b) {
+ | c
+ | }
+ |}
+ |
+ |fun test() {
+ | foo.bar(baz).zip(
+ | b) {
+ | c
+ | }
+ |}
+ |"""
+ .trimMargin(),
+ deduceMaxWidth = true)
+
+ @Test
+ fun `forward propagation of breaks in call expressions (at type args)`() =
+ assertFormatted(
+ """
+ |-------------------
+ |fun test() {
+ | foo_bar_baz__zip<
+ | A>(
+ | b) {
+ | c
+ | }
+ | foo.bar(baz).zip<
+ | A>(
+ | b) {
+ | c
+ | }
+ |}
+ |"""
+ .trimMargin(),
+ deduceMaxWidth = true)
+
+ @Test
+ fun `expected indent in methods following single-line strings`() =
assertFormatted(
"""
|-------------------------
+ |"Hello %s".format(
+ | someLongExpression)
+ |"""
+ .trimMargin(),
+ deduceMaxWidth = true)
+
+ @Test
+ fun `forced break between multi-line strings and their selectors`() =
+ assertFormatted(
+ """
+ |-------------------------
+ |val STRING =
+ | $TQ
+ | |foo
+ | |$TQ
+ | .wouldFit()
+ |
|val STRING =
- | ""${'"'}
+ | $TQ
| |foo
- | |""${'"'}.trimMargin()
+ | |----------------------------------$TQ
+ | .wouldntFit()
|
- |// This is a bug (line is longer than limit)
- |// that we don't know how to avoid, for now.
|val STRING =
- | ""${'"'}
+ | $TQ
| |foo
- | |----------------------------------""${'"'}.trimMargin()
- |""".trimMargin(),
+ | |$TQ
+ | .firstLink()
+ | .secondLink()
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -834,7 +949,8 @@ class FormatterTest {
| test */
|
|val x = FooBar.def { foosBars(bar) }
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
|import abc.def /*
@@ -845,7 +961,8 @@ class FormatterTest {
|import foo.bar // Test
|
|val x = FooBar.def { foosBars(bar) }
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -855,7 +972,8 @@ class FormatterTest {
"""
|import com.example.zab // test
|import com.example.foo ; val x = Sample(foo, zab)
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
@@ -863,7 +981,8 @@ class FormatterTest {
|import com.example.zab // test
|
|val x = Sample(foo, zab)
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -878,7 +997,8 @@ class FormatterTest {
|import com.example.wow
|
|val x = `if` { we.`when`(wow) }
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `backticks are ignored in import sort order ('as' directory)`() =
@@ -890,7 +1010,8 @@ class FormatterTest {
|import com.example.a as wow
|
|val x = `if` { we.`when`(wow) }
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `imports are deduplicated`() {
@@ -905,7 +1026,8 @@ class FormatterTest {
|import com.example.a as `when`
|
|val x = `if` { we.`when`(wow) } ?: b
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
|import com.example.a as `if`
@@ -916,7 +1038,8 @@ class FormatterTest {
|import com.example.b.*
|
|val x = `if` { we.`when`(wow) } ?: b
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -939,7 +1062,8 @@ class FormatterTest {
| `if` { bar }
| val x = unused()
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
|import com.used.FooBarBaz as Baz
@@ -954,7 +1078,8 @@ class FormatterTest {
| `if` { bar }
| val x = unused()
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -971,7 +1096,8 @@ class FormatterTest {
|fun test() {
| foo(CONSTANT, Sample())
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
|package com.example
@@ -982,7 +1108,8 @@ class FormatterTest {
|fun test() {
| foo(CONSTANT, Sample())
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -1019,7 +1146,8 @@ class FormatterTest {
| * @throws AnException
| */
|class Dummy
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
|package com.example.kdoc
@@ -1038,20 +1166,54 @@ class FormatterTest {
| *
| * Old {@link JavaDocLink} that gets removed.
| *
- | * @throws AnException
- | * @exception Sample.SampleException
| * @param unused [Param]
- | * @property JavaDocLink [Param]
| * @return [Unit] as [ReturnedValue]
+ | * @property JavaDocLink [Param]
+ | * @throws AnException
+ | * @throws AnException
+ | * @exception Sample.SampleException
| * @sample Example
| * @see Bar for more info
- | * @throws AnException
| */
|class Dummy
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
+ @Test
+ fun `keep import elements only mentioned in kdoc, single line`() {
+ assertFormatted(
+ """
+ |import com.shopping.Bag
+ |
+ |/**
+ | * Some summary.
+ | *
+ | * @param count you can fit this many in a [Bag]
+ | */
+ |fun fetchBananas(count: Int)
+ |"""
+ .trimMargin())
+ }
+
+ @Test
+ fun `keep import elements only mentioned in kdoc, multiline`() {
+ assertFormatted(
+ """
+ |import com.shopping.Bag
+ |
+ |/**
+ | * Some summary.
+ | *
+ | * @param count this is how many of these wonderful fruit you can fit into the useful object that
+ | * you may refer to as a [Bag]
+ | */
+ |fun fetchBananas(count: Int)
+ |"""
+ .trimMargin())
+ }
+
@Test
fun `keep component imports`() =
assertFormatted(
@@ -1063,7 +1225,8 @@ class FormatterTest {
|import com.example.component3
|import com.example.component4
|import com.example.component5
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `keep operator imports`() =
@@ -1101,7 +1264,8 @@ class FormatterTest {
|import com.example.timesAssign
|import com.example.unaryMinus
|import com.example.unaryPlus
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `keep unused imports when formatting options has feature turned off`() {
@@ -1116,7 +1280,8 @@ class FormatterTest {
|import com.unused.b as we
|import com.unused.bar // test
|import com.unused.`class`
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code)
.withOptions(FormattingOptions(removeUnusedImports = false))
@@ -1138,7 +1303,8 @@ class FormatterTest {
|// trailing comment
|
|val x = Sample(abc, bcd)
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
|package com.facebook.ktfmt
@@ -1153,7 +1319,8 @@ class FormatterTest {
|// trailing comment
|
|val x = Sample(abc, bcd)
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -1166,7 +1333,8 @@ class FormatterTest {
|/*
|bar
|*/
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `basic annotations`() =
@@ -1179,7 +1347,8 @@ class FormatterTest {
| @Fancy val a = 1 + foo
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `function calls with multiple arguments`() =
@@ -1193,7 +1362,8 @@ class FormatterTest {
| 123456789012345678901234567890,
| 123456789012345678901234567890)
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `function calls with multiple named arguments`() =
@@ -1207,7 +1377,8 @@ class FormatterTest {
| b = 23456789012345678901234567890,
| c = 3456789012345678901234567890)
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `named arguments indent their value expression`() =
@@ -1221,7 +1392,8 @@ class FormatterTest {
| print()
| },
| duration = duration)
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `Arguments are blocks`() =
@@ -1247,7 +1419,8 @@ class FormatterTest {
| initializer = property.initializer)
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -1260,7 +1433,8 @@ class FormatterTest {
| println(number)
| })
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `anonymous function with receiver`() =
@@ -1272,7 +1446,8 @@ class FormatterTest {
| println(this)
| })
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `when() with a subject expression`() =
@@ -1290,7 +1465,8 @@ class FormatterTest {
| }
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `when() expression with complex predicates`() =
@@ -1305,7 +1481,8 @@ class FormatterTest {
| }
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `when() expression with several conditions`() =
@@ -1318,7 +1495,8 @@ class FormatterTest {
| else -> print(0)
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `when() expression with is and in`() =
@@ -1336,7 +1514,8 @@ class FormatterTest {
| else -> print(3)
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `when() expression with enum values`() =
@@ -1349,7 +1528,8 @@ class FormatterTest {
| else -> print(3)
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `when() expression with generic matcher and exhaustive`() =
@@ -1361,7 +1541,8 @@ class FormatterTest {
| is Failure -> print(2)
| }.exhaustive
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `when() expression with multiline condition`() =
@@ -1384,7 +1565,8 @@ class FormatterTest {
| 2 -> print(2)
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -1402,7 +1584,8 @@ class FormatterTest {
| doItOnce()
| doItTwice()
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `when() expression storing in local variable`() =
@@ -1414,7 +1597,8 @@ class FormatterTest {
| is Failure -> print(2)
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `line breaks inside when expressions and conditions`() =
@@ -1435,7 +1619,8 @@ class FormatterTest {
| }
| .build()
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `function return types`() =
@@ -1444,7 +1629,8 @@ class FormatterTest {
|fun f1(): Int = 0
|
|fun f2(): Int {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `multi line function without a block body`() =
@@ -1457,7 +1643,8 @@ class FormatterTest {
|
|fun shortFun(): Int =
| 1234567 + 1234567
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -1480,7 +1667,8 @@ class FormatterTest {
| //
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -1496,7 +1684,8 @@ class FormatterTest {
|class Derived4 : Super1()
|
|class Derived5 : Super3()
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `list of superclasses over multiple lines`() =
@@ -1519,20 +1708,25 @@ class FormatterTest {
|
|class Derived5 :
| Super3()
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
fun `annotations with parameters`() =
- assertFormatted("""
+ assertFormatted(
+ """
|@AnnWithArrayValue(1, 2, 3) class C
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `method modifiers`() =
- assertFormatted("""
+ assertFormatted(
+ """
|override internal fun f() {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `class modifiers`() =
@@ -1545,7 +1739,8 @@ class FormatterTest {
|final class Foo
|
|open class Foo
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `kdoc comments`() {
@@ -1556,11 +1751,14 @@ class FormatterTest {
| */ class F {
|
| }
- |""".trimMargin()
- val expected = """
+ |"""
+ .trimMargin()
+ val expected =
+ """
|/** foo */
|class F {}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -1573,11 +1771,14 @@ class FormatterTest {
| */ class F {
|
| }
- |""".trimMargin()
- val expected = """
+ |"""
+ .trimMargin()
+ val expected =
+ """
|/** foo /* bla */ */
|class F {}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -1591,7 +1792,8 @@ class FormatterTest {
| * ```
| */
|fun foo() {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `formatting kdoc doesn't add p HTML tags`() =
@@ -1605,7 +1807,8 @@ class FormatterTest {
| *
| * On the other hand, we respect existing tags, and don't remove them.
| */
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `formatting kdoc preserves lists`() =
@@ -1618,7 +1821,8 @@ class FormatterTest {
| *
| * This is another paragraph
| */
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `formatting kdoc lists with line wraps breaks and merges correctly`() {
@@ -1632,66 +1836,20 @@ class FormatterTest {
| *
| * This is another paragraph
| */
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
|/**
| * Here are some fruit I like:
| * - Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana
- | * Banana Banana Banana Banana Banana
+ | * Banana Banana Banana Banana Banana
| * - Apple Apple Apple Apple Apple Apple
| *
| * This is another paragraph
| */
- |""".trimMargin()
- assertThatFormatting(code).isEqualTo(expected)
- }
-
- @Test
- fun `too many spaces on list continuation mean it's a code block, so mark it accordingly`() {
- val code =
- """
- |/**
- | * Here are some fruit I like:
- | * - Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana
- | * Banana Banana Banana Banana Banana
- | */
- |""".trimMargin()
- val expected =
- """
- |/**
- | * Here are some fruit I like:
- | * - Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana Banana
- | * ```
- | * Banana Banana Banana Banana Banana
- | * ```
- | */
- |""".trimMargin()
- assertThatFormatting(code).isEqualTo(expected)
- }
-
- @Test
- fun `add explicit code markers around indented code`() {
- val code =
- """
- |/**
- | * This is a code example:
- | *
- | * this_is_code()
- | *
- | * This is not code again
- | */
- |""".trimMargin()
- val expected =
- """
- |/**
- | * This is a code example:
- | * ```
- | * this_is_code()
- | * ```
- | * This is not code again
- | */
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -1706,7 +1864,8 @@ class FormatterTest {
| *
| * This is another paragraph
| */
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `formatting kdoc preserves numbered`() =
@@ -1719,7 +1878,8 @@ class FormatterTest {
| *
| * This is another paragraph
| */
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `formatting kdoc with markdown errors`() =
@@ -1727,7 +1887,8 @@ class FormatterTest {
"""
|/** \[ */
|fun markdownError() = Unit
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `return statement with value`() =
@@ -1736,7 +1897,8 @@ class FormatterTest {
|fun random(): Int {
| return 4
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `return statement without value`() =
@@ -1746,7 +1908,8 @@ class FormatterTest {
| print(b)
| return
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `return expression without value`() =
@@ -1755,7 +1918,8 @@ class FormatterTest {
|fun print(b: Boolean?) {
| print(b ?: return)
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `if statement without else`() =
@@ -1766,7 +1930,8 @@ class FormatterTest {
| println(b)
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `if statement with else`() =
@@ -1779,7 +1944,8 @@ class FormatterTest {
| println(1)
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `if expression with else`() =
@@ -1794,7 +1960,8 @@ class FormatterTest {
| } else 2)
| return if (b) 1 else 2
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `if expression with break before else`() =
@@ -1808,7 +1975,8 @@ class FormatterTest {
| return if (a + b < 20) a + b
| else c
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -1827,7 +1995,8 @@ class FormatterTest {
| a + b
| else c
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -1845,7 +2014,8 @@ class FormatterTest {
| println("Everything is okay")
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `if expression with multiline condition`() =
@@ -1866,7 +2036,8 @@ class FormatterTest {
| bar()
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -1879,14 +2050,17 @@ class FormatterTest {
| myVariable =
| function1(4, 60, 8) + function2(57, 39, 20)
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
fun `A program that tickled a bug in KotlinInput`() =
- assertFormatted("""
+ assertFormatted(
+ """
|val x = 2
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `a few variations of constructors`() =
@@ -1910,7 +2084,8 @@ class FormatterTest {
| number5: Int,
| number6: Int
|) {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `a primary constructor without a class body `() =
@@ -1920,7 +2095,8 @@ class FormatterTest {
|data class Foo(
| val number: Int = 0
|)
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -1933,7 +2109,8 @@ class FormatterTest {
| val number: Int = 0
| )
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -1946,7 +2123,8 @@ class FormatterTest {
| val number: Int = 0
| ) {}
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -1960,7 +2138,8 @@ class FormatterTest {
| val title: String,
| val offspring2: List
|) {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `a constructor with keyword and many arguments over breaking to next line`() =
@@ -1974,7 +2153,8 @@ class FormatterTest {
| val offspring: List,
| val foo: String
|) {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `a constructor with many arguments over multiple lines`() =
@@ -1989,7 +2169,8 @@ class FormatterTest {
| val title: String,
| val offspring: List
|) {}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2002,7 +2183,8 @@ class FormatterTest {
| println("built")
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `a secondary constructor with many arguments over multiple lines`() =
@@ -2018,7 +2200,8 @@ class FormatterTest {
| val offspring: List
| )
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2041,7 +2224,8 @@ class FormatterTest {
| offspring,
| offspring)
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2056,7 +2240,8 @@ class FormatterTest {
| Foo.createSpeciallyDesignedParameter(),
| )
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2073,7 +2258,8 @@ class FormatterTest {
| init(attrs)
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle calling super constructor in secondary constructor`() =
@@ -2082,7 +2268,8 @@ class FormatterTest {
|class Foo : Bar {
| internal constructor(number: Int) : super(number) {}
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle super statement with with type argument`() =
@@ -2093,7 +2280,8 @@ class FormatterTest {
| super.doIt()
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle super statement with with label argument`() =
@@ -2109,7 +2297,8 @@ class FormatterTest {
| }
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `primary constructor without parameters with a KDoc`() =
@@ -2118,12 +2307,16 @@ class FormatterTest {
|class Class
|/** A comment */
|constructor() {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
- fun `handle objects`() = assertFormatted("""
+ fun `handle objects`() =
+ assertFormatted(
+ """
|object Foo(n: Int) {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle object expression`() =
@@ -2132,7 +2325,8 @@ class FormatterTest {
|fun f(): Any {
| return object : Adapter() {}
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle object expression in parenthesis`() =
@@ -2141,7 +2335,8 @@ class FormatterTest {
|fun f(): Any {
| return (object : Adapter() {})
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle array indexing operator`() =
@@ -2151,7 +2346,8 @@ class FormatterTest {
| a[3]
| b[3, 4]
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `keep array indexing grouped with expression is possible`() =
@@ -2172,7 +2368,8 @@ class FormatterTest {
| .foobar[1, 2, 3]
| .barfoo[3, 2, 1]
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2194,7 +2391,8 @@ class FormatterTest {
| .foobar[1, 2, 3]
| .barfoo[3, 2, 1]
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2211,7 +2409,8 @@ class FormatterTest {
| oneTwoThreeFourFiveSixSeven(
| foo, bar, zed, boo)
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
fun `chains with derferences and array indexing`() =
@@ -2227,7 +2426,8 @@ class FormatterTest {
| .feep[1]
| as Boo
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2242,7 +2442,8 @@ class FormatterTest {
| println(it)
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2255,7 +2456,8 @@ class FormatterTest {
| println(it)
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2281,7 +2483,8 @@ class FormatterTest {
| FooFoo.foooooooo()
| .foooooooo()
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2298,7 +2501,8 @@ class FormatterTest {
| .someItems[0]
| .doIt()
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2313,7 +2517,8 @@ class FormatterTest {
| somePropertiesProvider, somePropertyCallbacks]
| .also { _somePropertyWithBackingOne = it }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `array access in middle of chain and end of it behaves similarly`() =
@@ -2328,7 +2533,8 @@ class FormatterTest {
| println()
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2340,13 +2546,16 @@ class FormatterTest {
|}
|
|fun doItWithNulls(a: String, b: String?) {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `nullable function type`() =
- assertFormatted("""
+ assertFormatted(
+ """
|var listener: ((Boolean) -> Unit)? = null
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `redundant parenthesis in function types`() =
@@ -2355,7 +2564,8 @@ class FormatterTest {
|val a: (Int) = 7
|
|var listener: ((Boolean) -> Unit) = foo
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle string literals`() =
@@ -2366,7 +2576,8 @@ class FormatterTest {
| println("Hello! ${'$'}world")
| println("Hello! ${'$'}{"wor" + "ld"}")
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle multiline string literals`() =
@@ -2376,7 +2587,8 @@ class FormatterTest {
| println(${"\"".repeat(3)}Hello
| world!${"\"".repeat(3)})
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `Trailing whitespaces are preserved in multiline strings`() {
@@ -2401,13 +2613,14 @@ class FormatterTest {
fun `Consecutive line breaks in multiline strings are preserved`() =
assertFormatted(
"""
- |val x = ""${'"'}
+ |val x = $TQ
|
|
|
|Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
- |""${'"'}
- |""".trimMargin())
+ |$TQ
+ |"""
+ .trimMargin())
@Test
fun `Trailing spaces in a comment are not preserved`() {
@@ -2419,11 +2632,13 @@ class FormatterTest {
@Test
fun `Code with tombstones is not supported`() {
- val code = """
+ val code =
+ """
|fun good() {
| // ${'\u0003'}
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
try {
Formatter.format(code)
fail()
@@ -2444,7 +2659,8 @@ class FormatterTest {
|}
|
|class Foo
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle for loops`() =
@@ -2455,7 +2671,8 @@ class FormatterTest {
| println(i)
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle for loops with long dot chains`() =
@@ -2476,7 +2693,8 @@ class FormatterTest {
| println(child)
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2494,7 +2712,8 @@ class FormatterTest {
| number = 2 * number
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2512,7 +2731,8 @@ class FormatterTest {
| number = 2 * number
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2527,7 +2747,8 @@ class FormatterTest {
| }
| .methodCall()
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `keep last expression in qualified indented`() =
@@ -2543,7 +2764,8 @@ class FormatterTest {
| Foo.doIt()
| .doThat())
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2560,7 +2782,8 @@ class FormatterTest {
| red.orange.yellow()
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2590,7 +2813,8 @@ class FormatterTest {
| red.orange.yellow()
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2622,7 +2846,8 @@ class FormatterTest {
| red.orange.yellow()
| // this is a comment
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2646,7 +2871,8 @@ class FormatterTest {
| action2()
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2663,7 +2889,8 @@ class FormatterTest {
| foo1, foo2, foo3)
| .doThat()
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2680,7 +2907,8 @@ class FormatterTest {
| doStuff()
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2701,7 +2929,8 @@ class FormatterTest {
| a + a
| })
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `Qualified type`() =
@@ -2712,7 +2941,8 @@ class FormatterTest {
| var x: Map.Entry
| var x: List.Iterator
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle destructuring declaration in for loop`() =
@@ -2721,7 +2951,8 @@ class FormatterTest {
|fun f(a: List>) {
| for ((x, y: Int) in a) {}
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle function references`() =
@@ -2744,7 +2975,8 @@ class FormatterTest {
| invoke(a, b, c)::
| functionName
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2762,7 +2994,8 @@ class FormatterTest {
|}
|
|class `more spaces`
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle annotations with arguments`() =
@@ -2777,7 +3010,8 @@ class FormatterTest {
|class Test {
| //
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `no newlines after annotations if entire expr fits in one line`() =
@@ -2834,7 +3068,8 @@ class FormatterTest {
| println("")
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2845,7 +3080,8 @@ class FormatterTest {
|@Suppress("UnsafeCast")
|val ClassA.methodA
| get() = foo as Bar
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2858,7 +3094,8 @@ class FormatterTest {
| @LongLongLongLongLongAnnotation
| private val ROW_HEIGHT = 72
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -2866,7 +3103,8 @@ class FormatterTest {
assertFormatted(
"""
|val callback: (@Anno List<@JvmSuppressWildcards String>) -> Unit = foo
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `annotations on type parameters`() =
@@ -2875,7 +3113,8 @@ class FormatterTest {
|class Foo<@Anno out @Anno T, @Anno in @Anno U> {
| inline fun <@Anno reified @Anno X, @Anno reified @Anno Y> bar() {}
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `annotations on type constraints`() =
@@ -2884,19 +3123,24 @@ class FormatterTest {
|class Foo where U : @Anno Kip, U : @Anno Qux {
| fun bar() where U : @Anno Kip, U : @Anno Qux {}
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `annotations on type arguments`() =
- assertFormatted("""
+ assertFormatted(
+ """
|fun foo(x: Foo) {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `annotations on destructuring declaration elements`() =
- assertFormatted("""
+ assertFormatted(
+ """
|val x = { (@Anno x, @Anno y) -> x }
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `annotations on exceptions`() =
@@ -2909,7 +3153,8 @@ class FormatterTest {
| //
| } catch (@Suppress("GeneralException") e: Exception) {}
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `Unary prefix expressions`() =
@@ -2940,7 +3185,8 @@ class FormatterTest {
| !++a
| !--a
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `Unary postfix expressions`() =
@@ -2956,7 +3202,8 @@ class FormatterTest {
|
| a!! !!
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle wildcard generics`() =
@@ -2966,39 +3213,59 @@ class FormatterTest {
| val l: List<*>
| val p: Pair<*, *>
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
+
+ @Test
+ fun `handle intersection generics`() =
+ assertFormatted(
+ """
+ |fun f() {
+ | val l: Decl
+ | val p = Ctor
+ |}
+ |"""
+ .trimMargin())
@Test
fun `handle covariant and contravariant type arguments`() =
- assertFormatted("""
+ assertFormatted(
+ """
|val p: Pair
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle covariant and contravariant type parameters`() =
- assertFormatted("""
+ assertFormatted(
+ """
|class Foo
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle bounds for type parameters`() =
- assertFormatted("""
+ assertFormatted(
+ """
|class Foo, out S : Any?>
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle compound generic bounds on classes`() =
assertFormatted(
"""
|class Foo(n: Int) where T : Bar, T : FooBar {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle compound generic bounds on functions`() =
assertFormatted(
"""
|fun foo(n: Int) where T : Bar, T : FooBar {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle compound generic bounds on properties`() =
@@ -3008,7 +3275,8 @@ class FormatterTest {
| get() {
| return 2 * sum()
| }
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle compound generic bounds on class with delegate`() =
@@ -3016,7 +3284,8 @@ class FormatterTest {
"""
|class Foo() : Bar by bar
|where T : Qux
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `explicit type on property getter`() =
@@ -3026,7 +3295,8 @@ class FormatterTest {
| val silly: Int
| get(): Int = 1
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle method calls with lambda arg only`() =
@@ -3035,7 +3305,8 @@ class FormatterTest {
|fun f() {
| val a = g { 1 + 1 }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle method calls value args and a lambda arg`() =
@@ -3044,7 +3315,8 @@ class FormatterTest {
|fun f() {
| val a = g(1, 2) { 1 + 1 }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle top level constants`() =
@@ -3056,7 +3328,8 @@ class FormatterTest {
|const val b = "a"
|
|val a = 5
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -3066,7 +3339,8 @@ class FormatterTest {
|fun f() {
| val b = { x: Int, y: Int -> x + y }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `avoid newline before lambda argument if it is named`() =
@@ -3078,9 +3352,12 @@ class FormatterTest {
| lambdaArgument = {
| step1()
| step2()
- | }) { it.doIt() }
+ | }) {
+ | it.doIt()
+ | }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle labeled this pointer`() =
@@ -3091,13 +3368,16 @@ class FormatterTest {
| g { println(this@Foo) }
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle extension and operator functions`() =
- assertFormatted("""
+ assertFormatted(
+ """
|operator fun Point.component1() = x
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle extension methods with very long names`() =
@@ -3111,7 +3391,8 @@ class FormatterTest {
| n: Int,
| f: Float
|) {}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -3120,13 +3401,16 @@ class FormatterTest {
"""
|val Int.isPrime: Boolean
| get() = runMillerRabinPrimality(this)
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `generic extension property`() =
- assertFormatted("""
+ assertFormatted(
+ """
|val List.twiceSize = 2 * size()
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle file annotations`() =
@@ -3141,7 +3425,8 @@ class FormatterTest {
|class Foo {
| val a = example2("and 1")
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle init block`() =
@@ -3152,20 +3437,24 @@ class FormatterTest {
| println("Init!")
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle interface delegation`() =
assertFormatted(
"""
|class MyList(impl: List) : Collection by impl
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle property delegation`() =
- assertFormatted("""
+ assertFormatted(
+ """
|val a by lazy { 1 + 1 }
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle property delegation with type and breaks`() =
@@ -3186,7 +3475,8 @@ class FormatterTest {
|
|val importantValue: Int by
| doIt(1 + 1)
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -3205,7 +3495,8 @@ class FormatterTest {
| var httpClient: OkHttpClient
|}
|
- """.trimMargin())
+ """
+ .trimMargin())
@Test
fun `handle parameters with annoations with parameters`() =
@@ -3217,7 +3508,8 @@ class FormatterTest {
| }
|}
|
- """.trimMargin())
+ """
+ .trimMargin())
@Test
fun `handle lambda types`() =
@@ -3230,13 +3522,16 @@ class FormatterTest {
|val listener3: (Int, Double) -> Int = { a, b -> a }
|
|val listener4: Int.(Int, Boolean) -> Unit
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle unicode in string literals`() =
- assertFormatted("""
+ assertFormatted(
+ """
|val a = "\uD83D\uDC4D"
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle casting`() =
@@ -3248,7 +3543,8 @@ class FormatterTest {
| doIt(o as Int)
| doIt(o as? Int)
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle casting with breaks`() =
@@ -3294,7 +3590,8 @@ class FormatterTest {
|val a =
| l.sOrNull() is
| SomethingLongEnough
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -3305,7 +3602,8 @@ class FormatterTest {
|fun doIt(o: Object) {
| //
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle try, catch and finally`() =
@@ -3320,7 +3618,8 @@ class FormatterTest {
| println("finally")
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle infix methods`() =
@@ -3329,7 +3628,8 @@ class FormatterTest {
|fun numbers() {
| (0 until 100).size
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle while loops`() =
@@ -3340,7 +3640,8 @@ class FormatterTest {
| println("Everything is okay")
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle do while loops`() =
@@ -3353,7 +3654,8 @@ class FormatterTest {
|
| do while (1 < 2)
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle break and continue`() =
@@ -3369,7 +3671,8 @@ class FormatterTest {
| }
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle all kinds of labels and jumps`() =
@@ -3391,7 +3694,8 @@ class FormatterTest {
| return@map 2 * it
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `don't crash on top level statements with semicolons`() {
@@ -3404,7 +3708,8 @@ class FormatterTest {
|foo { 0 };
|
|val fill = 0;
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
|val x = { 0 }
@@ -3414,7 +3719,8 @@ class FormatterTest {
|foo { 0 }
|
|val fill = 0
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -3431,7 +3737,8 @@ class FormatterTest {
|
| fun isOne(): Boolean = this == ONE
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
|enum class SemiColonIsNotRequired {
@@ -3445,7 +3752,8 @@ class FormatterTest {
|
| fun isOne(): Boolean = this == ONE
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -3455,24 +3763,26 @@ class FormatterTest {
"""
|fun f() {
| val x = ";"
- | val x = ""${'"'} don't touch ; in raw strings ""${'"'}
+ | val x = $TQ don't touch ; in raw strings $TQ
|}
|
|// Don't touch ; inside comments.
|
|/** Don't touch ; inside comments. */
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
|fun f() {
| val x = ";"
- | val x = ""${'"'} don't touch ; in raw strings ""${'"'}
+ | val x = $TQ don't touch ; in raw strings $TQ
|}
|
|// Don't touch ; inside comments.
|
|/** Don't touch ; inside comments. */
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -3491,7 +3801,8 @@ class FormatterTest {
| else
| ;
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
|fun f() {
@@ -3503,11 +3814,12 @@ class FormatterTest {
| if (true) ;
| if (true)
| /** a */
- | ;
+ | ;
|
| if (true) else ;
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -3539,7 +3851,8 @@ class FormatterTest {
| // Literally any callable expression is dangerous
| val x = (if (cond) x::foo else x::bar); { dead -> lambda }
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
|fun f() {
@@ -3579,7 +3892,8 @@ class FormatterTest {
| val x = (if (cond) x::foo else x::bar);
| { dead -> lambda }
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -3603,9 +3917,11 @@ class FormatterTest {
| someLongVariableName.let {
| someReallyLongFunctionNameThatMakesThisNotFitInOneLineWithTheAboveVariable();
| }
+ | if (cond) ; else 6
|} ;
|
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
|package org.examples
@@ -3630,24 +3946,29 @@ class FormatterTest {
| someLongVariableName.let {
| someReallyLongFunctionNameThatMakesThisNotFitInOneLineWithTheAboveVariable()
| }
+ | if (cond) else 6
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@Test
fun `pretty-print after dropping redundant semicolons`() {
- val code = """
+ val code =
+ """
|fun f() {
| val veryLongName = 5;
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
|fun f() {
| val veryLongName = 5
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).withOptions(FormattingOptions(maxWidth = 22)).isEqualTo(expected)
}
@@ -3658,7 +3979,8 @@ class FormatterTest {
|fun f() {
| a { println("a") }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle multi statement lambdas`() =
@@ -3670,7 +3992,8 @@ class FormatterTest {
| println("b")
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle multi line one statement lambda`() =
@@ -3682,7 +4005,8 @@ class FormatterTest {
| println(foo.bar.boom)
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -3695,7 +4019,8 @@ class FormatterTest {
| return
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `properly break fully qualified nested user types`() =
@@ -3710,7 +4035,8 @@ class FormatterTest {
| Int, Nothing>,
| Nothing>> =
| DUMMY
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -3740,7 +4066,8 @@ class FormatterTest {
| .sum
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle multi line lambdas with explicit args`() =
@@ -3752,7 +4079,8 @@ class FormatterTest {
| x + y
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -3763,7 +4091,8 @@ class FormatterTest {
| g { (a, b): List -> a }
| g { (a, b): List, (c, d): List -> a }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle parenthesis in lambda calls for now`() =
@@ -3772,7 +4101,8 @@ class FormatterTest {
|fun f() {
| a() { println("a") }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle chaining of calls with lambdas`() =
@@ -3788,7 +4118,8 @@ class FormatterTest {
| }
| .sum
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle break of lambda args per line with indentation`() =
@@ -3812,7 +4143,8 @@ class FormatterTest {
| doIt()
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -3830,7 +4162,8 @@ class FormatterTest {
| doIt()
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -3845,7 +4178,8 @@ class FormatterTest {
| .find { it.contains(someSearchValue) }
| ?: someDefaultValue
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -3861,7 +4195,8 @@ class FormatterTest {
| // this is a comment
| .doItTwice()
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -3875,7 +4210,8 @@ class FormatterTest {
|inline fun foo2(t: T) {
| println(t)
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle suspended types`() =
@@ -3888,7 +4224,8 @@ class FormatterTest {
|inline fun foo(noinline block: suspend () -> R): suspend () -> R
|
|inline fun bar(noinline block: (suspend () -> R)?): (suspend () -> R)?
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle simple enum classes`() =
@@ -3899,7 +4236,8 @@ class FormatterTest {
| FALSE,
| FILE_NOT_FOUND,
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle enum class with functions`() =
@@ -3913,7 +4251,8 @@ class FormatterTest {
| return true
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle enum with annotations`() =
@@ -3923,7 +4262,8 @@ class FormatterTest {
| @True TRUE,
| @False @WhatIsTruth FALSE,
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle enum constructor calls`() =
@@ -3933,7 +4273,8 @@ class FormatterTest {
| TRUE("true"),
| FALSE("false", false),
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle enum entries with body`() =
@@ -3945,20 +4286,25 @@ class FormatterTest {
| },
| FISH(false) {},
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle empty enum`() =
- assertFormatted("""
+ assertFormatted(
+ """
|enum class YTho {
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `expect enum class`() =
- assertFormatted("""
+ assertFormatted(
+ """
|expect enum class ExpectedEnum
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `enum without trailing comma`() =
@@ -3967,7 +4313,8 @@ class FormatterTest {
|enum class Highlander {
| ONE
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `enum comma and semicolon`() {
@@ -3976,13 +4323,15 @@ class FormatterTest {
|enum class Highlander {
| ONE,;
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
.isEqualTo(
"""
|enum class Highlander {
| ONE,
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
}
@Test
@@ -3996,7 +4345,8 @@ class FormatterTest {
|
| fun f() {}
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle varargs and spread operator`() =
@@ -4006,7 +4356,8 @@ class FormatterTest {
| foo2(*args)
| foo3(options = *args)
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle typealias`() =
@@ -4019,7 +4370,8 @@ class FormatterTest {
|typealias PairPair = Pair, X>
|
|class Foo
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -4029,7 +4381,8 @@ class FormatterTest {
|fun x(): dynamic = "x"
|
|val dyn: dynamic = 1
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle class expression with generics`() =
@@ -4038,7 +4391,8 @@ class FormatterTest {
|fun f() {
| println(Array::class.java)
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `ParseError contains correct line and column numbers`() {
@@ -4050,7 +4404,8 @@ class FormatterTest {
|}
|
|fn (
- |""".trimMargin()
+ |"""
+ .trimMargin()
try {
Formatter.format(code)
fail()
@@ -4069,7 +4424,8 @@ class FormatterTest {
|fun good() {
| return@ 5
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
try {
Formatter.format(code)
fail()
@@ -4090,7 +4446,8 @@ class FormatterTest {
| return (@Fancy 1)
| }
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `annotations on function types`() =
@@ -4113,7 +4470,8 @@ class FormatterTest {
| (@field:[Inject Named("WEB_VIEW")]
| (x) -> Unit)
|) {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle annotations with use-site targets`() =
@@ -4124,7 +4482,8 @@ class FormatterTest {
|
| @set:Magic(name = "Jane") var field: String
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle annotations mixed with keywords since we cannot reorder them for now`() =
@@ -4135,7 +4494,8 @@ class FormatterTest {
|public @Magic(1) final class Foo
|
|@Magic(1) public final class Foo
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle annotations more`() =
@@ -4154,7 +4514,8 @@ class FormatterTest {
| @Annotation // test a comment after annotations
| return 5
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -4191,7 +4552,8 @@ class FormatterTest {
| add(20) &&
| add(30)
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -4207,7 +4569,8 @@ class FormatterTest {
|fun f() {
| add(10)
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `annotated class declarations`() =
@@ -4221,14 +4584,16 @@ class FormatterTest {
|// Foo
|@Anno("param")
|class F
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle type arguments in annotations`() =
assertFormatted(
"""
|@TypeParceler() class MyClass {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle one line KDoc`() =
@@ -4236,7 +4601,8 @@ class FormatterTest {
"""
|/** Hi, I am a one line kdoc */
|class MyClass {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle KDoc with Link`() =
@@ -4244,7 +4610,8 @@ class FormatterTest {
"""
|/** This links to [AnotherClass] */
|class MyClass {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle KDoc with paragraphs`() =
@@ -4256,7 +4623,8 @@ class FormatterTest {
| * There's a space line to preserve between them
| */
|class MyClass {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle KDoc with blocks`() =
@@ -4269,7 +4637,8 @@ class FormatterTest {
| * @param[param2] this is param2
| */
|class MyClass {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle KDoc with code examples`() =
@@ -4277,7 +4646,6 @@ class FormatterTest {
"""
|/**
| * This is how you write a simple hello world in Kotlin:
- | *
| * ```
| * fun main(args: Array) {
| * println("Hello World!")
@@ -4285,13 +4653,16 @@ class FormatterTest {
| * ```
| *
| * Amazing ah?
+ | *
| * ```
| * fun `code can be with a blank line above it` () {}
| * ```
+ | *
| * Or after it!
| */
|class MyClass {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle KDoc with tagged code examples`() =
@@ -4305,7 +4676,8 @@ class FormatterTest {
| * ```
| */
|class MyClass {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle stray code markers in lines and produce stable output`() {
@@ -4318,7 +4690,8 @@ class FormatterTest {
| * ```
| */
|class MyClass {}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertFormatted(Formatter.format(code))
}
@@ -4333,7 +4706,8 @@ class FormatterTest {
| * ```
| */
|class MyClass {}
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
|/**
@@ -4343,7 +4717,8 @@ class FormatterTest {
| * ```
| */
|class MyClass {}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -4357,7 +4732,8 @@ class FormatterTest {
| * foo ``` wow
| */
|class MyClass {}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertFormatted(Formatter.format(code))
}
@@ -4367,7 +4743,8 @@ class FormatterTest {
"""
|/** Doc line with a reference to [AnotherClass] in the middle of a sentence */
|class MyClass {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle KDoc with links one after another`() =
@@ -4375,7 +4752,8 @@ class FormatterTest {
"""
|/** Here are some links [AnotherClass] [AnotherClass2] */
|class MyClass {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `don't add spaces after links in Kdoc`() =
@@ -4383,7 +4761,8 @@ class FormatterTest {
"""
|/** Here are some links [AnotherClass][AnotherClass2]hello */
|class MyClass {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `don't remove spaces after links in Kdoc`() =
@@ -4391,7 +4770,8 @@ class FormatterTest {
"""
|/** Please see [onNext] (which has more details) */
|class MyClass {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `link anchor in KDoc are preserved`() =
@@ -4399,7 +4779,8 @@ class FormatterTest {
"""
|/** [link anchor](the URL for the link anchor goes here) */
|class MyClass {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `don't add spaces between links in KDoc (because they're actually references)`() =
@@ -4410,7 +4791,8 @@ class FormatterTest {
|
|/** The final produced value may have [size][ByteString.size] < [bufferSize]. */
|class MyClass {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `collapse spaces after links in KDoc`() {
@@ -4418,12 +4800,14 @@ class FormatterTest {
"""
|/** Here are some links [Class1], [Class2] [Class3]. hello */
|class MyClass {}
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
|/** Here are some links [Class1], [Class2] [Class3]. hello */
|class MyClass {}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -4436,21 +4820,25 @@ class FormatterTest {
| * [Class2]
| */
|class MyClass {}
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
|/** Here are some links [Class1] [Class2] */
|class MyClass {}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@Test
fun `do not crash because of malformed KDocs and produce stable output`() {
- val code = """
+ val code =
+ """
|/** Surprise ``` */
|class MyClass {}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertFormatted(Formatter.format(code))
}
@@ -4463,7 +4851,8 @@ class FormatterTest {
|
|/** There are many [FooObject]s. */
|class MyClass {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle KDoc with multiple separated param tags, breaking and merging lines and missing asterisk`() {
@@ -4484,7 +4873,8 @@ class FormatterTest {
| * @see kotlin.text.isWhitespace
| */
|class ThisWasCopiedFromTheTrimMarginMethod {}
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
|/**
@@ -4497,14 +4887,14 @@ class FormatterTest {
| * Doesn't preserve the original line endings.
| *
| * @param marginPrefix non-blank string, which is used as a margin delimiter. Default is `|` (pipe
- | * character).
- | *
+ | * character).
| * @sample samples.text.Strings.trimMargin
| * @see trimIndent
| * @see kotlin.text.isWhitespace
| */
|class ThisWasCopiedFromTheTrimMarginMethod {}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -4514,7 +4904,8 @@ class FormatterTest {
"""
|/** Lorem ipsum dolor sit amet, consectetur */
|class MyClass {}
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
|/**
@@ -4522,7 +4913,8 @@ class FormatterTest {
| * consectetur
| */
|class MyClass {}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).withOptions(FormattingOptions(maxWidth = 33)).isEqualTo(expected)
}
@@ -4537,7 +4929,8 @@ class FormatterTest {
| println(child)
| }
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code)
.withOptions(FormattingOptions(maxWidth = 35, blockIndent = 4, continuationIndent = 4))
.isEqualTo(code)
@@ -4550,7 +4943,8 @@ class FormatterTest {
|fun doIt() {}
|
|/* this is the first comment */
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `preserve LF, CRLF and CR line endings`() {
@@ -4579,7 +4973,8 @@ class FormatterTest {
| a: Int,
| b: Int
|)
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -4603,7 +4998,8 @@ class FormatterTest {
| a: Int,
| b: Int
|)
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -4630,7 +5026,8 @@ class FormatterTest {
| b: Int
| )
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -4666,7 +5063,8 @@ class FormatterTest {
| b: Int,
| c: Int,
|) {}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -4706,7 +5104,8 @@ class FormatterTest {
| 3,
| )
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -4718,7 +5117,8 @@ class FormatterTest {
| set(
| value,
| ) {}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -4732,7 +5132,8 @@ class FormatterTest {
| Int,
| ) -> Unit
|) {}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -4741,9 +5142,12 @@ class FormatterTest {
"""
|--------------------------
|fun foo() {
- | foo({ it },)
+ | foo(
+ | { it },
+ | )
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -4807,7 +5211,8 @@ class FormatterTest {
| //
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -4847,7 +5252,8 @@ class FormatterTest {
| foo()
| //
| }
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -4884,7 +5290,8 @@ class FormatterTest {
|val g = 1
|
|data class Qux(val foo: String)
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
assertThatFormatting(
@@ -4893,7 +5300,8 @@ class FormatterTest {
|import com.example.bar
|const val SOME_CONST = foo.a
|val SOME_STR = bar.a
- |""".trimMargin())
+ |"""
+ .trimMargin())
.isEqualTo(
"""
|import com.example.bar
@@ -4901,18 +5309,23 @@ class FormatterTest {
|
|const val SOME_CONST = foo.a
|val SOME_STR = bar.a
- |""".trimMargin())
+ |"""
+ .trimMargin())
}
@Test
fun `first line is never empty`() =
- assertThatFormatting("""
+ assertThatFormatting(
+ """
|
|fun f() {}
- |""".trimMargin())
- .isEqualTo("""
+ |"""
+ .trimMargin())
+ .isEqualTo(
+ """
|fun f() {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `at most one newline between any adjacent top-level elements`() =
@@ -4940,7 +5353,8 @@ class FormatterTest {
|
|
|val x = Bar()
- |""".trimMargin())
+ |"""
+ .trimMargin())
.isEqualTo(
"""
|import com.Bar
@@ -4957,7 +5371,8 @@ class FormatterTest {
|val x = Foo()
|
|val x = Bar()
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `at least one newline between any adjacent top-level elements, unless it's a property`() =
@@ -4971,7 +5386,8 @@ class FormatterTest {
|class C {}
|val x = Foo()
|val x = Bar()
- |""".trimMargin())
+ |"""
+ .trimMargin())
.isEqualTo(
"""
|import com.Bar
@@ -4987,7 +5403,8 @@ class FormatterTest {
|
|val x = Foo()
|val x = Bar()
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle array of annotations with field prefix`() {
@@ -4998,7 +5415,8 @@ class FormatterTest {
| var myVar: String? = null
|}
|
- """.trimMargin()
+ """
+ .trimMargin()
assertThatFormatting(code).isEqualTo(code)
}
@@ -5011,7 +5429,8 @@ class FormatterTest {
| var myVar: String? = null
|}
|
- """.trimMargin()
+ """
+ .trimMargin()
assertThatFormatting(code).isEqualTo(code)
}
@@ -5031,7 +5450,8 @@ class FormatterTest {
| }
| }
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
// Don't throw.
Formatter.format(code)
@@ -5047,7 +5467,8 @@ class FormatterTest {
| val y = 0
| y
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `lambda with optional arrow`() =
@@ -5059,7 +5480,8 @@ class FormatterTest {
| val y = 0
| y
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `lambda missing optional arrow`() =
@@ -5071,7 +5493,8 @@ class FormatterTest {
| val y = 0
| y
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `chaining - many dereferences`() =
@@ -5086,7 +5509,8 @@ class FormatterTest {
| .cyan
| .magenta
| .key
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5095,7 +5519,8 @@ class FormatterTest {
"""
|---------------------------------------------------------------------------
|rainbow.red.orange.yellow.green.blue.indigo.violet.cyan.magenta.key
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5112,7 +5537,8 @@ class FormatterTest {
| .magenta
| .key
| .build()
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5121,7 +5547,8 @@ class FormatterTest {
"""
|---------------------------------------------------------------------------
|rainbow.red.orange.yellow.green.blue.indigo.violet.cyan.magenta.key.build()
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5139,7 +5566,8 @@ class FormatterTest {
| .key
| .build()
| .shine()
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5156,7 +5584,8 @@ class FormatterTest {
| .cyan
| .magenta
| .key
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5174,7 +5603,8 @@ class FormatterTest {
| .cyan
| .magenta
| .key
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5191,7 +5621,8 @@ class FormatterTest {
| .magenta
| .key
| .build { it.appear }
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5208,7 +5639,8 @@ class FormatterTest {
| .magenta
| .key
| .z { it }
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5228,7 +5660,8 @@ class FormatterTest {
| it
| it
| }
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5245,7 +5678,8 @@ class FormatterTest {
| .cyan
| .magenta
| .key
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5265,7 +5699,8 @@ class FormatterTest {
| .cyan
| .magenta
| .key
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5283,7 +5718,8 @@ class FormatterTest {
| .cyan
| .magenta
| .key
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5303,7 +5739,8 @@ class FormatterTest {
| .cyan
| .magenta
| .key
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5321,7 +5758,8 @@ class FormatterTest {
| .key
| .z { it }
| .shine()
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5342,7 +5780,8 @@ class FormatterTest {
| it
| }
| .shine()
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5360,7 +5799,8 @@ class FormatterTest {
| .key
| .shine()
| .z { it }
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5378,7 +5818,8 @@ class FormatterTest {
| .cyan
| .magenta
| .key
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5396,7 +5837,8 @@ class FormatterTest {
| .cyan
| .magenta
| .key
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5412,7 +5854,8 @@ class FormatterTest {
| .cyan
| .magenta
| .key
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5429,7 +5872,8 @@ class FormatterTest {
| .magenta
| .key
| .build()
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5445,7 +5889,8 @@ class FormatterTest {
| .cyan
| .magenta
| .key
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5462,7 +5907,8 @@ class FormatterTest {
| .magenta
| .key
| .build()
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5478,7 +5924,8 @@ class FormatterTest {
| .cyan
| .magenta
| .key
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5495,7 +5942,8 @@ class FormatterTest {
| .magenta
| .key
| .build()
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5515,7 +5963,8 @@ class FormatterTest {
| .magenta
| .key
| .shine()
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5535,7 +5984,8 @@ class FormatterTest {
| .magenta
| .key
| .shine()
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5544,7 +5994,8 @@ class FormatterTest {
"""
|-------------------------
|rainbow.a().b().c()
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5556,7 +6007,8 @@ class FormatterTest {
| it
| it
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5574,7 +6026,8 @@ class FormatterTest {
| .cyan
| .magenta
| .key
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5585,7 +6038,8 @@ class FormatterTest {
|z12.shine()
| .bright()
| .z { it }
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5596,7 +6050,8 @@ class FormatterTest {
|getRainbow(
| aa, bb, cc)
| .z { it }
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5607,7 +6062,8 @@ class FormatterTest {
|z { it }
| .shine()
| .bright()
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5618,7 +6074,8 @@ class FormatterTest {
|com.sky.Rainbow
| .colorFactory
| .build()
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5630,7 +6087,8 @@ class FormatterTest {
| it
| it
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5644,7 +6102,8 @@ class FormatterTest {
| it
| }
| .red
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5657,7 +6116,8 @@ class FormatterTest {
| it
| it
| }
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5671,7 +6131,8 @@ class FormatterTest {
| it
| }
| .red
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5683,7 +6144,8 @@ class FormatterTest {
| it
| it
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5696,7 +6158,8 @@ class FormatterTest {
| it
| it
| }
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5710,7 +6173,8 @@ class FormatterTest {
| it
| it
| }
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5722,7 +6186,8 @@ class FormatterTest {
| infrared,
| ultraviolet,
|)
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5736,7 +6201,8 @@ class FormatterTest {
| ultraviolet,
| )
| .red
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5749,7 +6215,8 @@ class FormatterTest {
| infrared,
| ultraviolet,
| )
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5763,7 +6230,8 @@ class FormatterTest {
| infrared,
| ultraviolet,
| )
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5778,7 +6246,8 @@ class FormatterTest {
| ultraviolet,
| )
| .red
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5792,7 +6261,8 @@ class FormatterTest {
| ultraviolet,
| )
| .bright()
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5806,7 +6276,8 @@ class FormatterTest {
| ultraviolet,
| )
| .z { it }
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5819,7 +6290,8 @@ class FormatterTest {
| ultraviolet,
| )
| .bright()
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5832,7 +6304,8 @@ class FormatterTest {
| ultraviolet,
| )
| .bright()
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5844,7 +6317,8 @@ class FormatterTest {
| infrared,
| ultraviolet,
|)
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5857,7 +6331,8 @@ class FormatterTest {
| ultraviolet,
| )
| .z { it }
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
deduceMaxWidth = true)
@Test
@@ -5880,7 +6355,8 @@ class FormatterTest {
|
| @Anno1 /* comment */ @Anno2 f(1) as Int
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `annotations for expressions 2`() {
@@ -5891,7 +6367,8 @@ class FormatterTest {
| @Suppress("UNCHECKED_CAST")
| f(1 + f(1) as Int)
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
@@ -5899,8 +6376,75 @@ class FormatterTest {
| @Suppress("UNCHECKED_CAST") f(1 + f(1) as Int)
| @Suppress("UNCHECKED_CAST") f(1 + f(1) as Int)
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
+
+ @Test
+ fun `function call following long multiline string`() =
+ assertFormatted(
+ """
+ |--------------------------------
+ |fun f() {
+ | val str1 =
+ | $TQ
+ | Some very long string that might mess things up
+ | $TQ
+ | .trimIndent()
+ |
+ | val str2 =
+ | $TQ
+ | Some very long string that might mess things up
+ | $TQ
+ | .trimIndent(someArg)
+ |}
+ |"""
+ .trimMargin(),
+ deduceMaxWidth = true)
+
+ @Test
+ fun `array-literal in annotation`() =
+ assertFormatted(
+ """
+ |--------------------------------
+ |@Anno(
+ | array =
+ | [
+ | someItem,
+ | andAnother,
+ | noTrailingComma])
+ |class Host
+ |
+ |@Anno(
+ | array =
+ | [
+ | someItem,
+ | andAnother,
+ | withTrailingComma,
+ | ])
+ |class Host
+ |
+ |@Anno(
+ | array =
+ | [
+ | // Comment
+ | someItem,
+ | // Comment
+ | andAnother,
+ | // Comment
+ | withTrailingComment
+ | // Comment
+ | // Comment
+ | ])
+ |class Host
+ |"""
+ .trimMargin(),
+ deduceMaxWidth = true)
+
+ companion object {
+ /** Triple quotes, useful to use within triple-quoted strings. */
+ private const val TQ = "\"\"\""
+ }
}
diff --git a/core/src/test/java/com/facebook/ktfmt/format/GoogleStyleFormatterKtTest.kt b/core/src/test/java/com/facebook/ktfmt/format/GoogleStyleFormatterKtTest.kt
index 61b6b47..b09fb48 100644
--- a/core/src/test/java/com/facebook/ktfmt/format/GoogleStyleFormatterKtTest.kt
+++ b/core/src/test/java/com/facebook/ktfmt/format/GoogleStyleFormatterKtTest.kt
@@ -54,7 +54,8 @@ class GoogleStyleFormatterKtTest {
|
| ImmutableList.newBuilder().add(1).add(1).add(1).add(1).add(1).add(1).add(1).add(1).add(1).add(1).build()
| }
- |""".trimMargin()
+ |"""
+ .trimMargin()
val expected =
"""
@@ -96,7 +97,8 @@ class GoogleStyleFormatterKtTest {
| .add(1)
| .build()
|}
- |""".trimMargin()
+ |"""
+ .trimMargin()
assertThatFormatting(code).withOptions(Formatter.GOOGLE_FORMAT).isEqualTo(expected)
// Don't add more tests here
@@ -140,7 +142,8 @@ class GoogleStyleFormatterKtTest {
|class C(a: Int, var b: Int, val c: Int) {
| //
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -188,7 +191,8 @@ class GoogleStyleFormatterKtTest {
|fun c12(a: Int, var b: Int, val c: Int) {
| //
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -212,7 +216,8 @@ class GoogleStyleFormatterKtTest {
| //
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -233,28 +238,159 @@ class GoogleStyleFormatterKtTest {
| }
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@Test
- fun `don't one-line lambdas following parameter breaks`() =
+ fun `no forward propagation of breaks in call expressions (at trailing lambda)`() =
+ assertFormatted(
+ """
+ |--------------------------
+ |fun test() {
+ | foo_bar_baz__zip(b) {
+ | c
+ | }
+ | foo.bar(baz).zip(b) {
+ | c
+ | }
+ |}
+ |"""
+ .trimMargin(),
+ formattingOptions = Formatter.GOOGLE_FORMAT,
+ deduceMaxWidth = true)
+
+ @Test
+ fun `forward propagation of breaks in call expressions (at value args)`() =
+ assertFormatted(
+ """
+ |----------------------
+ |fun test() {
+ | foo_bar_baz__zip(
+ | b
+ | ) {
+ | c
+ | }
+ |}
+ |
+ |fun test() {
+ | foo.bar(baz).zip(
+ | b
+ | ) {
+ | c
+ | }
+ |}
+ |"""
+ .trimMargin(),
+ formattingOptions = Formatter.GOOGLE_FORMAT,
+ deduceMaxWidth = true)
+
+ @Test
+ fun `forward propagation of breaks in call expressions (at type args)`() =
+ assertFormatted(
+ """
+ |-------------------
+ |fun test() {
+ | foo_bar_baz__zip<
+ | A
+ | >(
+ | b
+ | ) {
+ | c
+ | }
+ | foo.bar(baz).zip<
+ | A
+ | >(
+ | b
+ | ) {
+ | c
+ | }
+ |}
+ |"""
+ .trimMargin(),
+ formattingOptions = Formatter.GOOGLE_FORMAT,
+ deduceMaxWidth = true)
+
+ @Test
+ fun `expected indent in methods following single-line strings`() =
+ assertFormatted(
+ """
+ |-------------------------
+ |"Hello %s".format(
+ | someLongExpression
+ |)
+ |"""
+ .trimMargin(),
+ formattingOptions = Formatter.GOOGLE_FORMAT,
+ deduceMaxWidth = true)
+
+ @Test
+ fun `forced break between multi-line strings and their selectors`() =
+ assertFormatted(
+ """
+ |-------------------------
+ |val STRING =
+ | $TQ
+ | |foo
+ | |$TQ
+ | .wouldFit()
+ |
+ |val STRING =
+ | $TQ
+ | |foo
+ | |----------------------------------$TQ
+ | .wouldntFit()
+ |
+ |val STRING =
+ | $TQ
+ | |foo
+ | |$TQ
+ | .firstLink()
+ | .secondLink()
+ |"""
+ .trimMargin(),
+ formattingOptions = Formatter.GOOGLE_FORMAT,
+ deduceMaxWidth = true)
+
+ @Test
+ fun `properly break fully qualified nested user types`() =
+ assertFormatted(
+ """
+ |-------------------------------------------------------
+ |val complicated:
+ | com.example.interesting.SomeType<
+ | com.example.interesting.SomeType,
+ | com.example.interesting.SomeType<
+ | com.example.interesting.SomeType,
+ | Nothing
+ | >
+ | > =
+ | DUMMY
+ |"""
+ .trimMargin(),
+ formattingOptions = Formatter.GOOGLE_FORMAT,
+ deduceMaxWidth = true)
+
+ @Test
+ fun `don't one-line lambdas following argument breaks`() =
assertFormatted(
"""
|------------------------------------------------------------------------
|class Foo : Bar() {
| fun doIt() {
- | // don't break in lambda, no parameter breaks found
+ | // don't break in lambda, no argument breaks found
| fruit.forEach { eat(it) }
|
- | // don't break in lambda, because we only detect parameter breaks
- | // with trailing commas
+ | // break in lambda, without comma
| fruit.forEach(
| someVeryLongParameterNameThatWillCauseABreak,
| evenWithoutATrailingCommaOnTheParameterListSoLetsSeeIt
- | ) { eat(it) }
+ | ) {
+ | eat(it)
+ | }
|
- | // break in the lambda
+ | // break in the lambda, with comma
| fruit.forEach(
| fromTheVine = true,
| ) {
@@ -291,7 +427,8 @@ class GoogleStyleFormatterKtTest {
| }
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -308,7 +445,8 @@ class GoogleStyleFormatterKtTest {
| 123456789012345678901234567890
| )
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT)
@Test
@@ -338,7 +476,8 @@ class GoogleStyleFormatterKtTest {
| }
| .build()
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
)
@@ -353,7 +492,8 @@ class GoogleStyleFormatterKtTest {
| }
| )
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
)
@@ -368,9 +508,12 @@ class GoogleStyleFormatterKtTest {
| step1()
| step2()
| }
- | ) { it.doIt() }
+ | ) {
+ | it.doIt()
+ | }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
)
@@ -385,7 +528,8 @@ class GoogleStyleFormatterKtTest {
| }
| )
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
)
@@ -402,7 +546,8 @@ class GoogleStyleFormatterKtTest {
| c = 3456789012345678901234567890
| )
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT)
@Test
@@ -430,7 +575,8 @@ class GoogleStyleFormatterKtTest {
| )
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -450,7 +596,8 @@ class GoogleStyleFormatterKtTest {
| .doThat()
| )
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -468,7 +615,8 @@ class GoogleStyleFormatterKtTest {
| )
| return if (b) 1 else 2
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT)
@Test
@@ -484,7 +632,8 @@ class GoogleStyleFormatterKtTest {
| },
| duration = duration
| )
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT)
@Test
@@ -507,7 +656,8 @@ class GoogleStyleFormatterKtTest {
| ) +
| value9
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -556,7 +706,8 @@ class GoogleStyleFormatterKtTest {
| b is String
| )
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -587,7 +738,8 @@ class GoogleStyleFormatterKtTest {
| State(0)
| )
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -616,7 +768,8 @@ class GoogleStyleFormatterKtTest {
| )
| .doThat()
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -641,7 +794,8 @@ class GoogleStyleFormatterKtTest {
| offspring
| )
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -657,7 +811,8 @@ class GoogleStyleFormatterKtTest {
| Foo.createSpeciallyDesignedParameter(),
| )
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -700,7 +855,8 @@ class GoogleStyleFormatterKtTest {
| 3,
| )
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -755,7 +911,8 @@ class GoogleStyleFormatterKtTest {
| .methodName4()
| .abcdefghijkl()
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -778,7 +935,8 @@ class GoogleStyleFormatterKtTest {
| }
| )
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT)
@Test
@@ -796,7 +954,8 @@ class GoogleStyleFormatterKtTest {
| println("b")
| )
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -815,7 +974,59 @@ class GoogleStyleFormatterKtTest {
| doItOnce()
| doItTwice()
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
+
+ @Test
+ fun `comma separated lists, no automatic trailing break after lambda params`() =
+ assertFormatted(
+ """
+ |----------------------------
+ |fun foo() {
+ | someExpr.let { x -> x }
+ | someExpr.let { x, y -> x }
+ |
+ | someExpr.let { paramFits
+ | ->
+ | butNotArrow
+ | }
+ | someExpr.let { params, fit
+ | ->
+ | butNotArrow
+ | }
+ |
+ | someExpr.let {
+ | parameterToLong ->
+ | fits
+ | }
+ | someExpr.let {
+ | tooLong,
+ | together ->
+ | fits
+ | }
+ |}
+ |"""
+ .trimMargin(),
+ formattingOptions = Formatter.GOOGLE_FORMAT,
+ deduceMaxWidth = true)
+
+ @Test
+ fun `comma separated lists, no automatic trailing break after supertype list`() =
+ assertFormatted(
+ """
+ |----------------------------
+ |class Foo() :
+ | ThisList,
+ | WillBe,
+ | TooLong(thats = ok) {
+ | fun someMethod() {
+ | val forceBodyBreak = 0
+ | }
+ |}
+ |"""
+ .trimMargin(),
+ formattingOptions = Formatter.GOOGLE_FORMAT,
+ deduceMaxWidth = true)
@Test
fun `if expression with multiline condition`() =
@@ -841,7 +1052,8 @@ class GoogleStyleFormatterKtTest {
| bar()
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -857,7 +1069,8 @@ class GoogleStyleFormatterKtTest {
| bar()
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -887,7 +1100,8 @@ class GoogleStyleFormatterKtTest {
| 2 -> print(2)
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -904,7 +1118,8 @@ class GoogleStyleFormatterKtTest {
| 2 -> print(2)
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -932,7 +1147,8 @@ class GoogleStyleFormatterKtTest {
| bar()
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -948,7 +1164,8 @@ class GoogleStyleFormatterKtTest {
| bar()
| }
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -970,7 +1187,8 @@ class GoogleStyleFormatterKtTest {
| boo
| )
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -988,7 +1206,8 @@ class GoogleStyleFormatterKtTest {
| param2
| )
|}
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -1002,7 +1221,8 @@ class GoogleStyleFormatterKtTest {
| .doOp(1)
| .doOp(2)
|)
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
@@ -1018,7 +1238,80 @@ class GoogleStyleFormatterKtTest {
| c: String,
| d: String
| ) -> Unit
- |""".trimMargin(),
+ |"""
+ .trimMargin(),
+ formattingOptions = Formatter.GOOGLE_FORMAT,
+ deduceMaxWidth = true)
+
+ @Test
+ fun `function call following long multiline string`() =
+ assertFormatted(
+ """
+ |--------------------------------
+ |fun f() {
+ | val str1 =
+ | $TQ
+ | Some very long string that might mess things up
+ | $TQ
+ | .trimIndent()
+ |
+ | val str2 =
+ | $TQ
+ | Some very long string that might mess things up
+ | $TQ
+ | .trimIndent(someArg)
+ |}
+ |"""
+ .trimMargin(),
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
+
+ @Test
+ fun `array-literal in annotation`() =
+ assertFormatted(
+ """
+ |--------------------------------
+ |@Anno(
+ | array =
+ | [
+ | someItem,
+ | andAnother,
+ | noTrailingComma
+ | ]
+ |)
+ |class Host
+ |
+ |@Anno(
+ | array =
+ | [
+ | someItem,
+ | andAnother,
+ | withTrailingComma,
+ | ]
+ |)
+ |class Host
+ |
+ |@Anno(
+ | array =
+ | [
+ | // Comment
+ | someItem,
+ | // Comment
+ | andAnother,
+ | // Comment
+ | withTrailingComment
+ | // Comment
+ | // Comment
+ | ]
+ |)
+ |class Host
+ |"""
+ .trimMargin(),
+ formattingOptions = Formatter.GOOGLE_FORMAT,
+ deduceMaxWidth = true)
+
+ companion object {
+ /** Triple quotes, useful to use within triple-quoted strings. */
+ private const val TQ = "\"\"\""
+ }
}
diff --git a/core/src/test/java/com/facebook/ktfmt/kdoc/DokkaVerifier.kt b/core/src/test/java/com/facebook/ktfmt/kdoc/DokkaVerifier.kt
new file mode 100644
index 0000000..76fe54d
--- /dev/null
+++ b/core/src/test/java/com/facebook/ktfmt/kdoc/DokkaVerifier.kt
@@ -0,0 +1,197 @@
+/*
+ * Copyright (c) Tor Norbye.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("PropertyName", "PrivatePropertyName")
+
+package com.facebook.ktfmt.kdoc
+
+import com.google.common.truth.Truth.assertThat
+import java.io.BufferedReader
+import java.io.File
+
+/**
+ * Verifies that two KDoc comment strings render to the same HTML documentation using Dokka. This is
+ * used by the test infrastructure to make sure that the transformations we're allowing are not
+ * changing the appearance of the documentation.
+ *
+ * Unfortunately, just diffing HTML strings isn't always enough, because dokka will preserve some
+ * text formatting which is immaterial to the HTML appearance. Therefore, if you've also installed
+ * Pandoc, it will use that to generate a text rendering of the HTML which is then used for diffing
+ * instead. (Even this isn't fullproof because pandoc also preserves some details that should not
+ * matter). Text rendering does drop a lot of markup (such as bold and italics) so it would be
+ * better to compare in some other format, such as PDF, but unfortunately, the PDF rendering doesn't
+ * appear to be stable; rendering the same document twice yields a binary diff.
+ *
+ * Dokka no longer provides a fat/shadow jar; instead you have to download a bunch of different
+ * dependencies. Therefore, for convenience this is set up to point to an AndroidX checkout, which
+ * has all the prebuilts. Point the below to AndroidX and the rest should work.
+ */
+class DokkaVerifier(private val tempFolder: File) {
+ // Configuration parameters
+ // Checkout of https://github.com/androidx/androidx
+ private val ANDROIDX_HOME: String? = null
+
+ // Optional install of pandoc, e.g. "/opt/homebrew/bin/pandoc"
+ private val PANDOC: String? = null
+
+ // JDK install
+ private val JAVA_HOME: String? = System.getenv("JAVA_HOME") ?: System.getProperty("java.home")
+
+ fun verify(before: String, after: String) {
+ JAVA_HOME ?: return
+ ANDROIDX_HOME ?: return
+
+ val androidx = File(ANDROIDX_HOME)
+ if (!androidx.isDirectory) {
+ return
+ }
+
+ val prebuilts = File(androidx, "prebuilts")
+ if (!prebuilts.isDirectory) {
+ println("AndroidX prebuilts not found; not verifying with Dokka")
+ }
+ val cli = find(prebuilts, "org.jetbrains.dokka", "dokka-cli")
+ val analysis = find(prebuilts, "org.jetbrains.dokka", "dokka-analysis")
+ val base = find(prebuilts, "org.jetbrains.dokka", "dokka-base")
+ val compiler = find(prebuilts, "org.jetbrains.dokka", "kotlin-analysis-compiler")
+ val intellij = find(prebuilts, "org.jetbrains.dokka", "kotlin-analysis-intellij")
+ val coroutines = find(prebuilts, "org.jetbrains.kotlinx", "kotlinx-coroutines-core")
+ val html = find(prebuilts, "org.jetbrains.kotlinx", "kotlinx-html-jvm")
+ val freemarker = find(prebuilts, "org.freemarker", "freemarker")
+
+ val src = File(tempFolder, "src")
+ val out = File(tempFolder, "dokka")
+ src.mkdirs()
+ out.mkdirs()
+
+ val beforeFile = File(src, "before.kt")
+ beforeFile.writeText("${before.split("\n").joinToString("\n") { it.trim() }}\nclass Before\n")
+
+ val afterFile = File(src, "after.kt")
+ afterFile.writeText("${after.split("\n").joinToString("\n") { it.trim() }}\nclass After\n")
+
+ val args = mutableListOf()
+ args.add(File(JAVA_HOME, "bin/java").path)
+ args.add("-jar")
+ args.add(cli.path)
+ args.add("-pluginsClasspath")
+ val pathSeparator =
+ ";" // instead of File.pathSeparator as would have been reasonable (e.g. : on Unix)
+ val path =
+ listOf(analysis, base, compiler, intellij, coroutines, html, freemarker).joinToString(
+ pathSeparator) {
+ it.path
+ }
+ args.add(path)
+ args.add("-sourceSet")
+ args.add("-src $src") // (nested parameter within -sourceSet)
+ args.add("-outputDir")
+ args.add(out.path)
+ executeProcess(args)
+
+ fun getHtml(file: File): String {
+ val rendered = file.readText()
+ val begin = rendered.indexOf("