aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJordan Demeulenaere <jdemeulenaere@google.com>2023-02-23 16:44:33 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2023-02-23 16:44:33 +0000
commitf2ef27b6a0dd6c027e814ab4af4ee21f88746fed (patch)
tree7cdf574cfeb4a3392972eb66ac363ecb10ec9251
parentad7515c1cc7434597eed9b3f09a0f279afc72b03 (diff)
parent8c6e055aaf424db0e4f9ab5321a3937005a67ed0 (diff)
downloadktfmt-f2ef27b6a0dd6c027e814ab4af4ee21f88746fed.tar.gz
Revert "Revert "Merge tag 'v0.42' into update"" am: 582c58a7a2 am: 8c6e055aaf
Original change: https://android-review.googlesource.com/c/platform/external/ktfmt/+/2453025 Change-Id: Ie659886795e7779f06b69b44b11d38bf495bb782 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r--core/pom.xml2
-rw-r--r--core/src/main/java/com/facebook/ktfmt/cli/Main.kt9
-rw-r--r--core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt16
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/Formatter.kt8
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/KotlinInput.kt32
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt426
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/KotlinTok.kt6
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/RedundantElementRemover.kt10
-rw-r--r--core/src/main/java/com/facebook/ktfmt/kdoc/CommentType.kt64
-rw-r--r--core/src/main/java/com/facebook/ktfmt/kdoc/FormattingTask.kt58
-rw-r--r--core/src/main/java/com/facebook/ktfmt/kdoc/KDocCommentsHelper.kt14
-rw-r--r--core/src/main/java/com/facebook/ktfmt/kdoc/KDocFormatter.kt305
-rw-r--r--core/src/main/java/com/facebook/ktfmt/kdoc/KDocFormattingOptions.kt134
-rw-r--r--core/src/main/java/com/facebook/ktfmt/kdoc/Paragraph.kt617
-rw-r--r--core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphList.kt28
-rw-r--r--core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphListBuilder.kt830
-rw-r--r--core/src/main/java/com/facebook/ktfmt/kdoc/Table.kt270
-rw-r--r--core/src/main/java/com/facebook/ktfmt/kdoc/Utilities.kt329
-rw-r--r--core/src/test/java/com/facebook/ktfmt/cli/MainTest.kt41
-rw-r--r--core/src/test/java/com/facebook/ktfmt/cli/ParsedArgsTest.kt81
-rw-r--r--core/src/test/java/com/facebook/ktfmt/format/FormatterTest.kt1494
-rw-r--r--core/src/test/java/com/facebook/ktfmt/format/GoogleStyleFormatterKtTest.kt383
-rw-r--r--core/src/test/java/com/facebook/ktfmt/kdoc/DokkaVerifier.kt197
-rw-r--r--core/src/test/java/com/facebook/ktfmt/kdoc/KDocFormatterTest.kt4706
-rw-r--r--core/src/test/java/com/facebook/ktfmt/kdoc/UtilitiesTest.kt105
-rw-r--r--pom.xml2
-rw-r--r--version.txt2
-rw-r--r--website/package-lock.json66
28 files changed, 9244 insertions, 991 deletions
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 @@
<parent>
<groupId>com.facebook</groupId>
<artifactId>ktfmt-parent</artifactId>
- <version>0.39</version>
+ <version>0.42</version>
</parent>
<properties>
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=<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<File>
@@ -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() ?: "<stdin>"
+ val fileName = file?.toString() ?: parsedArgs.stdinName ?: "<stdin>"
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("<stdin>")
+ 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}=<value>'")
+ 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<Int, Token>()
- 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<KotlinTok>): ImmutableMap<Int, Int> {
- val builder = LinkedHashMap<Int, Int>()
- for (tok in toks) {
- builder.put(tok.position, tok.column)
- }
- return ImmutableMap.copyOf(builder)
- }
+ private fun makePositionToColumnMap(toks: List<KotlinTok>) =
+ ImmutableMap.copyOf(toks.map { it.position to it.column }.toMap())
private fun buildToks(file: KtFile, fileText: String): ImmutableList<KotlinTok> {
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<Token>): ImmutableRangeMap<Int, Token> {
+ val tokenLocations = ImmutableRangeMap.builder<Int, Token>()
+ 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 `<Int, String>` in `List<Int, String>` */
override fun visitTypeArgumentList(typeArgumentList: KtTypeArgumentList) {
builder.sync(typeArgumentList)
- builder.block(ZERO) {
- builder.token("<")
- builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO)
- builder.block(ZERO) {
- emitParameterLikeList(
- typeArgumentList.arguments, typeArgumentList.trailingComma != null, wrapInBlock = true)
- }
- }
- builder.token(">")
+ visitEachCommaSeparated(
+ typeArgumentList.arguments,
+ typeArgumentList.trailingComma != null,
+ prefix = "<",
+ postfix = ">",
+ )
}
override fun visitTypeProjection(typeProjection: KtTypeProjection) {
@@ -456,7 +466,18 @@ class KotlinInputAstVisitor(
visit(selectorExpression)
}
}
- receiver is KtWhenExpression || receiver is KtStringTemplateExpression -> {
+ receiver is KtStringTemplateExpression -> {
+ val isMultiline = receiver.text.contains('\n')
+ builder.block(if (isMultiline) expressionBreakIndent else ZERO) {
+ visit(receiver)
+ if (isMultiline) {
+ builder.forcedBreak()
+ }
+ builder.token(expression.operationSign.value)
+ visit(expression.selectorExpression)
+ }
+ }
+ receiver is KtWhenExpression -> {
builder.block(ZERO) {
visit(receiver)
builder.token(expression.operationSign.value)
@@ -522,6 +543,7 @@ class KotlinInputAstVisitor(
}
val argsIndentElse = if (index == parts.size - 1) ZERO else expressionBreakIndent
val lambdaIndentElse = if (isTrailingLambda) expressionBreakNegativeIndent else ZERO
+ val negativeLambdaIndentElse = if (isTrailingLambda) expressionBreakIndent else ZERO
// emit `(1, 2) { it }` from `doIt(1, 2) { it }`
visitCallElement(
@@ -531,6 +553,7 @@ class KotlinInputAstVisitor(
selectorExpression.lambdaArguments,
argumentsIndent = Indent.If.make(nameTag, expressionBreakIndent, argsIndentElse),
lambdaIndent = Indent.If.make(nameTag, ZERO, lambdaIndentElse),
+ negativeLambdaIndent = Indent.If.make(nameTag, ZERO, negativeLambdaIndentElse),
)
}
}
@@ -714,82 +737,107 @@ class KotlinInputAstVisitor(
typeArgumentList,
valueArgumentList,
lambdaArguments,
- lambdaIndent = ZERO)
+ )
}
}
- /** Examples `foo<T>(a, b)`, `foo(a)`, `boo()`, `super(a)` */
+ /**
+ * Examples `foo<T>(a, b)`, `foo(a)`, `boo()`, `super(a)`
+ *
+ * @param lambdaIndent how to indent [lambdaArguments], if present
+ * @param negativeLambdaIndent the negative indentation of [lambdaIndent]
+ */
private fun visitCallElement(
callee: KtExpression?,
typeArgumentList: KtTypeArgumentList?,
argumentList: KtValueArgumentList?,
lambdaArguments: List<KtLambdaArgument>,
argumentsIndent: Indent = expressionBreakIndent,
- lambdaIndent: Indent = ZERO
+ lambdaIndent: Indent = ZERO,
+ negativeLambdaIndent: Indent = ZERO,
) {
- builder.block(ZERO) {
- visit(callee)
- val arguments = argumentList?.arguments.orEmpty()
- builder.block(argumentsIndent) { visit(typeArgumentList) }
- builder.block(argumentsIndent) {
- if (argumentList != null) {
- builder.token("(")
- }
- if (arguments.isNotEmpty()) {
- if (isGoogleStyle) {
- visit(argumentList)
- val first = arguments.first()
- if (arguments.size != 1 ||
- first?.isNamed() != false ||
- first.getArgumentExpression() !is KtLambdaExpression) {
- builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakNegativeIndent)
- }
- } else {
- builder.block(ZERO) { visit(argumentList) }
+ // Apply the lambda indent to the callee, type args, value args, and the lambda.
+ // This is undone for the first three by the negative lambda indent.
+ // This way they're in one block, and breaks in the argument list cause a break in the lambda.
+ builder.block(lambdaIndent) {
+
+ // Used to keep track of whether or not we need to indent the lambda
+ // This is based on if there is a break in the argument list
+ var brokeBeforeBrace: BreakTag? = null
+
+ builder.block(negativeLambdaIndent) {
+ visit(callee)
+ builder.block(argumentsIndent) {
+ builder.block(ZERO) { visit(typeArgumentList) }
+ if (argumentList != null) {
+ brokeBeforeBrace = visitValueArgumentListInternal(argumentList)
}
}
- if (argumentList != null) {
- builder.token(")")
- }
}
- val hasTrailingComma = argumentList?.trailingComma != null
if (lambdaArguments.isNotEmpty()) {
builder.space()
- builder.block(lambdaIndent) {
- lambdaArguments.forEach {
- visitArgumentInternal(it, forceBreakLambdaBody = hasTrailingComma)
- }
- }
+ visitArgumentInternal(
+ lambdaArguments.single(),
+ wrapInBlock = false,
+ brokeBeforeBrace = brokeBeforeBrace,
+ )
}
}
}
/** Example (`1, "hi"`) in a function call */
override fun visitValueArgumentList(list: KtValueArgumentList) {
+ visitValueArgumentListInternal(list)
+ }
+
+ /**
+ * Example (`1, "hi"`) in a function call
+ *
+ * @return a [BreakTag] which can tell you if a break was taken, but only when the list doesn't
+ * terminate in a negative closing indent. See [visitEachCommaSeparated] for examples.
+ */
+ private fun visitValueArgumentListInternal(list: KtValueArgumentList): BreakTag? {
builder.sync(list)
+
val arguments = list.arguments
val isSingleUnnamedLambda =
arguments.size == 1 &&
arguments.first().getArgumentExpression() is KtLambdaExpression &&
arguments.first().getArgumentName() == null
+ val hasTrailingComma = list.trailingComma != null
+
+ val wrapInBlock: Boolean
+ val breakBeforePostfix: Boolean
+ val leadingBreak: Boolean
+ val breakAfterPrefix: Boolean
+
if (isSingleUnnamedLambda) {
- builder.block(expressionBreakNegativeIndent) {
- visit(arguments.first())
- if (list.trailingComma != null) {
- builder.token(",")
- }
- }
+ wrapInBlock = true
+ breakBeforePostfix = false
+ leadingBreak = arguments.isNotEmpty() && hasTrailingComma
+ breakAfterPrefix = false
} else {
- // Break before args.
- builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO)
- emitParameterLikeList(
- list.arguments, list.trailingComma != null, wrapInBlock = !isGoogleStyle)
- }
+ wrapInBlock = !isGoogleStyle
+ breakBeforePostfix = isGoogleStyle && arguments.isNotEmpty()
+ leadingBreak = arguments.isNotEmpty()
+ breakAfterPrefix = arguments.isNotEmpty()
+ }
+
+ return visitEachCommaSeparated(
+ list.arguments,
+ hasTrailingComma,
+ wrapInBlock = wrapInBlock,
+ breakBeforePostfix = breakBeforePostfix,
+ leadingBreak = leadingBreak,
+ prefix = "(",
+ postfix = ")",
+ breakAfterPrefix = breakAfterPrefix,
+ )
}
/** Example `{ 1 + 1 }` (as lambda) or `{ (x, y) -> x + y }` */
override fun visitLambdaExpression(lambdaExpression: KtLambdaExpression) {
- visitLambdaExpressionInternal(lambdaExpression, brokeBeforeBrace = null, forceBreakBody = false)
+ visitLambdaExpressionInternal(lambdaExpression, brokeBeforeBrace = null)
}
/**
@@ -806,21 +854,10 @@ class KotlinInputAstVisitor(
* car()
* }
* ```
- * @param forceBreakBody if true, forces the lambda to be multi-line. Useful for call expressions
- * where it would look weird for the lambda to be on one-line. For example, here we avoid
- * one-lining `{ x = 0 }` since the parameters have a trailing comma:
- * ```
- * foo.bar(
- * trailingComma,
- * ) {
- * x = 0
- * }
- * ```
*/
private fun visitLambdaExpressionInternal(
lambdaExpression: KtLambdaExpression,
brokeBeforeBrace: BreakTag?,
- forceBreakBody: Boolean,
) {
builder.sync(lambdaExpression)
@@ -855,9 +892,7 @@ class KotlinInputAstVisitor(
if (hasParams || hasArrow) {
builder.space()
- builder.block(bracePlusExpressionIndent) {
- forEachCommaSeparated(valueParams) { it.accept(this) }
- }
+ builder.block(bracePlusExpressionIndent) { visitEachCommaSeparated(valueParams) }
builder.block(bracePlusBlockIndent) {
if (lambdaExpression.functionLiteral.valueParameterList?.trailingComma != null) {
builder.token(",")
@@ -870,10 +905,6 @@ class KotlinInputAstVisitor(
builder.breakOp(Doc.FillMode.UNIFIED, "", bracePlusZeroIndent)
}
- if (forceBreakBody) {
- builder.forcedBreak()
- }
-
if (hasStatements) {
builder.breakOp(Doc.FillMode.UNIFIED, " ", bracePlusBlockIndent)
builder.block(bracePlusBlockIndent) {
@@ -931,32 +962,11 @@ class KotlinInputAstVisitor(
/** e.g., `a: Int, b: Int, c: Int` in `fun foo(a: Int, b: Int, c: Int) { ... }`. */
override fun visitParameterList(list: KtParameterList) {
- emitParameterLikeList(list.parameters, list.trailingComma != null, wrapInBlock = false)
- }
-
- /**
- * Emit a list of elements that look like function parameters or arguments, e.g., `a, b, c` in
- * `foo(a, b, c)`
- */
- private fun <T : PsiElement> emitParameterLikeList(
- list: List<T>?,
- hasTrailingComma: Boolean,
- wrapInBlock: Boolean
- ) {
- if (list.isNullOrEmpty()) {
- return
- }
-
- forEachCommaSeparated(list, hasTrailingComma, wrapInBlock, trailingBreak = isGoogleStyle) {
- visit(it)
- }
- if (hasTrailingComma) {
- builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakNegativeIndent)
- }
+ visitEachCommaSeparated(list.parameters, list.trailingComma != null, wrapInBlock = false)
}
/**
- * Call `function` for each element in `list`, with comma (,) tokens inbetween.
+ * Visit each element in [list], with comma (,) tokens in-between.
*
* Example:
* ```
@@ -973,6 +983,15 @@ class KotlinInputAstVisitor(
* 5
* ```
*
+ * Optionally include a prefix and postfix:
+ * ```
+ * (
+ * a,
+ * b,
+ * c,
+ * )
+ * ```
+ *
* @param hasTrailingComma if true, each element is placed on its own line (even if they could've
* fit in a single line), and a trailing comma is emitted.
*
@@ -981,88 +1000,160 @@ class KotlinInputAstVisitor(
* a,
* b,
* ```
+ *
+ * @param wrapInBlock if true, place all the elements in a block. When there's no [leadingBreak],
+ * this will be negatively indented. Note that the [prefix] and [postfix] aren't included in the
+ * block.
+ * @param leadingBreak if true, break before the first element.
+ * @param prefix if provided, emit this before the first element.
+ * @param postfix if provided, emit this after the last element (or trailing comma).
+ * @param breakAfterPrefix if true, emit a break after [prefix], but before the start of the
+ * block.
+ * @param breakBeforePostfix if true, place a break after the last element. Redundant when
+ * [hasTrailingComma] is true.
+ * @return a [BreakTag] which can tell you if a break was taken, but only when the list doesn't
+ * terminate in a negative closing indent.
+ *
+ * Example 1, this returns a BreakTag which tells you a break wasn't taken:
+ * ```
+ * (arg1, arg2)
+ * ```
+ *
+ * Example 2, this returns a BreakTag which tells you a break WAS taken:
+ * ```
+ * (
+ * arg1,
+ * arg2)
+ * ```
+ *
+ * Example 3, this returns null:
+ * ```
+ * (
+ * arg1,
+ * arg2,
+ * )
+ * ```
+ *
+ * Example 4, this also returns null (similar to example 2, but Google style):
+ * ```
+ * (
+ * arg1,
+ * arg2
+ * )
+ * ```
*/
- private fun <T> forEachCommaSeparated(
- list: Iterable<T>,
+ private fun visitEachCommaSeparated(
+ list: Iterable<PsiElement>,
hasTrailingComma: Boolean = false,
wrapInBlock: Boolean = true,
- trailingBreak: Boolean = false,
- function: (T) -> Unit
- ) {
- if (hasTrailingComma) {
- builder.block(ZERO) {
- builder.forcedBreak()
- for (value in list) {
- function(value)
- builder.token(",")
- builder.forcedBreak()
- }
+ leadingBreak: Boolean = true,
+ prefix: String? = null,
+ postfix: String? = null,
+ breakAfterPrefix: Boolean = true,
+ breakBeforePostfix: Boolean = isGoogleStyle,
+ ): BreakTag? {
+ val breakAfterLastElement = hasTrailingComma || (postfix != null && breakBeforePostfix)
+ val nameTag = if (breakAfterLastElement) null else genSym()
+
+ if (prefix != null) {
+ builder.token(prefix)
+ if (breakAfterPrefix) {
+ builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO, Optional.ofNullable(nameTag))
}
- return
}
- builder.block(ZERO, isEnabled = wrapInBlock) {
+ val breakType = if (hasTrailingComma) Doc.FillMode.FORCED else Doc.FillMode.UNIFIED
+ fun emitComma() {
+ builder.token(",")
+ builder.breakOp(breakType, " ", ZERO)
+ }
+
+ val indent = if (leadingBreak) ZERO else expressionBreakNegativeIndent
+ builder.block(indent, isEnabled = wrapInBlock) {
+ if (leadingBreak) {
+ builder.breakOp(breakType, "", ZERO)
+ }
+
var first = true
- builder.breakOp(Doc.FillMode.UNIFIED, "", ZERO)
for (value in list) {
- if (!first) {
- builder.token(",")
- builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO)
- }
+ if (!first) emitComma()
first = false
+ visit(value)
+ }
- function(value)
+ if (hasTrailingComma) {
+ emitComma()
}
}
- if (trailingBreak) {
- builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakNegativeIndent)
+
+ if (breakAfterLastElement) {
+ // a negative closing indent places the postfix to the left of the elements
+ // see examples 2 and 4 in the docstring
+ builder.breakOp(breakType, "", expressionBreakNegativeIndent)
}
+
+ if (postfix != null) {
+ if (breakAfterLastElement) {
+ // Indent trailing comments to the same depth as list items. We really have to fight
+ // googlejavaformat here for some reason.
+ builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO)
+ builder.block(expressionBreakNegativeIndent) {
+ builder.breakOp(breakType, "", ZERO)
+ builder.token(postfix, expressionBreakIndent)
+ }
+ } else {
+ builder.token(postfix)
+ }
+ }
+
+ return nameTag
}
/** Example `a` in `foo(a)`, or `*a`, or `limit = 50` */
override fun visitArgument(argument: KtValueArgument) {
- visitArgumentInternal(argument, forceBreakLambdaBody = false)
+ visitArgumentInternal(
+ argument,
+ wrapInBlock = true,
+ brokeBeforeBrace = null,
+ )
}
/**
* The internal version of [visitArgument].
*
- * @param forceBreakLambdaBody if true (and [argument] is of type [KtLambdaExpression]), forces
- * the lambda to be multi-line. See documentation of [visitLambdaExpressionInternal] for an
- * example.
+ * @param wrapInBlock if true places the argument expression in a block.
*/
private fun visitArgumentInternal(
argument: KtValueArgument,
- forceBreakLambdaBody: Boolean,
+ wrapInBlock: Boolean,
+ brokeBeforeBrace: BreakTag?,
) {
builder.sync(argument)
val hasArgName = argument.getArgumentName() != null
val isLambda = argument.getArgumentExpression() is KtLambdaExpression
- builder.block(ZERO) {
- if (hasArgName) {
- visit(argument.getArgumentName())
+ if (hasArgName) {
+ visit(argument.getArgumentName())
+ builder.space()
+ builder.token("=")
+ if (isLambda) {
builder.space()
- builder.token("=")
- if (isLambda) {
- builder.space()
- }
}
- builder.block(if (hasArgName && !isLambda) expressionBreakIndent else ZERO) {
- if (hasArgName && !isLambda) {
- builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO)
- }
- if (argument.isSpread) {
- builder.token("*")
- }
- if (isLambda) {
- visitLambdaExpressionInternal(
- argument.getArgumentExpression() as KtLambdaExpression,
- brokeBeforeBrace = null,
- forceBreakBody = forceBreakLambdaBody,
- )
- } else {
- visit(argument.getArgumentExpression())
- }
+ }
+ val indent = if (hasArgName && !isLambda) expressionBreakIndent else ZERO
+ builder.block(indent, isEnabled = wrapInBlock) {
+ if (hasArgName && !isLambda) {
+ builder.breakOp(Doc.FillMode.INDEPENDENT, " ", ZERO)
+ }
+ if (argument.isSpread) {
+ builder.token("*")
+ }
+ if (isLambda) {
+ visitLambdaExpressionInternal(
+ argument.getArgumentExpression() as KtLambdaExpression,
+ brokeBeforeBrace = brokeBeforeBrace,
+ )
+ } else {
+ visit(argument.getArgumentExpression())
}
}
}
@@ -1357,11 +1448,7 @@ class KotlinInputAstVisitor(
else -> throw AssertionError(expr)
}
- visitLambdaExpressionInternal(
- lambdaExpression,
- brokeBeforeBrace = breakToExpr,
- forceBreakBody = false,
- )
+ visitLambdaExpressionInternal(lambdaExpression, brokeBeforeBrace = breakToExpr)
}
override fun visitClassOrObject(classOrObject: KtClassOrObject) {
@@ -1510,7 +1597,7 @@ class KotlinInputAstVisitor(
call.typeArgumentList,
call.valueArgumentList,
call.lambdaArguments,
- lambdaIndent = ZERO)
+ )
}
}
@@ -1744,7 +1831,7 @@ class KotlinInputAstVisitor(
override fun visitSuperTypeList(list: KtSuperTypeList) {
builder.sync(list)
- builder.block(expressionBreakIndent) { forEachCommaSeparated(list.entries) { visit(it) } }
+ builder.block(expressionBreakIndent) { visitEachCommaSeparated(list.entries) }
}
override fun visitSuperTypeCallEntry(call: KtSuperTypeCallEntry) {
@@ -1891,7 +1978,7 @@ class KotlinInputAstVisitor(
builder.token("[")
builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakIndent)
builder.block(expressionBreakIndent) {
- emitParameterLikeList(
+ visitEachCommaSeparated(
expression.indexExpressions, expression.trailingComma != null, wrapInBlock = true)
}
}
@@ -1911,7 +1998,7 @@ class KotlinInputAstVisitor(
builder.token("(")
builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakIndent)
builder.block(expressionBreakIndent) {
- emitParameterLikeList(
+ visitEachCommaSeparated(
destructuringDeclaration.entries, hasTrailingComma, wrapInBlock = true)
}
}
@@ -1973,7 +2060,7 @@ class KotlinInputAstVisitor(
// Break before args.
builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakIndent)
builder.block(expressionBreakIndent) {
- emitParameterLikeList(list.parameters, list.trailingComma != null, wrapInBlock = true)
+ visitEachCommaSeparated(list.parameters, list.trailingComma != null, wrapInBlock = true)
}
}
builder.token(">")
@@ -1998,7 +2085,7 @@ class KotlinInputAstVisitor(
builder.token("where")
builder.space()
builder.sync(list)
- forEachCommaSeparated(list.constraints) { visit(it) }
+ visitEachCommaSeparated(list.constraints)
}
/** Example `T : Foo` */
@@ -2137,10 +2224,16 @@ class KotlinInputAstVisitor(
builder.token(".")
}
builder.block(expressionBreakIndent) {
- builder.token("(")
- visit(type.parameterList)
+ val parameterList = type.parameterList
+ if (parameterList != null) {
+ visitEachCommaSeparated(
+ parameterList.parameters,
+ prefix = "(",
+ postfix = ")",
+ hasTrailingComma = parameterList.trailingComma != null,
+ )
+ }
}
- builder.token(")")
builder.space()
builder.token("->")
builder.space()
@@ -2192,15 +2285,14 @@ class KotlinInputAstVisitor(
*/
override fun visitCollectionLiteralExpression(expression: KtCollectionLiteralExpression) {
builder.sync(expression)
- builder.block(ZERO) {
- builder.token("[")
- builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakIndent)
- builder.block(expressionBreakIndent) {
- emitParameterLikeList(
- expression.getInnerExpressions(), expression.trailingComma != null, wrapInBlock = true)
- }
+ builder.block(expressionBreakIndent) {
+ visitEachCommaSeparated(
+ expression.getInnerExpressions(),
+ expression.trailingComma != null,
+ prefix = "[",
+ postfix = "]",
+ wrapInBlock = true)
}
- builder.token("]")
}
override fun visitTryExpression(expression: KtTryExpression) {
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<String> = 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<String>()
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<KDocToken>()
- 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<KDocToken>,
- 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<KDocToken>, 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("</pre>", 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)) {
+ // "<b>" or </b> -> "**"
+ sb.append('*').append('*')
+ if (s[i] == '/') i++
+ i += 2
+ continue
+ }
+ if (s.startsWith("i>", i, false) || s.startsWith("/i>", i, false)) {
+ // "<i>" or </i> -> "*"
+ sb.append('*')
+ if (s[i] == '/') i++
+ i += 2
+ continue
+ }
+ if (s.startsWith("em>", i, false) || s.startsWith("/em>", i, false)) {
+ // "<em>" or </em> -> "_"
+ sb.append('_')
+ if (s[i] == '/') i++
+ i += 3
+ continue
+ }
+ // (We don't convert <pre> 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)) { // "&lt;" -> "<"
+ sb.append('<')
+ i += 3
+ continue
+ }
+ if (s.startsWith("gt;", i, true)) { // "&gt;" -> ">"
+ 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<String> {
+ 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<String> = 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<String>, lineWidth: Int, hangingIndentSize: Int): List<String> {
+ 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("<li>", true) || wordWithSpace.isQuoted()) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ private fun computeWords(): List<String> {
+ 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<String>(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<String>): List<String> {
+ val count = words.size
+ val lines = ArrayList<String>()
+
+ val offsets = ArrayList<Int>()
+ 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<Quadruple>()
+ 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<String>
+ ): List<String> {
+ // Greedy implementation
+
+ var width = lineWidth
+ if (options.hangingIndent > 0 && hanging && continuation) {
+ width -= getIndentSize(hangingIndent, options)
+ }
+
+ val lines = mutableListOf<String>()
+ 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<Paragraph>) : Iterable<Paragraph> {
+ fun isSingleParagraph() = paragraphs.size <= 1
+ override fun iterator(): Iterator<Paragraph> = 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<Paragraph> = 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 <pre> 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 <pre>
+ // without a closing </pre> 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 <h> 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("<pre>", 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("</code>", "", 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("<code>", "", true).trim()
+ if (after.isNotBlank()) {
+ newParagraph()
+ appendText(after)
+ paragraph.preformatted = true
+ paragraph.allowEmpty = true
+ }
+ }
+
+ handleTag("<pre>")
+ handleTag("</pre>")
+ }
+ },
+ until = { it.contains("</pre>", 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("<ul>", true) ||
+ lineWithoutIndentation.equals("<ol>", true)) {
+ newParagraph(i - 1).block = true
+ appendText(lineWithoutIndentation)
+ newParagraph(i).hanging = true
+ i =
+ addLines(
+ i,
+ includeEnd = true,
+ until = { _, w, _ -> w.equals("</ul>", true) || w.equals("</ol>", true) },
+ customize = { _, p -> p.block = true },
+ shouldBreak = { w, _ ->
+ w.startsWith("<li>", true) ||
+ w.startsWith("</ul>", true) ||
+ w.startsWith("</ol>", 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("<pre>") ||
+ 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("<p>", true) ||
+ lineWithoutIndentation.startsWith("<p/>", true) ||
+ lineWithoutIndentation.startsWith("<h1", true) ||
+ lineWithoutIndentation.startsWith("<h2", true) ||
+ lineWithoutIndentation.startsWith("<h3", true) ||
+ lineWithoutIndentation.startsWith("<h4", true) ||
+ lineWithoutIndentation.startsWith("<table", true) ||
+ lineWithoutIndentation.startsWith("<tr", true) ||
+ lineWithoutIndentation.startsWith("<caption", true) ||
+ lineWithoutIndentation.startsWith("<td", true) ||
+ lineWithoutIndentation.startsWith("<div", true))) {
+ newParagraph(i - 1).block = true
+ if (lineWithoutIndentation.equals("<p>", true) ||
+ lineWithoutIndentation.equals("<p/>", true) ||
+ options.convertMarkup && lineWithoutIndentation.equals("</p>", true)) {
+ if (options.convertMarkup) {
+ // Replace <p> with a blank line
+ paragraph.separate = true
+ } else {
+ appendText(lineWithoutIndentation)
+ newParagraph(i).block = true
+ }
+ continue
+ } else if (lineWithoutIndentation.endsWith("</h1>", true) ||
+ lineWithoutIndentation.endsWith("</h2>", true) ||
+ lineWithoutIndentation.endsWith("</h3>", true) ||
+ lineWithoutIndentation.endsWith("</h4>", true)) {
+ if (lineWithoutIndentation.startsWith("<h", true) &&
+ options.convertMarkup &&
+ paragraph.isEmpty()) {
+ paragraph.separate = true
+ val count = lineWithoutIndentation[lineWithoutIndentation.length - 2] - '0'
+ for (j in 0 until count.coerceAtLeast(0).coerceAtMost(8)) {
+ appendText("#")
+ }
+ appendText(" ")
+ appendText(lineWithoutIndentation.substring(4, lineWithoutIndentation.length - 5))
+ } else if (options.collapseSpaces) {
+ appendText(lineWithoutIndentation.collapseSpaces())
+ } else {
+ appendText(lineWithoutIndentation)
+ }
+ newParagraph(i).block = true
+ continue
+ }
+ }
+
+ i = addPlainText(i, lineWithoutIndentation)
+ }
+ }
+
+ closeParagraph()
+ arrange()
+ if (!lineComment) {
+ punctuate()
+ }
+
+ return ParagraphList(paragraphs)
+ }
+
+ private fun addPlainText(i: Int, text: String, braceBalance: Int = 0): Int {
+ val s =
+ if (options.convertMarkup &&
+ (text.startsWith("<p>", true) || text.startsWith("<p/>", 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<List<Paragraph>> {
+ override fun compare(l1: List<Paragraph>, l2: List<Paragraph>): 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<List<Paragraph>>()
+ var tag: MutableList<Paragraph>? = 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 <pre> tags where it's implicit)
+ paragraph.preformatted ->
+ !prev.preformatted &&
+ !text.startsWith("<pre", true) &&
+ (!text.startsWith("```") || !prev.text.isExpectingMore())
+ prev.preformatted && prev.text.startsWith("</pre>", true) -> false
+ paragraph.continuation -> true
+ paragraph.hanging -> false
+ paragraph.quoted -> prev.quoted
+ text.isHeader() -> true
+ text.startsWith("<p>", true) || text.startsWith("<p/>", true) -> true
+ else -> !paragraph.block && !paragraph.isEmpty()
+ }
+
+ if (paragraph.hanging) {
+ if (paragraph.doc || text.startsWith("<li>", 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<Int>? = 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<Int>().also { levels = it }).add(paragraph.originalIndent)
+ }
+ }
+ }
+ }
+
+ levels?.sorted()?.let { sorted ->
+ val assignments = mutableMapOf<Int, Int>()
+ 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<Int>,
+ private val rows: List<Row>,
+ private val align: List<Align>,
+ private val original: List<String>
+) {
+ fun original(): List<String> {
+ 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<String> {
+ val tableMaxWidth =
+ 2 + widths.sumOf { it + 2 } // +2: "| " in each cell and final " |" on the right
+
+ val pad = tableMaxWidth <= maxWidth
+ val lines = mutableListOf<String>()
+ 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<String>,
+ start: Int,
+ lineContent: (String) -> String
+ ): Pair<Table, Int>? {
+ 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<Row>()
+ 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<Int>()
+ 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<Align>()
+ 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<String>()
+ }
+}
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("<li>", 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("<h", true)
+}
+
+fun String.isQuoted(): Boolean {
+ return startsWith("> ")
+}
+
+fun String.isDirectiveMarker(): Boolean {
+ return startsWith("<!--") || 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<String, String> {
+ 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 <T, R : Comparable<R>> Iterable<T>.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
@@ -123,6 +123,21 @@ class MainTest {
}
@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")
fooBar.writeText("fun f1 ( ")
@@ -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("<stdin>\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=<value>'\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=<value>'\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<ParsedArgs, String> {
+ 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<A>(b) {
+ | c
+ | }
+ | foo.bar(baz).zip<A>(b) {
+ | c
+ | }
+ |}
+ |"""
+ .trimMargin(),
+ deduceMaxWidth = true)
+
+ @Test
+ fun `forward propagation of breaks in call expressions (at value args)`() =
+ assertFormatted(
+ """
+ |----------------------
+ |fun test() {
+ | foo_bar_baz__zip<A>(
+ | b) {
+ | c
+ | }
+ |}
+ |
+ |fun test() {
+ | foo.bar(baz).zip<A>(
+ | 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,21 +1166,55 @@ 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<Int>()
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `list of superclasses over multiple lines`() =
@@ -1519,20 +1708,25 @@ class FormatterTest {
|
|class Derived5 :
| Super3<Int>()
- |""".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 {
| *
| * <p>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<Foo>
|) {}
- |""".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<Foo>,
| 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<Foo>
|) {}
- |""".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<Foo>
| )
|}
- |""".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<FooBar>.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<T>
- |""".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<String, Integer>
| var x: List<String>.Iterator
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle destructuring declaration in for loop`() =
@@ -2721,7 +2951,8 @@ class FormatterTest {
|fun f(a: List<Pair<Int, Int>>) {
| 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<T : @Anno Kip, U> where U : @Anno Kip, U : @Anno Qux {
| fun <T : @Anno Kip, U> bar() where U : @Anno Kip, U : @Anno Qux {}
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `annotations on type arguments`() =
- assertFormatted("""
+ assertFormatted(
+ """
|fun foo(x: Foo<in @Anno Int>) {}
- |""".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<A & B & C>
+ | val p = Ctor<A & B & C, T & Y & Z>
+ |}
+ |"""
+ .trimMargin())
@Test
fun `handle covariant and contravariant type arguments`() =
- assertFormatted("""
+ assertFormatted(
+ """
|val p: Pair<in T, out S>
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle covariant and contravariant type parameters`() =
- assertFormatted("""
+ assertFormatted(
+ """
|class Foo<in T, out S>
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle bounds for type parameters`() =
- assertFormatted("""
+ assertFormatted(
+ """
|class Foo<in T : List<*>, out S : Any?>
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle compound generic bounds on classes`() =
assertFormatted(
"""
|class Foo<T>(n: Int) where T : Bar, T : FooBar {}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle compound generic bounds on functions`() =
assertFormatted(
"""
|fun <T> 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<T>() : 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 <T> List<T>.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<Int>) : Collection<Int> 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<Int> -> a }
| g { (a, b): List<Int>, (c, d): List<Int> -> 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 <reified in T> foo2(t: T) {
| println(t)
|}
- |""".trimMargin())
+ |"""
+ .trimMargin())
@Test
fun `handle suspended types`() =
@@ -3888,7 +4224,8 @@ class FormatterTest {
|inline fun <R> foo(noinline block: suspend () -> R): suspend () -> R
|
|inline fun <R> 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<X, Y> = Pair<Pair<X, Y>, 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<String>::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<UUID, UUIDParceler>() 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<String>) {
| * 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<A>(b) {
+ | c
+ | }
+ | foo.bar(baz).zip<A>(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<A>(
+ | b
+ | ) {
+ | c
+ | }
+ |}
+ |
+ |fun test() {
+ | foo.bar(baz).zip<A>(
+ | 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<Int, Nothing>,
+ | com.example.interesting.SomeType<
+ | com.example.interesting.SomeType<Int, Nothing>,
+ | 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<String>()
+ 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("<div class=\"copy-popup-wrapper popup-to-left\">")
+ val end = rendered.indexOf("<div class=\"tabbedcontent\">", begin)
+ return rendered.substring(begin, end).replace(Regex(" +"), " ").replace(">", ">\n")
+ }
+
+ fun getText(file: File): String? {
+ return if (PANDOC != null) {
+ val pandocFile = File(PANDOC)
+ if (!pandocFile.isFile) {
+ error("Cannot execute $pandocFile")
+ }
+ val outFile = File(out, "text.text")
+ executeProcess(listOf(PANDOC, file.path, "-o", outFile.path))
+ val rendered = outFile.readText()
+
+ val begin = rendered.indexOf("[]{.copy-popup-icon}Content copied to clipboard")
+ val end = rendered.indexOf("::: tabbedcontent", begin)
+ rendered.substring(begin, end).replace(Regex(" +"), " ").replace(">", ">\n")
+ } else {
+ null
+ }
+ }
+
+ val indexBefore = File("$out/root/[root]/-before/index.html")
+ val beforeContents = getHtml(indexBefore)
+ val indexAfter = File("$out/root/[root]/-after/index.html")
+ val afterContents = getHtml(indexAfter)
+ if (beforeContents != afterContents) {
+ val beforeText = getText(indexBefore)
+ val afterText = getText(indexAfter)
+ if (beforeText != null && afterText != null) {
+ assertThat(beforeText).isEqualTo(afterText)
+ return
+ }
+
+ assertThat(beforeContents).isEqualTo(afterContents)
+ }
+ }
+
+ private fun find(prebuilts: File, group: String, artifact: String): File {
+ val versionDir = File(prebuilts, "androidx/external/${group.replace('.','/')}/$artifact")
+ val versions =
+ versionDir.listFiles().filter { it.name.first().isDigit() }.sortedByDescending { it.name }
+ for (version in versions.map { it.name }) {
+ val jar = File(versionDir, "$version/$artifact-$version.jar")
+ if (jar.isFile) {
+ return jar
+ }
+ }
+ error("Could not find a valid jar file for $group:$artifact")
+ }
+
+ private fun executeProcess(args: List<String>) {
+ var input: BufferedReader? = null
+ var error: BufferedReader? = null
+ try {
+ val process = Runtime.getRuntime().exec(args.toTypedArray())
+ input = process.inputStream.bufferedReader()
+ error = process.errorStream.bufferedReader()
+ val exitVal = process.waitFor()
+ if (exitVal != 0) {
+ val sb = StringBuilder()
+ sb.append("Failed to execute process\n")
+ sb.append("Command args:\n")
+ for (arg in args) {
+ sb.append(" ").append(arg).append("\n")
+ }
+ sb.append("Standard output:\n")
+ var line: String?
+ while (input.readLine().also { line = it } != null) {
+ sb.append(line).append("\n")
+ }
+ sb.append("Error output:\n")
+ while (error.readLine().also { line = it } != null) {
+ sb.append(line).append("\n")
+ }
+ error(sb.toString())
+ }
+ } catch (t: Throwable) {
+ val sb = StringBuilder()
+ for (arg in args) {
+ sb.append(" ").append(arg).append("\n")
+ }
+ t.printStackTrace()
+ error("Could not run process:\n$sb")
+ } finally {
+ input?.close()
+ error?.close()
+ }
+ }
+}
diff --git a/core/src/test/java/com/facebook/ktfmt/kdoc/KDocFormatterTest.kt b/core/src/test/java/com/facebook/ktfmt/kdoc/KDocFormatterTest.kt
index 4efdf42..bb1b156 100644
--- a/core/src/test/java/com/facebook/ktfmt/kdoc/KDocFormatterTest.kt
+++ b/core/src/test/java/com/facebook/ktfmt/kdoc/KDocFormatterTest.kt
@@ -1,5 +1,21 @@
/*
- * Copyright (c) Meta Platforms, Inc. and affiliates.
+ * 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.
@@ -16,28 +32,4686 @@
package com.facebook.ktfmt.kdoc
-import com.facebook.ktfmt.kdoc.KDocFormatter.tokenizeKdocText
import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlin.io.path.createTempDirectory
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class KDocFormatterTest {
+ private val tempDir = createTempDirectory().toFile()
+
+ private fun checkFormatter(
+ task: FormattingTask,
+ expected: String,
+ verify: Boolean = true,
+ verifyDokka: Boolean = true,
+ ) {
+ val reformatted = reformatComment(task)
+
+ val indent = task.initialIndent
+ val options = task.options
+ val source = task.comment
+
+ // Because .trimIndent() will remove it:
+ val indentedExpected = expected.split("\n").joinToString("\n") { indent + it }
+
+ assertThat(reformatted).isEqualTo(indentedExpected)
+
+ if (verifyDokka && !options.addPunctuation) {
+ DokkaVerifier(tempDir).verify(source, reformatted)
+ }
+
+ // Make sure that formatting is stable -- format again and make sure it's the same
+ if (verify) {
+ val again =
+ FormattingTask(
+ options,
+ reformatted.trim(),
+ task.initialIndent,
+ task.secondaryIndent,
+ task.orderedParameterNames)
+ val formattedAgain = reformatComment(again)
+ if (reformatted != formattedAgain) {
+ assertWithMessage("Formatting is unstable: if formatted a second time, it changes")
+ .that("$indent// FORMATTED TWICE (implies unstable formatting)\n\n$formattedAgain")
+ .isEqualTo("$indent// FORMATTED ONCE\n\n$reformatted")
+ }
+ }
+ }
+
+ private fun checkFormatter(
+ source: String,
+ options: KDocFormattingOptions,
+ expected: String,
+ indent: String = " ",
+ verify: Boolean = true,
+ verifyDokka: Boolean = true
+ ) {
+ val task = FormattingTask(options, source.trim(), indent)
+ checkFormatter(task, expected, verify, verifyDokka)
+ }
+
+ private fun reformatComment(task: FormattingTask): String {
+ val formatter = KDocFormatter(task.options)
+ val formatted = formatter.reformatComment(task)
+ return task.initialIndent + formatted
+ }
+
+ @Test
+ fun test1() {
+ checkFormatter(
+ """
+ /**
+ * Returns whether lint should check all warnings,
+ * including those off by default, or null if
+ *not configured in this configuration. This is a really really really long sentence which needs to be broken up.
+ * And ThisIsALongSentenceWhichCannotBeBrokenUpAndMustBeIncludedAsAWholeWithoutNewlinesInTheMiddle.
+ *
+ * This is a separate section
+ * which should be flowed together with the first one.
+ * *bold* should not be removed even at beginning.
+ */
+ """
+ .trimIndent(),
+ KDocFormattingOptions(72),
+ """
+ /**
+ * Returns whether lint should check all warnings, including
+ * those off by default, or null if not configured in
+ * this configuration. This is a really really really
+ * long sentence which needs to be broken up. And
+ * ThisIsALongSentenceWhichCannotBeBrokenUpAndMustBeIncludedAsAWholeWithoutNewlinesInTheMiddle.
+ *
+ * This is a separate section which should be flowed together with
+ * the first one. *bold* should not be removed even at beginning.
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testWithOffset() {
+ val source =
+ """
+ /** Returns whether lint should check all warnings,
+ * including those off by default */
+ """
+ .trimIndent()
+ val reformatted =
+ """
+ /**
+ * Returns whether lint should check all warnings, including those
+ * off by default
+ */
+ """
+ .trimIndent()
+ checkFormatter(source, KDocFormattingOptions(72), reformatted, indent = " ")
+ val initialOffset = source.indexOf("default")
+ val newOffset = findSamePosition(source, initialOffset, reformatted)
+ assertThat(newOffset).isNotEqualTo(initialOffset)
+ assertThat(reformatted.substring(newOffset, newOffset + "default".length)).isEqualTo("default")
+ }
+
+ @Test
+ fun testWordBreaking() {
+ // Without special handling, the "-" in the below would be placed at the
+ // beginning of line 2, which then implies a list item.
+ val source =
+ """
+ /** Returns whether lint should check all warnings,
+ * including aaaaaa - off by default */
+ """
+ .trimIndent()
+ val reformatted =
+ """
+ /**
+ * Returns whether lint should check all warnings, including
+ * aaaaaa - off by default
+ */
+ """
+ .trimIndent()
+ checkFormatter(source, KDocFormattingOptions(72), reformatted, indent = " ")
+ val initialOffset = source.indexOf("default")
+ val newOffset = findSamePosition(source, initialOffset, reformatted)
+ assertThat(newOffset).isNotEqualTo(initialOffset)
+ assertThat(reformatted.substring(newOffset, newOffset + "default".length)).isEqualTo("default")
+ }
+
+ @Test
+ fun testHeader() {
+ val source =
+ """
+ /**
+ * Information about a request to run lint.
+ *
+ * **NOTE: This is not a public or final API; if you rely on this be prepared
+ * to adjust your code for the next tools release.**
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72),
+ """
+ /**
+ * Information about a request to run lint.
+ *
+ * **NOTE: This is not a public or final API; if you rely on this be
+ * prepared to adjust your code for the next tools release.**
+ */
+ """
+ .trimIndent())
+
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40),
+ """
+ /**
+ * Information about a request to run
+ * lint.
+ *
+ * **NOTE: This is not a public or final
+ * API; if you rely on this be prepared
+ * to adjust your code for the next
+ * tools release.**
+ */
+ """
+ .trimIndent(),
+ indent = "")
+
+ checkFormatter(
+ source,
+ KDocFormattingOptions(100, 100),
+ """
+ /**
+ * Information about a request to run lint.
+ *
+ * **NOTE: This is not a public or final API; if you rely on this be prepared to adjust your code
+ * for the next tools release.**
+ */
+ """
+ .trimIndent(),
+ indent = "")
+ }
+
+ @Test
+ fun testSingle() {
+ val source =
+ """
+ /**
+ * The lint client requesting the lint check
+ *
+ * @return the client, never null
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72),
+ """
+ /**
+ * The lint client requesting the lint check
+ *
+ * @return the client, never null
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testEmpty() {
+ val source =
+ """
+ /** */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72),
+ """
+ /** */
+ """
+ .trimIndent())
+
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72).apply { collapseSingleLine = false },
+ """
+ /**
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testJavadocParams() {
+ val source =
+ """
+ /**
+ * Sets the scope to use; lint checks which require a wider scope set
+ * will be ignored
+ *
+ * @param scope the scope
+ *
+ * @return this, for constructor chaining
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72),
+ """
+ /**
+ * Sets the scope to use; lint checks which require a wider scope
+ * set will be ignored
+ *
+ * @param scope the scope
+ * @return this, for constructor chaining
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testBracketParam() {
+ // Regression test for https://github.com/tnorbye/kdoc-formatter/issues/72
+ val source =
+ """
+ /**
+ * Summary
+ * @param [ param1 ] some value
+ * @param[param2] another value
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72),
+ """
+ /**
+ * Summary
+ *
+ * @param param1 some value
+ * @param param2 another value
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testMultiLineLink() {
+ // Regression test for https://github.com/tnorbye/kdoc-formatter/issues/70
+ val source =
+ """
+ /**
+ * Single line is converted {@link foo}
+ *
+ * Multi line is converted {@link
+ * foo}
+ *
+ * Single line with hash is converted {@link #foo}
+ *
+ * Multi line with has is converted {@link
+ * #foo}
+ *
+ * Don't interpret {@code
+ * # This is not a header
+ * * this is
+ * * not a nested list
+ * }
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72),
+ """
+ /**
+ * Single line is converted [foo]
+ *
+ * Multi line is converted [foo]
+ *
+ * Single line with hash is converted [foo]
+ *
+ * Multi line with has is converted [foo]
+ *
+ * Don't interpret {@code # This is not a header * this is * not a
+ * nested list }
+ */
+ """
+ .trimIndent(),
+ // {@link} text is not rendered by dokka when it cannot resolve the symbols
+ verifyDokka = false)
+ }
+
+ @Test
+ fun testPreformattedWithinCode() {
+ // Regression test for https://github.com/tnorbye/kdoc-formatter/issues/77
+ val source =
+ """
+ /**
+ * Some summary.
+ * {@code
+ *
+ * foo < bar?}
+ * Done.
+ *
+ *
+ * {@code
+ * ```
+ * Some code.
+ * ```
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72),
+ """
+ /**
+ * Some summary. {@code
+ *
+ * foo < bar?} Done.
+ *
+ * {@code
+ *
+ * ```
+ * Some code.
+ * ```
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testPreStability() {
+ // Regression test for https://github.com/tnorbye/kdoc-formatter/issues/78
+ val source =
+ """
+ /**
+ * Some summary
+ *
+ * <pre>
+ * line one
+ * ```
+ * line two
+ * ```
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72),
+ """
+ /**
+ * Some summary
+ * <pre>
+ * line one
+ * ```
+ * line two
+ * ```
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testPreStability2() {
+ // Regression test for https://github.com/tnorbye/kdoc-formatter/issues/78
+ // (second scenario
+ val source =
+ """
+ /**
+ * Some summary
+ *
+ * <pre>
+ * ```
+ * code
+ * ```
+ * </pre>
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72),
+ """
+ /**
+ * Some summary
+ * <pre>
+ * ```
+ * code
+ * ```
+ * </pre>
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testConvertParamReference() {
+ // Regression test for https://github.com/tnorbye/kdoc-formatter/issues/79
+ val source =
+ """
+ /**
+ * Some summary.
+ *
+ * Another summary about {@param someParam}.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72),
+ """
+ /**
+ * Some summary.
+ *
+ * Another summary about [someParam].
+ */
+ """
+ .trimIndent(),
+ // {@param reference} text is not rendered by dokka when it cannot resolve the symbols
+ verifyDokka = false)
+ }
+
+ @Test
+ fun testLineWidth1() {
+ // Perform in KDocFileFormatter test too to make sure we properly account
+ // for indent!
+ val source =
+ """
+ /**
+ * 89 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789
+ *
+ * 10 20 30 40 50 60 70 80
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72),
+ """
+ /**
+ * 89 123456789 123456789 123456789 123456789 123456789 123456789
+ * 123456789 123456789
+ *
+ * 10 20 30 40 50 60 70 80
+ */
+ """
+ .trimIndent())
+
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40),
+ """
+ /**
+ * 89 123456789 123456789 123456789
+ * 123456789 123456789 123456789
+ * 123456789 123456789
+ *
+ * 10 20 30 40 50 60 70 80
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testBlockTagsNoSeparators() {
+ checkFormatter(
+ """
+ /**
+ * Marks the given warning as "ignored".
+ *
+ * @param context The scanning context
+ * @param issue the issue to be ignored
+ * @param location The location to ignore the warning at, if any
+ * @param message The message for the warning
+ */
+ """
+ .trimIndent(),
+ KDocFormattingOptions(72),
+ """
+ /**
+ * Marks the given warning as "ignored".
+ *
+ * @param context The scanning context
+ * @param issue the issue to be ignored
+ * @param location The location to ignore the warning at, if any
+ * @param message The message for the warning
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testBlockTagsHangingIndents() {
+ val options = KDocFormattingOptions(40)
+ options.hangingIndent = 6
+ checkFormatter(
+ """
+ /**
+ * Creates a list of class entries from the given class path and specific set of files within
+ * it.
+ *
+ * @param client the client to report errors to and to use to read files
+ * @param classFiles the specific set of class files to look for
+ * @param classFolders the list of class folders to look in (to determine the package root)
+ * @param sort if true, sort the results
+ * @return the list of class entries, never null.
+ */
+ """
+ .trimIndent(),
+ options,
+ """
+ /**
+ * Creates a list of class entries
+ * from the given class path and
+ * specific set of files within it.
+ *
+ * @param client the client to
+ * report errors to and to use
+ * to read files
+ * @param classFiles the specific
+ * set of class files to look
+ * for
+ * @param classFolders the list of
+ * class folders to look in
+ * (to determine the package
+ * root)
+ * @param sort if true, sort the
+ * results
+ * @return the list of class
+ * entries, never null.
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testGreedyBlockIndent() {
+ val options = KDocFormattingOptions(100, 72)
+ options.hangingIndent = 6
+ checkFormatter(
+ """
+ /**
+ * Returns the project resources, if available
+ *
+ * @param includeModuleDependencies if true, include merged view of
+ * all module dependencies
+ * @param includeLibraries if true, include merged view of all
+ * library dependencies (this also requires all module dependencies)
+ * @return the project resources, or null if not available
+ */
+ """
+ .trimIndent(),
+ options,
+ """
+ /**
+ * Returns the project resources, if available
+ *
+ * @param includeModuleDependencies if true, include merged view of all
+ * module dependencies
+ * @param includeLibraries if true, include merged view of all library
+ * dependencies (this also requires all module dependencies)
+ * @return the project resources, or null if not available
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testBlockTagsHangingIndents2() {
+ checkFormatter(
+ """
+ /**
+ * @param client the client to
+ * report errors to and to use to
+ * read files
+ */
+ """
+ .trimIndent(),
+ KDocFormattingOptions(40),
+ """
+ /**
+ * @param client the client to
+ * report errors to and to use to
+ * read files
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testSingleLine() {
+ // Also tests punctuation feature.
+ val source =
+ """
+ /**
+ * This could all fit on one line
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72),
+ """
+ /** This could all fit on one line */
+ """
+ .trimIndent())
+ val options = KDocFormattingOptions(72)
+ options.collapseSingleLine = false
+ options.addPunctuation = true
+ checkFormatter(
+ source,
+ options,
+ """
+ /**
+ * This could all fit on one line.
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testPunctuationWithLabelLink() {
+ val source =
+ """
+ /** Default implementation of [MyInterface] */
+ """
+ .trimIndent()
+
+ val options = KDocFormattingOptions(72)
+ options.addPunctuation = true
+ checkFormatter(
+ source,
+ options,
+ """
+ /** Default implementation of [MyInterface]. */
+ """
+ .trimIndent())
+ }
+
@Test
- fun testTokenizeKdocText() {
- assertThat(tokenizeKdocText(" one two three ").asIterable())
- .containsExactly(" ", "one", " ", "two", " ", "three", " ")
- .inOrder()
- assertThat(tokenizeKdocText("one two three ").asIterable())
- .containsExactly("one", " ", "two", " ", "three", " ")
- .inOrder()
- assertThat(tokenizeKdocText("one two three").asIterable())
- .containsExactly("one", " ", "two", " ", "three")
- .inOrder()
- assertThat(tokenizeKdocText("onetwothree").asIterable())
- .containsExactly("onetwothree")
- .inOrder()
- assertThat(tokenizeKdocText("").asIterable()).isEmpty()
+ fun testWrapingOfLinkText() {
+ val source =
+ """
+ /**
+ * Sometimes the text of a link can have spaces, like [this link's text](https://example.com).
+ * The following text should wrap like usual.
+ */
+ """
+ .trimIndent()
+
+ val options = KDocFormattingOptions(72)
+ checkFormatter(
+ source,
+ options,
+ """
+ /**
+ * Sometimes the text of a link can have spaces, like
+ * [this link's text](https://example.com). The following text
+ * should wrap like usual.
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testPreformattedTextIndented() {
+ val source =
+ """
+ /**
+ * Parser for the list of forward socket connection returned by the
+ * `host:forward-list` command.
+ *
+ * Input example
+ *
+ * ```
+ *
+ * HT75B1A00212 tcp:51222 tcp:5000 HT75B1A00212 tcp:51227 tcp:5001
+ * HT75B1A00212 tcp:51232 tcp:5002 HT75B1A00212 tcp:51239 tcp:5003
+ * HT75B1A00212 tcp:51244 tcp:5004
+ *
+ * ```
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source, KDocFormattingOptions(72, 72).apply { convertMarkup = true }, source, indent = "")
+ }
+
+ @Test
+ fun testPreformattedText() {
+ val source =
+ """
+ /**
+ * Code sample:
+ *
+ * val s = "hello, and this is code so should not be line broken at all, it should stay on one line";
+ * println(s);
+ *
+ * This is not preformatted and can be combined into multiple sentences again.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40),
+ """
+ /**
+ * Code sample:
+ *
+ * val s = "hello, and this is code so should not be line broken at all, it should stay on one line";
+ * println(s);
+ *
+ * This is not preformatted and
+ * can be combined into multiple
+ * sentences again.
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testPreformattedText2() {
+ val source =
+ """
+ /**
+ * Code sample:
+ * ```kotlin
+ * val s = "hello, and this is code so should not be line broken at all, it should stay on one line";
+ * println(s);
+ * ```
+ *
+ * This is not preformatted and can be combined into multiple sentences again.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40),
+ """
+ /**
+ * Code sample:
+ * ```kotlin
+ * val s = "hello, and this is code so should not be line broken at all, it should stay on one line";
+ * println(s);
+ * ```
+ *
+ * This is not preformatted and
+ * can be combined into multiple
+ * sentences again.
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testPreformattedText3() {
+ val source =
+ """
+ /**
+ * Code sample:
+ * <PRE>
+ * val s = "hello, and this is code so should not be line broken at all, it should stay on one line";
+ * println(s);
+ * </pre>
+ * This is not preformatted and can be combined into multiple sentences again.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40),
+ """
+ /**
+ * Code sample:
+ * ```
+ * val s = "hello, and this is code so should not be line broken at all, it should stay on one line";
+ * println(s);
+ * ```
+ *
+ * This is not preformatted and
+ * can be combined into multiple
+ * sentences again.
+ */
+ """
+ .trimIndent(),
+ // <pre> and ``` are rendered differently; this is an intentional diff
+ verifyDokka = false)
+ }
+
+ @Test
+ fun testPreformattedTextWithBlankLines() {
+ val source =
+ """
+ /**
+ * Code sample:
+ * ```kotlin
+ * val s = "hello, and this is code so should not be line broken at all, it should stay on one line";
+ *
+ * println(s);
+ * ```
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40),
+ """
+ /**
+ * Code sample:
+ * ```kotlin
+ * val s = "hello, and this is code so should not be line broken at all, it should stay on one line";
+ *
+ * println(s);
+ * ```
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testPreformattedTextWithBlankLinesAndTrailingSpaces() {
+ val source =
+ """
+ /**
+ * Code sample:
+ * ```kotlin
+ * val s = "hello, and this is code so should not be line broken at all, it should stay on one line";
+ *
+ * println(s);
+ * ```
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40),
+ """
+ /**
+ * Code sample:
+ * ```kotlin
+ * val s = "hello, and this is code so should not be line broken at all, it should stay on one line";
+ *
+ * println(s);
+ * ```
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testPreformattedTextSeparation() {
+ val source =
+ """
+ /**
+ * For example,
+ *
+ * val s = "hello, and this is code so should not be line broken at all, it should stay on one line";
+ * println(s);
+ * And here's another example:
+ * This is not preformatted text.
+ *
+ * And a third example,
+ *
+ * ```
+ * Preformatted.
+ * ```
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40),
+ """
+ /**
+ * For example,
+ *
+ * val s = "hello, and this is code so should not be line broken at all, it should stay on one line";
+ * println(s);
+ *
+ * And here's another example: This
+ * is not preformatted text.
+ *
+ * And a third example,
+ * ```
+ * Preformatted.
+ * ```
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testSeparateParagraphMarkers1() {
+ // If the markup still contains HTML paragraph separators, separate
+ // paragraphs
+ val source =
+ """
+ /**
+ * Here's paragraph 1.
+ *
+ * And here's paragraph 2.
+ * <p>And here's paragraph 3.
+ * <P/>And here's paragraph 4.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40).apply { convertMarkup = true },
+ """
+ /**
+ * Here's paragraph 1.
+ *
+ * And here's paragraph 2.
+ *
+ * And here's paragraph 3.
+ *
+ * And here's paragraph 4.
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testSeparateParagraphMarkers2() {
+ // From ktfmt Tokenizer.kt
+ val source =
+ """
+ /**
+ * Tokenizer traverses a Kotlin parse tree (which blessedly contains whitespaces and comments,
+ * unlike Javac) and constructs a list of 'Tok's.
+ *
+ * <p>The google-java-format infra expects newline Toks to be separate from maximal-whitespace Toks,
+ * but Kotlin emits them together. So, we split them using Java's \R regex matcher. We don't use
+ * 'split' et al. because we want Toks for the newlines themselves.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(100, 100).apply {
+ convertMarkup = true
+ optimal = false
+ },
+ """
+ /**
+ * Tokenizer traverses a Kotlin parse tree (which blessedly contains whitespaces and comments,
+ * unlike Javac) and constructs a list of 'Tok's.
+ *
+ * The google-java-format infra expects newline Toks to be separate from maximal-whitespace Toks,
+ * but Kotlin emits them together. So, we split them using Java's \R regex matcher. We don't use
+ * 'split' et al. because we want Toks for the newlines themselves.
+ */
+ """
+ .trimIndent(),
+ indent = "")
+ }
+
+ @Test
+ fun testConvertMarkup() {
+ // If the markup still contains HTML paragraph separators, separate
+ // paragraphs
+ val source =
+ """
+ /**
+ * This is <b>bold</b>, this is <i>italics</i>, but nothing
+ * should be converted in `<b>code</b>` or in
+ * ```
+ * <i>preformatted text</i>
+ * ```
+ * And this \` is <b>not code and should be converted</b>.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40).apply { convertMarkup = true },
+ """
+ /**
+ * This is **bold**, this is
+ * *italics*, but nothing should be
+ * converted in `<b>code</b>` or in
+ *
+ * ```
+ * <i>preformatted text</i>
+ * ```
+ *
+ * And this \` is **not code and
+ * should be converted**.
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testFormattingList() {
+ val source =
+ """
+ /**
+ * 1. This is a numbered list.
+ * 2. This is another item. We should be wrapping extra text under the same item.
+ * 3. This is the third item.
+ *
+ * Unordered list:
+ * * First
+ * * Second
+ * * Third
+ *
+ * Other alternatives:
+ * - First
+ * - Second
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40),
+ """
+ /**
+ * 1. This is a numbered list.
+ * 2. This is another item. We
+ * should be wrapping extra text
+ * under the same item.
+ * 3. This is the third item.
+ *
+ * Unordered list:
+ * * First
+ * * Second
+ * * Third
+ *
+ * Other alternatives:
+ * - First
+ * - Second
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testList1() {
+ val source =
+ """
+ /**
+ * * pre.errorlines: General > Text > Default Text
+ * * .prefix: XML > Namespace Prefix
+ * * .attribute: XML > Attribute name
+ * * .value: XML > Attribute value
+ * * .tag: XML > Tag name
+ * * .lineno: For color, General > Code > Line number, Foreground, and for background-color,
+ * Editor > Gutter background
+ * * .error: General > Errors and Warnings > Error
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40),
+ """
+ /**
+ * * pre.errorlines: General >
+ * Text > Default Text
+ * * .prefix: XML > Namespace Prefix
+ * * .attribute: XML > Attribute
+ * name
+ * * .value: XML > Attribute value
+ * * .tag: XML > Tag name
+ * * .lineno: For color, General >
+ * Code > Line number, Foreground,
+ * and for background-color,
+ * Editor > Gutter background
+ * * .error: General > Errors and
+ * Warnings > Error
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testIndentedList() {
+ val source =
+ """
+ /**
+ * Basic usage:
+ * 1. Create a configuration via [UastEnvironment.Configuration.create] and mutate it as needed.
+ * 2. Create a project environment via [UastEnvironment.create].
+ * You can create multiple environments in the same process (one for each "module").
+ * 3. Call [analyzeFiles] to initialize PSI machinery and precompute resolve information.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40),
+ """
+ /**
+ * Basic usage:
+ * 1. Create a configuration via
+ * [UastEnvironment.Configuration.create]
+ * and mutate it as needed.
+ * 2. Create a project environment
+ * via [UastEnvironment.create].
+ * You can create multiple
+ * environments in the same
+ * process (one for each
+ * "module").
+ * 3. Call [analyzeFiles] to
+ * initialize PSI machinery and
+ * precompute resolve
+ * information.
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testDocTags() {
+ val source =
+ """
+ /**
+ * @param configuration the configuration to look up which issues are
+ * enabled etc from
+ * @param platforms the platforms applying to this analysis
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40),
+ """
+ /**
+ * @param configuration the
+ * configuration to look up which
+ * issues are enabled etc from
+ * @param platforms the platforms
+ * applying to this analysis
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testAtInMiddle() {
+ val source =
+ """
+ /**
+ * If non-null, this issue can **only** be suppressed with one of the
+ * given annotations: not with @Suppress, not with @SuppressLint, not
+ * with lint.xml, not with lintOptions{} and not with baselines.
+ *
+ * Test @IntRange and @FloatRange support annotation applied to
+ * arrays and vargs.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72),
+ """
+ /**
+ * If non-null, this issue can **only** be suppressed with
+ * one of the given annotations: not with @Suppress, not
+ * with @SuppressLint, not with lint.xml, not with lintOptions{} and
+ * not with baselines.
+ *
+ * Test @IntRange and @FloatRange support annotation applied to
+ * arrays and vargs.
+ */
+ """
+ .trimIndent(),
+ )
+ }
+
+ @Test
+ fun testMaxCommentWidth() {
+ checkFormatter(
+ """
+ /**
+ * Returns whether lint should check all warnings,
+ * including those off by default, or null if
+ *not configured in this configuration. This is a really really really long sentence which needs to be broken up.
+ * This is a separate section
+ * which should be flowed together with the first one.
+ * *bold* should not be removed even at beginning.
+ */
+ """
+ .trimIndent(),
+ KDocFormattingOptions(maxLineWidth = 100, maxCommentWidth = 30),
+ """
+ /**
+ * Returns whether lint should
+ * check all warnings, including
+ * those off by default, or
+ * null if not configured in
+ * this configuration. This is
+ * a really really really long
+ * sentence which needs to be
+ * broken up. This is a separate
+ * section which should be flowed
+ * together with the first one.
+ * *bold* should not be removed
+ * even at beginning.
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testHorizontalRuler() {
+ checkFormatter(
+ """
+ /**
+ * This is a header. Should appear alone.
+ * --------------------------------------
+ *
+ * This should not be on the same line as the header.
+ */
+ """
+ .trimIndent(),
+ KDocFormattingOptions(maxLineWidth = 100, maxCommentWidth = 30),
+ """
+ /**
+ * This is a header. Should
+ * appear alone.
+ * --------------------------------------
+ * This should not be on the same
+ * line as the header.
+ */
+ """
+ .trimIndent(),
+ verifyDokka = false)
+ }
+
+ @Test
+ fun testQuoteOnlyOnFirstLine() {
+ checkFormatter(
+ """
+ /**
+ * More:
+ * > This whole paragraph should be treated as a block quote.
+ * This whole paragraph should be treated as a block quote.
+ * This whole paragraph should be treated as a block quote.
+ * This whole paragraph should be treated as a block quote.
+ * @sample Sample
+ */
+ """
+ .trimIndent(),
+ KDocFormattingOptions(maxLineWidth = 100, maxCommentWidth = 30),
+ """
+ /**
+ * More:
+ * > This whole paragraph should
+ * > be treated as a block quote.
+ * > This whole paragraph should
+ * > be treated as a block quote.
+ * > This whole paragraph should
+ * > be treated as a block quote.
+ * > This whole paragraph should
+ * > be treated as a block quote.
+ *
+ * @sample Sample
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testNoBreakUrl() {
+ checkFormatter(
+ """
+ /**
+ * # Design
+ * The splash screen icon uses the same specifications as
+ * [Adaptive Icons](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive)
+ */
+ """
+ .trimIndent(),
+ KDocFormattingOptions(maxLineWidth = 100, maxCommentWidth = 100),
+ """
+ /**
+ * # Design
+ * The splash screen icon uses the same specifications as
+ * [Adaptive Icons](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive)
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testAsciiArt() {
+ // Comment from
+ // https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:build-system/integration-test/application/src/test/java/com/android/build/gradle/integration/bundle/DynamicFeatureAndroidTestBuildTest.kt
+ checkFormatter(
+ """
+ /**
+ * Base <------------ Middle DF <------------- DF <--------- Android Test DF
+ * / \ / \ | / \ \
+ * v v v v v v \ \
+ * appLib sharedLib midLib sharedMidLib featureLib testFeatureLib \ \
+ * ^ ^_______________________________________/ /
+ * |________________________________________________________________/
+ *
+ * DF has a feature-on-feature dep on Middle DF, both depend on Base, Android Test DF is an
+ * android test variant for DF.
+ *
+ * Base depends on appLib and sharedLib.
+ * Middle DF depends on midLib and sharedMidLib.
+ * DF depends on featureLib.
+ * DF also has an android test dependency on testFeatureLib, shared and sharedMidLib.
+ */
+ """
+ .trimIndent(),
+ KDocFormattingOptions(maxLineWidth = 100, maxCommentWidth = 30),
+ """
+ /**
+ * Base <------------ Middle DF <------------- DF <--------- Android Test DF
+ * / \ / \ | / \ \
+ * v v v v v v \ \
+ * appLib sharedLib midLib sharedMidLib featureLib testFeatureLib \ \
+ * ^ ^_______________________________________/ /
+ * |________________________________________________________________/
+ *
+ * DF has a feature-on-feature
+ * dep on Middle DF, both depend
+ * on Base, Android Test DF is an
+ * android test variant for DF.
+ *
+ * Base depends on appLib and
+ * sharedLib. Middle DF depends
+ * on midLib and sharedMidLib. DF
+ * depends on featureLib. DF also
+ * has an android test dependency
+ * on testFeatureLib, shared and
+ * sharedMidLib.
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testAsciiArt2() {
+ checkFormatter(
+ """
+ /**
+ * +-> lib1
+ * |
+ * feature1 ---+-> javalib1
+ * |
+ * +-> baseModule
+ */
+ """
+ .trimIndent(),
+ KDocFormattingOptions(maxLineWidth = 100, maxCommentWidth = 30),
+ """
+ /**
+ * +-> lib1
+ * |
+ * feature1 ---+-> javalib1
+ * |
+ * +-> baseModule
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testAsciiArt3() {
+ val source =
+ """
+ /**
+ * This test creates a layout of this shape:
+ *
+ * ---------------
+ * | t | |
+ * | | |
+ * | |-------| |
+ * | | t | |
+ * | | | |
+ * | | | |
+ * |--| |-------|
+ * | | | t |
+ * | | | |
+ * | | | |
+ * | |--| |
+ * | | |
+ * ---------------
+ *
+ * There are 3 staggered children and 3 pointers, the first is on child 1, the second is on
+ * child 2 in a space that overlaps child 1, and the third is in a space in child 3 that
+ * overlaps child 2.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 100, maxCommentWidth = 30),
+ """
+ /**
+ * This test creates a layout of
+ * this shape:
+ * ---------------
+ * | t | | | | | | |-------| | |
+ * | t | | | | | | | | | | |--|
+ * |-------| | | | t | | | | | |
+ * | | | | |--| | | | |
+ * ---------------
+ * There are 3 staggered children
+ * and 3 pointers, the first is
+ * on child 1, the second is
+ * on child 2 in a space that
+ * overlaps child 1, and the
+ * third is in a space in child
+ * 3 that overlaps child 2.
+ */
+ """
+ .trimIndent(),
+ indent = "")
+ }
+
+ @Test
+ fun testBrokenAsciiArt() {
+ // The first illustration has indentation 3, not 4, so isn't preformatted.
+ // The formatter will garble this -- but so will Dokka!
+ // From androidx' TwoDimensionalFocusTraversalOutTest.kt
+ checkFormatter(
+ """
+ /**
+ * ___________________________
+ * | grandparent |
+ * | _____________________ |
+ * | | parent | |
+ * | | _______________ | | ____________
+ * | | | focusedItem | | | | nextItem |
+ * | | |______________| | | |___________|
+ * | |____________________| |
+ * |__________________________|
+ *
+ * __________________________
+ * | grandparent |
+ * | ____________________ |
+ * | | parent | |
+ * | | ______________ | |
+ * | | | focusedItem | | |
+ * | | |_____________| | |
+ * | |___________________| |
+ * |_________________________|
+ */
+ """
+ .trimIndent(),
+ KDocFormattingOptions(maxLineWidth = 100, 100),
+ """
+ /**
+ * ___________________________ | grandparent | | _____________________ | | | parent
+ * | | | | _______________ | | ____________ | | | focusedItem | | | | nextItem | | |
+ * |______________| | | |___________| | |____________________| | |__________________________|
+ *
+ * __________________________
+ * | grandparent |
+ * | ____________________ |
+ * | | parent | |
+ * | | ______________ | |
+ * | | | focusedItem | | |
+ * | | |_____________| | |
+ * | |___________________| |
+ * |_________________________|
+ */
+ """
+ .trimIndent(),
+ verifyDokka = false)
+ }
+
+ @Test
+ fun testHtmlLists() {
+ checkFormatter(
+ """
+ /**
+ * <ul>
+ * <li>Incremental merge will never clean the output.
+ * <li>The inputs must be able to tell which changes to relative files have been made.
+ * <li>Intermediate state must be saved between merges.
+ * </ul>
+ */
+ """
+ .trimIndent(),
+ KDocFormattingOptions(maxLineWidth = 100, maxCommentWidth = 60),
+ """
+ /**
+ * <ul>
+ * <li>Incremental merge will never clean the output.
+ * <li>The inputs must be able to tell which changes to
+ * relative files have been made.
+ * <li>Intermediate state must be saved between merges.
+ * </ul>
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testVariousMarkup() {
+ val source =
+ """
+ /**
+ * This document contains a bunch of markup examples
+ * that I will use
+ * to verify that things are handled
+ * correctly via markdown.
+ *
+ * This is a header. Should appear alone.
+ * --------------------------------------
+ * This should not be on the same line as the header.
+ *
+ * This is a header. Should appear alone.
+ * -
+ * This should not be on the same line as the header.
+ *
+ * This is a header. Should appear alone.
+ * ======================================
+ * This should not be on the same line as the header.
+ *
+ * This is a header. Should appear alone.
+ * =
+ * This should not be on the same line as the header.
+ * Note that we don't treat this as a header
+ * because it's not on its own line. Instead
+ * it's considered a separating line.
+ * ---
+ * More text. Should not be on the previous line.
+ *
+ * --- This usage of --- where it's not on its own
+ * line should not be used as a header or separator line.
+ *
+ * List stuff:
+ * 1. First item
+ * 2. Second item
+ * 3. Third item
+ *
+ * # Text styles #
+ * **Bold**, *italics*. \*Not italics\*.
+ *
+ * ## More text styles
+ * ~~strikethrough~~, _underlined_.
+ *
+ * ### Blockquotes #
+ *
+ * More:
+ * > This whole paragraph should be treated as a block quote.
+ * This whole paragraph should be treated as a block quote.
+ * This whole paragraph should be treated as a block quote.
+ * This whole paragraph should be treated as a block quote.
+ *
+ * ### Lists
+ * Plus lists:
+ * + First
+ * + Second
+ * + Third
+ *
+ * Dash lists:
+ * - First
+ * - Second
+ * - Third
+ *
+ * List items with multiple paragraphs:
+ *
+ * * This is my list item. It has
+ * text on many lines.
+ *
+ * This is a continuation of the first bullet.
+ * * And this is the second.
+ *
+ * ### Code blocks in list items
+ *
+ * Escapes: I should look for cases where I place a number followed
+ * by a period (or asterisk) at the beginning of a line and if so,
+ * escape it:
+ *
+ * The meaning of life:
+ * 42\. This doesn't seem to work in IntelliJ's markdown formatter.
+ *
+ * ### Horizontal rules
+ * *********
+ * ---------
+ * ***
+ * * * *
+ * - - -
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(100, 100),
+ """
+ /**
+ * This document contains a bunch of markup examples that I will use to verify that things are
+ * handled correctly via markdown.
+ *
+ * This is a header. Should appear alone.
+ * --------------------------------------
+ * This should not be on the same line as the header.
+ *
+ * This is a header. Should appear alone.
+ * -
+ * This should not be on the same line as the header.
+ *
+ * This is a header. Should appear alone.
+ * ======================================
+ * This should not be on the same line as the header.
+ *
+ * This is a header. Should appear alone.
+ * =
+ * This should not be on the same line as the header. Note that we don't treat this as a header
+ * because it's not on its own line. Instead it's considered a separating line.
+ * ---
+ * More text. Should not be on the previous line.
+ *
+ * --- This usage of --- where it's not on its own line should not be used as a header or
+ * separator line.
+ *
+ * List stuff:
+ * 1. First item
+ * 2. Second item
+ * 3. Third item
+ *
+ * # Text styles #
+ * **Bold**, *italics*. \*Not italics\*.
+ *
+ * ## More text styles
+ * ~~strikethrough~~, _underlined_.
+ *
+ * ### Blockquotes #
+ *
+ * More:
+ * > This whole paragraph should be treated as a block quote. This whole paragraph should be
+ * > treated as a block quote. This whole paragraph should be treated as a block quote. This whole
+ * > paragraph should be treated as a block quote.
+ *
+ * ### Lists
+ * Plus lists:
+ * + First
+ * + Second
+ * + Third
+ *
+ * Dash lists:
+ * - First
+ * - Second
+ * - Third
+ *
+ * List items with multiple paragraphs:
+ * * This is my list item. It has text on many lines.
+ *
+ * This is a continuation of the first bullet.
+ * * And this is the second.
+ *
+ * ### Code blocks in list items
+ *
+ * Escapes: I should look for cases where I place a number followed by a period (or asterisk) at
+ * the beginning of a line and if so, escape it:
+ *
+ * The meaning of life: 42\. This doesn't seem to work in IntelliJ's markdown formatter.
+ *
+ * ### Horizontal rules
+ * *********
+ * ---------
+ * ***
+ * * * *
+ * - - -
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testLineComments() {
+ val source =
+ """
+ //
+ // Information about a request to run lint.
+ //
+ // **NOTE: This is not a public or final API; if you rely on this be prepared
+ // to adjust your code for the next tools release.**
+ //
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40),
+ """
+ // Information about a request to
+ // run lint.
+ //
+ // **NOTE: This is not a public or
+ // final API; if you rely on this be
+ // prepared to adjust your code for
+ // the next tools release.**
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testMoreLineComments() {
+ val source =
+ """
+ // Do not clean
+ // this
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(70),
+ """
+ // Do not clean this
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testListContinuations() {
+ val source =
+ """
+ /**
+ * * This is my list item. It has
+ * text on many lines.
+ *
+ * This is a continuation of the first bullet.
+ * * And this is the second.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40),
+ """
+ /**
+ * * This is my list item. It has
+ * text on many lines.
+ *
+ * This is a continuation of the
+ * first bullet.
+ * * And this is the second.
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testListContinuations2() {
+ val source =
+ "/**\n" +
+ """
+ List items with multiple paragraphs:
+
+ * This is my list item. It has
+ text on many lines.
+
+ This is a continuation of the first bullet.
+ * And this is the second.
+ """
+ .trimIndent()
+ .split("\n")
+ .joinToString(separator = "\n") { " * $it".trimEnd() } +
+ "\n */"
+
+ checkFormatter(
+ source,
+ KDocFormattingOptions(100),
+ """
+ /**
+ * List items with multiple paragraphs:
+ * * This is my list item. It has text on many lines.
+ *
+ * This is a continuation of the first bullet.
+ * * And this is the second.
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testAccidentalHeader() {
+ val source =
+ """
+ /**
+ * Constructs a simplified version of the internal JVM description of the given method. This is
+ * in the same format as {@link #getMethodDescription} above, the difference being we don't have
+ * the actual PSI for the method type, we just construct the signature from the [method] name,
+ * the list of [argumentTypes] and optionally include the [returnType].
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72),
+ // Note how this places the "#" in column 0 which will then
+ // be re-interpreted as a header next time we format it!
+ // Idea: @{link #} should become {@link#} or with a nbsp;
+ """
+ /**
+ * Constructs a simplified version of the internal JVM
+ * description of the given method. This is in the same format as
+ * [getMethodDescription] above, the difference being we don't
+ * have the actual PSI for the method type, we just construct the
+ * signature from the [method] name, the list of [argumentTypes] and
+ * optionally include the [returnType].
+ */
+ """
+ .trimIndent(),
+ // {@link} text is not rendered by dokka when it cannot resolve the symbols
+ verifyDokka = false)
+ }
+
+ @Test
+ fun testTODO() {
+ val source =
+ """
+ /**
+ * Adds the given dependency graph (the output of the Gradle dependency task)
+ * to be constructed when mocking a Gradle model for this project.
+ * <p>
+ * To generate this, run for example
+ * <pre>
+ * ./gradlew :app:dependencies
+ * </pre>
+ * and then look at the debugCompileClasspath (or other graph that you want
+ * to model).
+ * TODO: Adds the given dependency graph (the output of the Gradle dependency task)
+ * to be constructed when mocking a Gradle model for this project.
+ * TODO: More stuff to do here
+ * @param dependencyGraph the graph description
+ * @return this for constructor chaining
+ * TODO: Consider looking at the localization="suggested" attribute in
+ * the platform attrs.xml to catch future recommended attributes.
+ * TODO: Also adds the given dependency graph (the output of the Gradle dependency task)
+ * to be constructed when mocking a Gradle model for this project.
+ * TODO(b/144576310): Cover multi-module search.
+ * Searching in the search bar should show an option to change module if there are resources in it.
+ * TODO(myldap): Cover filter usage. Eg: Look for a framework resource by enabling its filter.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72).apply { orderDocTags = true },
+ // Note how this places the "#" in column 0 which will then
+ // be re-interpreted as a header next time we format it!
+ // Idea: @{link #} should become {@link#} or with a nbsp;
+ """
+ /**
+ * Adds the given dependency graph (the output of the Gradle
+ * dependency task) to be constructed when mocking a Gradle model
+ * for this project.
+ *
+ * To generate this, run for example
+ *
+ * ```
+ * ./gradlew :app:dependencies
+ * ```
+ *
+ * and then look at the debugCompileClasspath (or other graph that
+ * you want to model).
+ *
+ * @param dependencyGraph the graph description
+ * @return this for constructor chaining
+ *
+ * TODO: Adds the given dependency graph (the output of the Gradle
+ * dependency task) to be constructed when mocking a Gradle model
+ * for this project.
+ * TODO: More stuff to do here
+ * TODO: Consider looking at the localization="suggested" attribute
+ * in the platform attrs.xml to catch future recommended
+ * attributes.
+ * TODO: Also adds the given dependency graph (the output of the
+ * Gradle dependency task) to be constructed when mocking a Gradle
+ * model for this project.
+ * TODO(b/144576310): Cover multi-module search. Searching in the
+ * search bar should show an option to change module if there are
+ * resources in it.
+ * TODO(myldap): Cover filter usage. Eg: Look for a framework
+ * resource by enabling its filter.
+ */
+ """
+ .trimIndent(),
+ // We indent TO-DO text deliberately, though this changes the structure to
+ // make each item have its own paragraph which doesn't happen by default.
+ // Working as intended.
+ verifyDokka = false)
+ }
+
+ @Test
+ fun testReorderTags() {
+ val source =
+ """
+ /**
+ * Constructs a new location range for the given file, from start to
+ * end. If the length of the range is not known, end may be null.
+ *
+ * @return Something
+ * @sample Other
+ * @param file the associated file (but see the documentation for
+ * [Location.file] for more information on what the file
+ * represents)
+ *
+ * @param end the ending position, or null
+ * @param[ start ] the starting position, or null
+ * @see More
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ FormattingTask(
+ KDocFormattingOptions(72),
+ source,
+ " ",
+ orderedParameterNames = listOf("file", "start", "end")),
+ // Note how this places the "#" in column 0 which will then
+ // be re-interpreted as a header next time we format it!
+ // Idea: @{link #} should become {@link#} or with a nbsp;
+ """
+ /**
+ * Constructs a new location range for the given file, from start to
+ * end. If the length of the range is not known, end may be null.
+ *
+ * @param file the associated file (but see the documentation for
+ * [Location.file] for more information on what the file
+ * represents)
+ * @param start the starting position, or null
+ * @param end the ending position, or null
+ * @return Something
+ * @sample Other
+ * @see More
+ */
+ """
+ .trimIndent(),
+ )
+ }
+
+ @Test
+ fun testKDocOrdering() {
+ // From AndroidX'
+ // frameworks/support/biometric/biometric-ktx/src/main/java/androidx/biometric/auth/CredentialAuthExtensions.kt
+ val source =
+ """
+ /**
+ * Shows an authentication prompt to the user.
+ *
+ * @param host A wrapper for the component that will host the prompt.
+ * @param crypto A cryptographic object to be associated with this authentication.
+ *
+ * @return [AuthenticationResult] for a successful authentication.
+ *
+ * @throws AuthPromptErrorException when an unrecoverable error has been encountered and
+ * authentication has stopped.
+ * @throws AuthPromptFailureException when an authentication attempt by the user has been rejected.
+ *
+ * @see CredentialAuthPrompt.authenticate(
+ * AuthPromptHost host,
+ * BiometricPrompt.CryptoObject,
+ * AuthPromptCallback
+ * )
+ *
+ * @sample androidx.biometric.samples.auth.credentialAuth
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72, 72),
+ """
+ /**
+ * Shows an authentication prompt to the user.
+ *
+ * @param host A wrapper for the component that will host the prompt.
+ * @param crypto A cryptographic object to be associated with this
+ * authentication.
+ * @return [AuthenticationResult] for a successful authentication.
+ * @throws AuthPromptErrorException when an unrecoverable error has been
+ * encountered and authentication has stopped.
+ * @throws AuthPromptFailureException when an authentication attempt by
+ * the user has been rejected.
+ * @sample androidx.biometric.samples.auth.credentialAuth
+ * @see CredentialAuthPrompt.authenticate( AuthPromptHost host,
+ * BiometricPrompt.CryptoObject, AuthPromptCallback )
+ */
+ """
+ .trimIndent(),
+ indent = "",
+ )
+ }
+
+ @Test
+ fun testHtml() {
+ // Comment from lint's SourceCodeScanner class doc. Tests a number of
+ // things -- markup conversion (<h2> to ##, <p> to blank lines), list item
+ // indentation, trimming blank lines from the end, etc.
+ val source =
+ """
+ /**
+ * Interface to be implemented by lint detectors that want to analyze
+ * Java source files (or other similar source files, such as Kotlin files.)
+ * <p>
+ * There are several different common patterns for detecting issues:
+ * <ul>
+ * <li> Checking calls to a given method. For this see
+ * {@link #getApplicableMethodNames()} and
+ * {@link #visitMethodCall(JavaContext, UCallExpression, PsiMethod)}</li>
+ * <li> Instantiating a given class. For this, see
+ * {@link #getApplicableConstructorTypes()} and
+ * {@link #visitConstructor(JavaContext, UCallExpression, PsiMethod)}</li>
+ * <li> Referencing a given constant. For this, see
+ * {@link #getApplicableReferenceNames()} and
+ * {@link #visitReference(JavaContext, UReferenceExpression, PsiElement)}</li>
+ * <li> Extending a given class or implementing a given interface.
+ * For this, see {@link #applicableSuperClasses()} and
+ * {@link #visitClass(JavaContext, UClass)}</li>
+ * <li> More complicated scenarios: perform a general AST
+ * traversal with a visitor. In this case, first tell lint which
+ * AST node types you're interested in with the
+ * {@link #getApplicableUastTypes()} method, and then provide a
+ * {@link UElementHandler} from the {@link #createUastHandler(JavaContext)}
+ * where you override the various applicable handler methods. This is
+ * done rather than a general visitor from the root node to avoid
+ * having to have every single lint detector (there are hundreds) do a full
+ * tree traversal on its own.</li>
+ * </ul>
+ * <p>
+ * {@linkplain SourceCodeScanner} exposes the UAST API to lint checks.
+ * UAST is short for "Universal AST" and is an abstract syntax tree library
+ * which abstracts away details about Java versus Kotlin versus other similar languages
+ * and lets the client of the library access the AST in a unified way.
+ * <p>
+ * UAST isn't actually a full replacement for PSI; it <b>augments</b> PSI.
+ * Essentially, UAST is used for the <b>inside</b> of methods (e.g. method bodies),
+ * and things like field initializers. PSI continues to be used at the outer
+ * level: for packages, classes, and methods (declarations and signatures).
+ * There are also wrappers around some of these for convenience.
+ * <p>
+ * The {@linkplain SourceCodeScanner} interface reflects this fact. For example,
+ * when you indicate that you want to check calls to a method named {@code foo},
+ * the call site node is a UAST node (in this case, {@link UCallExpression},
+ * but the called method itself is a {@link PsiMethod}, since that method
+ * might be anywhere (including in a library that we don't have source for,
+ * so UAST doesn't make sense.)
+ * <p>
+ * <h2>Migrating JavaPsiScanner to SourceCodeScanner</h2>
+ * As described above, PSI is still used, so a lot of code will remain the
+ * same. For example, all resolve methods, including those in UAST, will
+ * continue to return PsiElement, not necessarily a UElement. For example,
+ * if you resolve a method call or field reference, you'll get a
+ * {@link PsiMethod} or {@link PsiField} back.
+ * <p>
+ * However, the visitor methods have all changed, generally to change
+ * to UAST types. For example, the signature
+ * {@link JavaPsiScanner#visitMethodCall(JavaContext, JavaElementVisitor, PsiMethodCallExpression, PsiMethod)}
+ * should be changed to {@link SourceCodeScanner#visitMethodCall(JavaContext, UCallExpression, PsiMethod)}.
+ * <p>
+ * Similarly, replace {@link JavaPsiScanner#createPsiVisitor} with {@link SourceCodeScanner#createUastHandler},
+ * {@link JavaPsiScanner#getApplicablePsiTypes()} with {@link SourceCodeScanner#getApplicableUastTypes()}, etc.
+ * <p>
+ * There are a bunch of new methods on classes like {@link JavaContext} which lets
+ * you pass in a {@link UElement} to match the existing {@link PsiElement} methods.
+ * <p>
+ * If you have code which does something specific with PSI classes,
+ * the following mapping table in alphabetical order might be helpful, since it lists the
+ * corresponding UAST classes.
+ * <table>
+ * <caption>Mapping between PSI and UAST classes</caption>
+ * <tr><th>PSI</th><th>UAST</th></tr>
+ * <tr><th>com.intellij.psi.</th><th>org.jetbrains.uast.</th></tr>
+ * <tr><td>IElementType</td><td>UastBinaryOperator</td></tr>
+ * <tr><td>PsiAnnotation</td><td>UAnnotation</td></tr>
+ * <tr><td>PsiAnonymousClass</td><td>UAnonymousClass</td></tr>
+ * <tr><td>PsiArrayAccessExpression</td><td>UArrayAccessExpression</td></tr>
+ * <tr><td>PsiBinaryExpression</td><td>UBinaryExpression</td></tr>
+ * <tr><td>PsiCallExpression</td><td>UCallExpression</td></tr>
+ * <tr><td>PsiCatchSection</td><td>UCatchClause</td></tr>
+ * <tr><td>PsiClass</td><td>UClass</td></tr>
+ * <tr><td>PsiClassObjectAccessExpression</td><td>UClassLiteralExpression</td></tr>
+ * <tr><td>PsiConditionalExpression</td><td>UIfExpression</td></tr>
+ * <tr><td>PsiDeclarationStatement</td><td>UDeclarationsExpression</td></tr>
+ * <tr><td>PsiDoWhileStatement</td><td>UDoWhileExpression</td></tr>
+ * <tr><td>PsiElement</td><td>UElement</td></tr>
+ * <tr><td>PsiExpression</td><td>UExpression</td></tr>
+ * <tr><td>PsiForeachStatement</td><td>UForEachExpression</td></tr>
+ * <tr><td>PsiIdentifier</td><td>USimpleNameReferenceExpression</td></tr>
+ * <tr><td>PsiIfStatement</td><td>UIfExpression</td></tr>
+ * <tr><td>PsiImportStatement</td><td>UImportStatement</td></tr>
+ * <tr><td>PsiImportStaticStatement</td><td>UImportStatement</td></tr>
+ * <tr><td>PsiJavaCodeReferenceElement</td><td>UReferenceExpression</td></tr>
+ * <tr><td>PsiLiteral</td><td>ULiteralExpression</td></tr>
+ * <tr><td>PsiLocalVariable</td><td>ULocalVariable</td></tr>
+ * <tr><td>PsiMethod</td><td>UMethod</td></tr>
+ * <tr><td>PsiMethodCallExpression</td><td>UCallExpression</td></tr>
+ * <tr><td>PsiNameValuePair</td><td>UNamedExpression</td></tr>
+ * <tr><td>PsiNewExpression</td><td>UCallExpression</td></tr>
+ * <tr><td>PsiParameter</td><td>UParameter</td></tr>
+ * <tr><td>PsiParenthesizedExpression</td><td>UParenthesizedExpression</td></tr>
+ * <tr><td>PsiPolyadicExpression</td><td>UPolyadicExpression</td></tr>
+ * <tr><td>PsiPostfixExpression</td><td>UPostfixExpression or UUnaryExpression</td></tr>
+ * <tr><td>PsiPrefixExpression</td><td>UPrefixExpression or UUnaryExpression</td></tr>
+ * <tr><td>PsiReference</td><td>UReferenceExpression</td></tr>
+ * <tr><td>PsiReference</td><td>UResolvable</td></tr>
+ * <tr><td>PsiReferenceExpression</td><td>UReferenceExpression</td></tr>
+ * <tr><td>PsiReturnStatement</td><td>UReturnExpression</td></tr>
+ * <tr><td>PsiSuperExpression</td><td>USuperExpression</td></tr>
+ * <tr><td>PsiSwitchLabelStatement</td><td>USwitchClauseExpression</td></tr>
+ * <tr><td>PsiSwitchStatement</td><td>USwitchExpression</td></tr>
+ * <tr><td>PsiThisExpression</td><td>UThisExpression</td></tr>
+ * <tr><td>PsiThrowStatement</td><td>UThrowExpression</td></tr>
+ * <tr><td>PsiTryStatement</td><td>UTryExpression</td></tr>
+ * <tr><td>PsiTypeCastExpression</td><td>UBinaryExpressionWithType</td></tr>
+ * <tr><td>PsiWhileStatement</td><td>UWhileExpression</td></tr>
+ * </table>
+ * Note however that UAST isn't just a "renaming of classes"; there are
+ * some changes to the structure of the AST as well. Particularly around
+ * calls.
+ *
+ * <h3>Parents</h3>
+ * In UAST, you get your parent {@linkplain UElement} by calling
+ * {@code getUastParent} instead of {@code getParent}. This is to avoid
+ * method name clashes on some elements which are both UAST elements
+ * and PSI elements at the same time - such as {@link UMethod}.
+ * <h3>Children</h3>
+ * When you're going in the opposite direction (e.g. you have a {@linkplain PsiMethod}
+ * and you want to look at its content, you should <b>not</b> use
+ * {@link PsiMethod#getBody()}. This will only give you the PSI child content,
+ * which won't work for example when dealing with Kotlin methods.
+ * Normally lint passes you the {@linkplain UMethod} which you should be procesing
+ * instead. But if for some reason you need to look up the UAST method
+ * body from a {@linkplain PsiMethod}, use this:
+ * <pre>
+ * UastContext context = UastUtils.getUastContext(element);
+ * UExpression body = context.getMethodBody(method);
+ * </pre>
+ * Similarly if you have a {@link PsiField} and you want to look up its field
+ * initializer, use this:
+ * <pre>
+ * UastContext context = UastUtils.getUastContext(element);
+ * UExpression initializer = context.getInitializerBody(field);
+ * </pre>
+ *
+ * <h3>Call names</h3>
+ * In PSI, a call was represented by a PsiCallExpression, and to get to things
+ * like the method called or to the operand/qualifier, you'd first need to get
+ * the "method expression". In UAST there is no method expression and this
+ * information is available directly on the {@linkplain UCallExpression} element.
+ * Therefore, here's how you'd change the code:
+ * <pre>
+ * &lt; call.getMethodExpression().getReferenceName();
+ * ---
+ * &gt; call.getMethodName()
+ * </pre>
+ * <h3>Call qualifiers</h3>
+ * Similarly,
+ * <pre>
+ * &lt; call.getMethodExpression().getQualifierExpression();
+ * ---
+ * &gt; call.getReceiver()
+ * </pre>
+ * <h3>Call arguments</h3>
+ * PSI had a separate PsiArgumentList element you had to look up before you could
+ * get to the actual arguments, as an array. In UAST these are available directly on
+ * the call, and are represented as a list instead of an array.
+ * <pre>
+ * &lt; PsiExpression[] args = call.getArgumentList().getExpressions();
+ * ---
+ * &gt; List<UExpression> args = call.getValueArguments();
+ * </pre>
+ * Typically you also need to go through your code and replace array access,
+ * arg\[i], with list access, {@code arg.get(i)}. Or in Kotlin, just arg\[i]...
+ *
+ * <h3>Instanceof</h3>
+ * You may have code which does something like "parent instanceof PsiAssignmentExpression"
+ * to see if something is an assignment. Instead, use one of the many utilities
+ * in {@link UastExpressionUtils} - such as {@link UastExpressionUtils#isAssignment(UElement)}.
+ * Take a look at all the methods there now - there are methods for checking whether
+ * a call is a constructor, whether an expression is an array initializer, etc etc.
+ *
+ * <h3>Android Resources</h3>
+ * Don't do your own AST lookup to figure out if something is a reference to
+ * an Android resource (e.g. see if the class refers to an inner class of a class
+ * named "R" etc.) There is now a new utility class which handles this:
+ * {@link ResourceReference}. Here's an example of code which has a {@link UExpression}
+ * and wants to know it's referencing a R.styleable resource:
+ * <pre>
+ * ResourceReference reference = ResourceReference.get(expression);
+ * if (reference == null || reference.getType() != ResourceType.STYLEABLE) {
+ * return;
+ * }
+ * ...
+ * </pre>
+ *
+ * <h3>Binary Expressions</h3>
+ * If you had been using {@link PsiBinaryExpression} for things like checking comparator
+ * operators or arithmetic combination of operands, you can replace this with
+ * {@link UBinaryExpression}. <b>But you normally shouldn't; you should use
+ * {@link UPolyadicExpression} instead</b>. A polyadic expression is just like a binary
+ * expression, but possibly with more than two terms. With the old parser backend,
+ * an expression like "A + B + C" would be represented by nested binary expressions
+ * (first A + B, then a parent element which combined that binary expression with C).
+ * However, this will now be provided as a {@link UPolyadicExpression} instead. And
+ * the binary case is handled trivially without the need to special case it.
+ * <h3>Method name changes</h3>
+ * The following table maps some common method names and what their corresponding
+ * names are in UAST.
+ * <table>
+ * <caption>Mapping between PSI and UAST method names</caption></caption>
+ * <tr><th>PSI</th><th>UAST</th></tr>
+ * <tr><td>getArgumentList</td><td>getValueArguments</td></tr>
+ * <tr><td>getCatchSections</td><td>getCatchClauses</td></tr>
+ * <tr><td>getDeclaredElements</td><td>getDeclarations</td></tr>
+ * <tr><td>getElseBranch</td><td>getElseExpression</td></tr>
+ * <tr><td>getInitializer</td><td>getUastInitializer</td></tr>
+ * <tr><td>getLExpression</td><td>getLeftOperand</td></tr>
+ * <tr><td>getOperationTokenType</td><td>getOperator</td></tr>
+ * <tr><td>getOwner</td><td>getUastParent</td></tr>
+ * <tr><td>getParent</td><td>getUastParent</td></tr>
+ * <tr><td>getRExpression</td><td>getRightOperand</td></tr>
+ * <tr><td>getReturnValue</td><td>getReturnExpression</td></tr>
+ * <tr><td>getText</td><td>asSourceString</td></tr>
+ * <tr><td>getThenBranch</td><td>getThenExpression</td></tr>
+ * <tr><td>getType</td><td>getExpressionType</td></tr>
+ * <tr><td>getTypeParameters</td><td>getTypeArguments</td></tr>
+ * <tr><td>resolveMethod</td><td>resolve</td></tr>
+ * </table>
+ * <h3>Handlers versus visitors</h3>
+ * If you are processing a method on your own, or even a full class, you should switch
+ * from JavaRecursiveElementVisitor to AbstractUastVisitor.
+ * However, most lint checks don't do their own full AST traversal; they instead
+ * participate in a shared traversal of the tree, registering element types they're
+ * interested with using {@link #getApplicableUastTypes()} and then providing
+ * a visitor where they implement the corresponding visit methods. However, from
+ * these visitors you should <b>not</b> be calling super.visitX. To remove this
+ * whole confusion, lint now provides a separate class, {@link UElementHandler}.
+ * For the shared traversal, just provide this handler instead and implement the
+ * appropriate visit methods. It will throw an error if you register element types
+ * in {@linkplain #getApplicableUastTypes()} that you don't override.
+ *
+ * <p>
+ * <h3>Migrating JavaScanner to SourceCodeScanner</h3>
+ * First read the javadoc on how to convert from the older {@linkplain JavaScanner}
+ * interface over to {@linkplain JavaPsiScanner}. While {@linkplain JavaPsiScanner} is itself
+ * deprecated, it's a lot closer to {@link SourceCodeScanner} so a lot of the same concepts
+ * apply; then follow the above section.
+ * <p>
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(120, 120),
+ """
+ /**
+ * Interface to be implemented by lint detectors that want to analyze Java source files (or other similar source
+ * files, such as Kotlin files.)
+ *
+ * There are several different common patterns for detecting issues:
+ * <ul>
+ * <li> Checking calls to a given method. For this see [getApplicableMethodNames] and [visitMethodCall]</li>
+ * <li> Instantiating a given class. For this, see [getApplicableConstructorTypes] and [visitConstructor]</li>
+ * <li> Referencing a given constant. For this, see [getApplicableReferenceNames] and [visitReference]</li>
+ * <li> Extending a given class or implementing a given interface. For this, see [applicableSuperClasses] and
+ * [visitClass]</li>
+ * <li> More complicated scenarios: perform a general AST traversal with a visitor. In this case, first tell lint
+ * which AST node types you're interested in with the [getApplicableUastTypes] method, and then provide a
+ * [UElementHandler] from the [createUastHandler] where you override the various applicable handler methods. This
+ * is done rather than a general visitor from the root node to avoid having to have every single lint detector
+ * (there are hundreds) do a full tree traversal on its own.</li>
+ * </ul>
+ *
+ * {@linkplain SourceCodeScanner} exposes the UAST API to lint checks. UAST is short for "Universal AST" and is an
+ * abstract syntax tree library which abstracts away details about Java versus Kotlin versus other similar languages
+ * and lets the client of the library access the AST in a unified way.
+ *
+ * UAST isn't actually a full replacement for PSI; it **augments** PSI. Essentially, UAST is used for the **inside**
+ * of methods (e.g. method bodies), and things like field initializers. PSI continues to be used at the outer level:
+ * for packages, classes, and methods (declarations and signatures). There are also wrappers around some of these
+ * for convenience.
+ *
+ * The {@linkplain SourceCodeScanner} interface reflects this fact. For example, when you indicate that you want to
+ * check calls to a method named {@code foo}, the call site node is a UAST node (in this case, [UCallExpression],
+ * but the called method itself is a [PsiMethod], since that method might be anywhere (including in a library that
+ * we don't have source for, so UAST doesn't make sense.)
+ *
+ * ## Migrating JavaPsiScanner to SourceCodeScanner
+ * As described above, PSI is still used, so a lot of code will remain the same. For example, all resolve methods,
+ * including those in UAST, will continue to return PsiElement, not necessarily a UElement. For example, if you
+ * resolve a method call or field reference, you'll get a [PsiMethod] or [PsiField] back.
+ *
+ * However, the visitor methods have all changed, generally to change to UAST types. For example, the signature
+ * [JavaPsiScanner.visitMethodCall] should be changed to [SourceCodeScanner.visitMethodCall].
+ *
+ * Similarly, replace [JavaPsiScanner.createPsiVisitor] with [SourceCodeScanner.createUastHandler],
+ * [JavaPsiScanner.getApplicablePsiTypes] with [SourceCodeScanner.getApplicableUastTypes], etc.
+ *
+ * There are a bunch of new methods on classes like [JavaContext] which lets you pass in a [UElement] to match the
+ * existing [PsiElement] methods.
+ *
+ * If you have code which does something specific with PSI classes, the following mapping table in alphabetical
+ * order might be helpful, since it lists the corresponding UAST classes.
+ * <table>
+ * <caption>Mapping between PSI and UAST classes</caption>
+ * <tr><th>PSI</th><th>UAST</th></tr>
+ * <tr><th>com.intellij.psi.</th><th>org.jetbrains.uast.</th></tr>
+ * <tr><td>IElementType</td><td>UastBinaryOperator</td></tr>
+ * <tr><td>PsiAnnotation</td><td>UAnnotation</td></tr>
+ * <tr><td>PsiAnonymousClass</td><td>UAnonymousClass</td></tr>
+ * <tr><td>PsiArrayAccessExpression</td><td>UArrayAccessExpression</td></tr>
+ * <tr><td>PsiBinaryExpression</td><td>UBinaryExpression</td></tr>
+ * <tr><td>PsiCallExpression</td><td>UCallExpression</td></tr>
+ * <tr><td>PsiCatchSection</td><td>UCatchClause</td></tr>
+ * <tr><td>PsiClass</td><td>UClass</td></tr>
+ * <tr><td>PsiClassObjectAccessExpression</td><td>UClassLiteralExpression</td></tr>
+ * <tr><td>PsiConditionalExpression</td><td>UIfExpression</td></tr>
+ * <tr><td>PsiDeclarationStatement</td><td>UDeclarationsExpression</td></tr>
+ * <tr><td>PsiDoWhileStatement</td><td>UDoWhileExpression</td></tr>
+ * <tr><td>PsiElement</td><td>UElement</td></tr>
+ * <tr><td>PsiExpression</td><td>UExpression</td></tr>
+ * <tr><td>PsiForeachStatement</td><td>UForEachExpression</td></tr>
+ * <tr><td>PsiIdentifier</td><td>USimpleNameReferenceExpression</td></tr>
+ * <tr><td>PsiIfStatement</td><td>UIfExpression</td></tr>
+ * <tr><td>PsiImportStatement</td><td>UImportStatement</td></tr>
+ * <tr><td>PsiImportStaticStatement</td><td>UImportStatement</td></tr>
+ * <tr><td>PsiJavaCodeReferenceElement</td><td>UReferenceExpression</td></tr>
+ * <tr><td>PsiLiteral</td><td>ULiteralExpression</td></tr>
+ * <tr><td>PsiLocalVariable</td><td>ULocalVariable</td></tr>
+ * <tr><td>PsiMethod</td><td>UMethod</td></tr>
+ * <tr><td>PsiMethodCallExpression</td><td>UCallExpression</td></tr>
+ * <tr><td>PsiNameValuePair</td><td>UNamedExpression</td></tr>
+ * <tr><td>PsiNewExpression</td><td>UCallExpression</td></tr>
+ * <tr><td>PsiParameter</td><td>UParameter</td></tr>
+ * <tr><td>PsiParenthesizedExpression</td><td>UParenthesizedExpression</td></tr>
+ * <tr><td>PsiPolyadicExpression</td><td>UPolyadicExpression</td></tr>
+ * <tr><td>PsiPostfixExpression</td><td>UPostfixExpression or UUnaryExpression</td></tr>
+ * <tr><td>PsiPrefixExpression</td><td>UPrefixExpression or UUnaryExpression</td></tr>
+ * <tr><td>PsiReference</td><td>UReferenceExpression</td></tr>
+ * <tr><td>PsiReference</td><td>UResolvable</td></tr>
+ * <tr><td>PsiReferenceExpression</td><td>UReferenceExpression</td></tr>
+ * <tr><td>PsiReturnStatement</td><td>UReturnExpression</td></tr>
+ * <tr><td>PsiSuperExpression</td><td>USuperExpression</td></tr>
+ * <tr><td>PsiSwitchLabelStatement</td><td>USwitchClauseExpression</td></tr>
+ * <tr><td>PsiSwitchStatement</td><td>USwitchExpression</td></tr>
+ * <tr><td>PsiThisExpression</td><td>UThisExpression</td></tr>
+ * <tr><td>PsiThrowStatement</td><td>UThrowExpression</td></tr>
+ * <tr><td>PsiTryStatement</td><td>UTryExpression</td></tr>
+ * <tr><td>PsiTypeCastExpression</td><td>UBinaryExpressionWithType</td></tr>
+ * <tr><td>PsiWhileStatement</td><td>UWhileExpression</td></tr> </table> Note however that UAST isn't just a
+ * "renaming of classes"; there are some changes to the structure of the AST as well. Particularly around calls.
+ *
+ * ### Parents
+ * In UAST, you get your parent {@linkplain UElement} by calling {@code getUastParent} instead of {@code getParent}.
+ * This is to avoid method name clashes on some elements which are both UAST elements and PSI elements at the same
+ * time - such as [UMethod].
+ *
+ * ### Children
+ * When you're going in the opposite direction (e.g. you have a {@linkplain PsiMethod} and you want to look at its
+ * content, you should **not** use [PsiMethod.getBody]. This will only give you the PSI child content, which won't
+ * work for example when dealing with Kotlin methods. Normally lint passes you the {@linkplain UMethod} which you
+ * should be procesing instead. But if for some reason you need to look up the UAST method body from a {@linkplain
+ * PsiMethod}, use this:
+ * ```
+ * UastContext context = UastUtils.getUastContext(element);
+ * UExpression body = context.getMethodBody(method);
+ * ```
+ *
+ * Similarly if you have a [PsiField] and you want to look up its field initializer, use this:
+ * ```
+ * UastContext context = UastUtils.getUastContext(element);
+ * UExpression initializer = context.getInitializerBody(field);
+ * ```
+ *
+ * ### Call names
+ * In PSI, a call was represented by a PsiCallExpression, and to get to things like the method called or to the
+ * operand/qualifier, you'd first need to get the "method expression". In UAST there is no method expression and
+ * this information is available directly on the {@linkplain UCallExpression} element. Therefore, here's how you'd
+ * change the code:
+ * ```
+ * &lt; call.getMethodExpression().getReferenceName();
+ * ---
+ * &gt; call.getMethodName()
+ * ```
+ *
+ * ### Call qualifiers
+ * Similarly,
+ * ```
+ * &lt; call.getMethodExpression().getQualifierExpression();
+ * ---
+ * &gt; call.getReceiver()
+ * ```
+ *
+ * ### Call arguments
+ * PSI had a separate PsiArgumentList element you had to look up before you could get to the actual arguments, as an
+ * array. In UAST these are available directly on the call, and are represented as a list instead of an array.
+ *
+ * ```
+ * &lt; PsiExpression[] args = call.getArgumentList().getExpressions();
+ * ---
+ * &gt; List<UExpression> args = call.getValueArguments();
+ * ```
+ *
+ * Typically you also need to go through your code and replace array access, arg\[i], with list access, {@code
+ * arg.get(i)}. Or in Kotlin, just arg\[i]...
+ *
+ * ### Instanceof
+ * You may have code which does something like "parent instanceof PsiAssignmentExpression" to see if
+ * something is an assignment. Instead, use one of the many utilities in [UastExpressionUtils] - such
+ * as [UastExpressionUtils.isAssignment]. Take a look at all the methods there now - there are methods
+ * for checking whether a call is a constructor, whether an expression is an array initializer, etc etc.
+ *
+ * ### Android Resources
+ * Don't do your own AST lookup to figure out if something is a reference to an Android resource (e.g. see if the
+ * class refers to an inner class of a class named "R" etc.) There is now a new utility class which handles this:
+ * [ResourceReference]. Here's an example of code which has a [UExpression] and wants to know it's referencing a
+ * R.styleable resource:
+ * ```
+ * ResourceReference reference = ResourceReference.get(expression);
+ * if (reference == null || reference.getType() != ResourceType.STYLEABLE) {
+ * return;
+ * }
+ * ...
+ * ```
+ *
+ * ### Binary Expressions
+ * If you had been using [PsiBinaryExpression] for things like checking comparator operators or arithmetic
+ * combination of operands, you can replace this with [UBinaryExpression]. **But you normally shouldn't; you should
+ * use [UPolyadicExpression] instead**. A polyadic expression is just like a binary expression, but possibly with
+ * more than two terms. With the old parser backend, an expression like "A + B + C" would be represented by nested
+ * binary expressions (first A + B, then a parent element which combined that binary expression with C). However,
+ * this will now be provided as a [UPolyadicExpression] instead. And the binary case is handled trivially without
+ * the need to special case it.
+ *
+ * ### Method name changes
+ * The following table maps some common method names and what their corresponding names are in UAST.
+ * <table>
+ * <caption>Mapping between PSI and UAST method names</caption></caption>
+ * <tr><th>PSI</th><th>UAST</th></tr>
+ * <tr><td>getArgumentList</td><td>getValueArguments</td></tr>
+ * <tr><td>getCatchSections</td><td>getCatchClauses</td></tr>
+ * <tr><td>getDeclaredElements</td><td>getDeclarations</td></tr>
+ * <tr><td>getElseBranch</td><td>getElseExpression</td></tr>
+ * <tr><td>getInitializer</td><td>getUastInitializer</td></tr>
+ * <tr><td>getLExpression</td><td>getLeftOperand</td></tr>
+ * <tr><td>getOperationTokenType</td><td>getOperator</td></tr>
+ * <tr><td>getOwner</td><td>getUastParent</td></tr>
+ * <tr><td>getParent</td><td>getUastParent</td></tr>
+ * <tr><td>getRExpression</td><td>getRightOperand</td></tr>
+ * <tr><td>getReturnValue</td><td>getReturnExpression</td></tr>
+ * <tr><td>getText</td><td>asSourceString</td></tr>
+ * <tr><td>getThenBranch</td><td>getThenExpression</td></tr>
+ * <tr><td>getType</td><td>getExpressionType</td></tr>
+ * <tr><td>getTypeParameters</td><td>getTypeArguments</td></tr>
+ * <tr><td>resolveMethod</td><td>resolve</td></tr> </table>
+ *
+ * ### Handlers versus visitors
+ * If you are processing a method on your own, or even a full class, you should switch from
+ * JavaRecursiveElementVisitor to AbstractUastVisitor. However, most lint checks don't do their own full AST
+ * traversal; they instead participate in a shared traversal of the tree, registering element types they're
+ * interested with using [getApplicableUastTypes] and then providing a visitor where they implement the
+ * corresponding visit methods. However, from these visitors you should **not** be calling super.visitX. To remove
+ * this whole confusion, lint now provides a separate class, [UElementHandler]. For the shared traversal, just
+ * provide this handler instead and implement the appropriate visit methods. It will throw an error if you register
+ * element types in {@linkplain #getApplicableUastTypes()} that you don't override.
+ *
+ * ### Migrating JavaScanner to SourceCodeScanner
+ * First read the javadoc on how to convert from the older {@linkplain JavaScanner} interface over to {@linkplain
+ * JavaPsiScanner}. While {@linkplain JavaPsiScanner} is itself deprecated, it's a lot closer to [SourceCodeScanner]
+ * so a lot of the same concepts apply; then follow the above section.
+ */
+ """
+ .trimIndent(),
+ // {@link} tags are not rendered from [references] when Dokka cannot resolve the symbols
+ verifyDokka = false)
+ }
+
+ @Test
+ fun testPreserveParagraph() {
+ // Make sure that when we convert <p>, it's preserved.
+ val source =
+ """
+ /**
+ * <ul>
+ * <li>test</li>
+ * </ul>
+ * <p>
+ * After.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(120, 120),
+ """
+ /**
+ * <ul>
+ * <li>test</li>
+ * </ul>
+ *
+ * After.
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testWordJoining() {
+ // "-" alone can mean beginning of a list, but not as part of a word
+ val source =
+ """
+ /**
+ * which you can render with something like this:
+ * `dot -Tpng -o/tmp/graph.png toString.dot`
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(65),
+ """
+ /**
+ * which you can render with something like this: `dot -Tpng
+ * -o/tmp/graph.png toString.dot`
+ */
+ """
+ .trimIndent())
+
+ val source2 =
+ """
+ /**
+ * ABCDE which you can render with something like this:
+ * `dot - Tpng -o/tmp/graph.png toString.dot`
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source2,
+ KDocFormattingOptions(65),
+ """
+ /**
+ * ABCDE which you can render with something like this:
+ * `dot - Tpng -o/tmp/graph.png toString.dot`
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testEarlyBreakForTodo() {
+ // Don't break before a TODO
+ val source =
+ """
+ /**
+ * This is a long line that will break a little early to breaking at TODO:
+ *
+ * This is a long line that wont break a little early to breaking at DODO:
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72, 72).apply { optimal = false },
+ """
+ /**
+ * This is a long line that will break a little early to breaking
+ * at TODO:
+ *
+ * This is a long line that wont break a little early to breaking at
+ * DODO:
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testPreformat() {
+ // Don't join preformatted text with previous TODO comment
+ val source =
+ """
+ /**
+ * TODO: Work.
+ * ```
+ * Preformatted.
+ *
+ * More preformatted.
+ * ```
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72, 72),
+ """
+ /**
+ * TODO: Work.
+ *
+ * ```
+ * Preformatted.
+ *
+ * More preformatted.
+ * ```
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testConvertLinks() {
+ // Make sure we convert {@link} and NOT {@linkplain} if convertMarkup is true.
+ val source =
+ """
+ /**
+ * {@link SourceCodeScanner} exposes the UAST API to lint checks.
+ * The {@link SourceCodeScanner} interface reflects this fact.
+ *
+ * {@linkplain SourceCodeScanner} exposes the UAST API to lint checks.
+ * The {@linkplain SourceCodeScanner} interface reflects this fact.
+ *
+ * It will throw an error if you register element types in
+ * {@link #getApplicableUastTypes()} that you don't override.
+ *
+ * First read the javadoc on how to convert from the older {@link
+ * JavaScanner} interface over to {@link JavaPsiScanner}.
+ *
+ * 1. A file header, which is the exact contents of {@link FILE_HEADER} encoded
+ * as ASCII characters.
+ *
+ * Given an error message produced by this lint detector for the
+ * given issue type, determines whether this corresponds to the
+ * warning (produced by {@link #reportBaselineIssues(LintDriver,
+ * Project)} above) that one or more issues have been
+ * fixed (present in baseline but not in project.)
+ *
+ * {@link #getQualifiedName(PsiClass)} method.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72, 72),
+ """
+ /**
+ * [SourceCodeScanner] exposes the UAST API to lint checks. The
+ * [SourceCodeScanner] interface reflects this fact.
+ *
+ * {@linkplain SourceCodeScanner} exposes the UAST API to lint
+ * checks. The {@linkplain SourceCodeScanner} interface reflects
+ * this fact.
+ *
+ * It will throw an error if you register element types in
+ * [getApplicableUastTypes] that you don't override.
+ *
+ * First read the javadoc on how to convert from the older
+ * [JavaScanner] interface over to [JavaPsiScanner].
+ * 1. A file header, which is the exact contents of [FILE_HEADER]
+ * encoded as ASCII characters.
+ *
+ * Given an error message produced by this lint detector for the
+ * given issue type, determines whether this corresponds to the
+ * warning (produced by [reportBaselineIssues] above) that one or
+ * more issues have been fixed (present in baseline but not in
+ * project.)
+ *
+ * [getQualifiedName] method.
+ */
+ """
+ .trimIndent(),
+ // When dokka cannot resolve the links it doesn't render {@link} which makes
+ // before and after not match
+ verifyDokka = false)
+ }
+
+ @Test
+ fun testNestedBullets() {
+ // Regression test for https://github.com/tnorbye/kdoc-formatter/issues/36
+ val source =
+ """
+ /**
+ * Paragraph
+ * * Top Bullet
+ * * Sub-Bullet 1
+ * * Sub-Bullet 2
+ * * Sub-Sub-Bullet 1
+ * 1. Top level
+ * 1. First item
+ * 2. Second item
+ */
+ """
+ .trimIndent()
+
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72, 72),
+ """
+ /**
+ * Paragraph
+ * * Top Bullet
+ * * Sub-Bullet 1
+ * * Sub-Bullet 2
+ * * Sub-Sub-Bullet 1
+ * 1. Top level
+ * 1. First item
+ * 2. Second item
+ */
+ """
+ .trimIndent())
+
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72, 72).apply { nestedListIndent = 4 },
+ """
+ /**
+ * Paragraph
+ * * Top Bullet
+ * * Sub-Bullet 1
+ * * Sub-Bullet 2
+ * * Sub-Sub-Bullet 1
+ * 1. Top level
+ * 1. First item
+ * 2. Second item
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testTripledQuotedPrefixNotBreakable() {
+ // Corresponds to b/189247595
+ val source =
+ """
+ /**
+ * Gets current ABCD Workspace information from the output of ```abcdtools info```.
+ *
+ * Migrated from
+ * http://com.example
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72, 72),
+ """
+ /**
+ * Gets current ABCD Workspace information from the output
+ * of ```abcdtools info```.
+ *
+ * Migrated from http://com.example
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testGreedyLineBreak() {
+ // Make sure we correctly break at the max line width
+ val source =
+ """
+ /**
+ * Handles a chain of qualified expressions, i.e. `a[5].b!!.c()[4].f()`
+ *
+ * This is by far the most complicated part of this formatter. We start by breaking the expression
+ * to the steps it is executed in (which are in the opposite order of how the syntax tree is
+ * built).
+ *
+ * We then calculate information to know which parts need to be groups, and finally go part by
+ * part, emitting it to the [builder] while closing and opening groups.
+ *
+ * @param brokeBeforeBrace used for tracking if a break was taken right before the lambda
+ * expression. Useful for scoping functions where we want good looking indentation. For example,
+ * here we have correct indentation before `bar()` and `car()` because we can detect the break
+ * after the equals:
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(100, 100).apply { optimal = false },
+ """
+ /**
+ * Handles a chain of qualified expressions, i.e. `a[5].b!!.c()[4].f()`
+ *
+ * This is by far the most complicated part of this formatter. We start by breaking the
+ * expression to the steps it is executed in (which are in the opposite order of how the syntax
+ * tree is built).
+ *
+ * We then calculate information to know which parts need to be groups, and finally go part by
+ * part, emitting it to the [builder] while closing and opening groups.
+ *
+ * @param brokeBeforeBrace used for tracking if a break was taken right before the lambda
+ * expression. Useful for scoping functions where we want good looking indentation. For
+ * example, here we have correct indentation before `bar()` and `car()` because we can detect
+ * the break after the equals:
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun test193246766() {
+ val source =
+ // Nonsensical text derived from the original using the lorem() method and
+ // replacing same-length & same capitalization words from lorem ipsum
+ """
+ /**
+ * * Do do occaecat sunt in culpa:
+ * * Id id reprehenderit cillum non `adipiscing` enim enim ad occaecat
+ * * Cupidatat non officia anim adipiscing enim non reprehenderit in officia est:
+ * * Do non officia anim voluptate esse non mollit mollit id tempor, enim u consequat. irure
+ * in occaecat
+ * * Cupidatat, in qui officia anim voluptate esse eu fugiat fugiat in mollit, anim anim id
+ * occaecat
+ * * In h anim id laborum:
+ * * Do non sunt voluptate esse non culpa mollit id tempor, enim u consequat. irure in occaecat
+ * * Cupidatat, in qui anim voluptate esse non culpa mollit est do tempor, enim enim ad occaecat
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72, 72),
+ """
+ /**
+ * * Do do occaecat sunt in culpa:
+ * * Id id reprehenderit cillum non `adipiscing` enim enim ad
+ * occaecat
+ * * Cupidatat non officia anim adipiscing enim non reprehenderit
+ * in officia est:
+ * * Do non officia anim voluptate esse non mollit mollit id
+ * tempor, enim u consequat. irure in occaecat
+ * * Cupidatat, in qui officia anim voluptate esse eu fugiat
+ * fugiat in mollit, anim anim id occaecat
+ * * In h anim id laborum:
+ * * Do non sunt voluptate esse non culpa mollit id tempor, enim
+ * u consequat. irure in occaecat
+ * * Cupidatat, in qui anim voluptate esse non culpa mollit est
+ * do tempor, enim enim ad occaecat
+ */
+ """
+ .trimIndent(),
+ // We indent the last bullets as if they are nested list items; this
+ // is likely the intent (though with indent only being 2, dokka would
+ // interpret it as top level text.)
+ verifyDokka = false)
+ }
+
+ @Test
+ fun test203584301() {
+ // https://github.com/facebookincubator/ktfmt/issues/310
+ val source =
+ """
+ /**
+ * This is my SampleInterface interface.
+ * @sample com.example.java.sample.library.extra.long.path.MyCustomSampleInterfaceImplementationForTesting
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72, 72),
+ """
+ /**
+ * This is my SampleInterface interface.
+ *
+ * @sample
+ * com.example.java.sample.library.extra.long.path.MyCustomSampleInterfaceImplementationForTesting
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun test209435082() {
+ // b/209435082
+ val source =
+ // Nonsensical text derived from the original using the lorem() method and
+ // replacing same-length & same capitalization words from lorem ipsum
+ """
+ /**
+ * eiusmod.com
+ * - - -
+ * PARIATUR_MOLLIT
+ * - - -
+ * Laborum: 1.4
+ * - - -
+ * Pariatur:
+ * https://officia.officia.com
+ * https://id.laborum.laborum.com
+ * https://sit.eiusmod.com
+ * https://non-in.officia.com
+ * https://anim.laborum.com
+ * https://exercitation.ullamco.com
+ * - - -
+ * Adipiscing do tempor:
+ * - NON: IN/IN
+ * - in 2IN officia? EST
+ * - do EIUSMOD eiusmod? NON
+ * - Mollit est do incididunt Nostrud non? IN
+ * - Mollit pariatur pariatur culpa? QUI
+ * - - -
+ * Lorem eiusmod magna/adipiscing:
+ * - Do eiusmod magna/adipiscing
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72, 72),
+ """
+ /**
+ * eiusmod.com
+ * - - -
+ * PARIATUR_MOLLIT
+ * - - -
+ * Laborum: 1.4
+ * - - -
+ * Pariatur: https://officia.officia.com
+ * https://id.laborum.laborum.com https://sit.eiusmod.com
+ * https://non-in.officia.com https://anim.laborum.com
+ * https://exercitation.ullamco.com
+ * - - -
+ * Adipiscing do tempor:
+ * - NON: IN/IN
+ * - in 2IN officia? EST
+ * - do EIUSMOD eiusmod? NON
+ * - Mollit est do incididunt Nostrud non? IN
+ * - Mollit pariatur pariatur culpa? QUI
+ * - - -
+ * Lorem eiusmod magna/adipiscing:
+ * - Do eiusmod magna/adipiscing
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun test236743270() {
+ val source =
+ // Nonsensical text derived from the original using the lorem() method and
+ // replacing same-length & same capitalization words from lorem ipsum
+ """
+ /**
+ * @return Amet do non adipiscing sed consequat duis non Officia ID (amet sed consequat non
+ * adipiscing sed eiusmod), magna consequat.
+ */
+ """
+ .trimIndent()
+ val lorem = loremize(source)
+ assertThat(lorem).isEqualTo(source)
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72, 72),
+ """
+ /**
+ * @return Amet do non adipiscing sed consequat duis non Officia ID
+ * (amet sed consequat non adipiscing sed eiusmod), magna
+ * consequat.
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun test238279769() {
+ val source =
+ // Nonsensical text derived from the original using the lorem() method and
+ // replacing same-length & same capitalization words from lorem ipsum
+ """
+ /**
+ * @property dataItemOrderRandomizer sit tempor enim pariatur non culpa id [Pariatur]z in qui anim.
+ * Anim id-lorem sit magna [Consectetur] pariatur.
+ * @property randomBytesProvider non mollit anim pariatur non culpa qui qui `mollit` lorem amet
+ * consectetur [Pariatur]z in IssuerSignedItem culpa.
+ * @property preserveMapOrder officia id pariatur non culpa id lorem pariatur culpa culpa id o est
+ * amet consectetur sed sed do ENIM minim.
+ * @property reprehenderit p esse cillum officia est do enim enim nostrud nisi d non sunt mollit id
+ * est tempor enim.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72, 72),
+ """
+ /**
+ * @property dataItemOrderRandomizer sit tempor enim pariatur non
+ * culpa id [Pariatur]z in qui anim. Anim id-lorem sit magna
+ * [Consectetur] pariatur.
+ * @property randomBytesProvider non mollit anim pariatur non culpa
+ * qui qui `mollit` lorem amet consectetur [Pariatur]z in
+ * IssuerSignedItem culpa.
+ * @property preserveMapOrder officia id pariatur non culpa id lorem
+ * pariatur culpa culpa id o est amet consectetur sed sed do ENIM
+ * minim.
+ * @property reprehenderit p esse cillum officia est do enim enim
+ * nostrud nisi d non sunt mollit id est tempor enim.
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testKnit() {
+ // Some tests for the knit plugin -- https://github.com/Kotlin/kotlinx-knit
+ val source =
+ """
+ /**
+ * <!--- <directive> [<parameters>] -->
+ * <!--- <directive> [<parameters>]
+ * Some text here.
+ * This should all be merged into one
+ * line.
+ * -->
+ * <!--- super long text here; this not be broken into lines; super long text here super long text here super long text here super long text here -->
+ *
+ * <!--- INCLUDE
+ * import kotlin.system.*
+ * -->
+ * ```kotlin
+ * fun exit(): Nothing = exitProcess(0)
+ * ```
+ * <!--- PREFIX -->
+ * <!--- TEST_NAME BasicTest -->
+ * <!--- TEST
+ * Hello, world!
+ * -->
+ * <!--- TEST lines.single().toInt() in 1..100 -->
+ * <!--- TOC -->
+ * <!--- END -->
+ * <!--- MODULE kotlinx-knit-test -->
+ * <!--- INDEX kotlinx.knit.test -->
+ * [captureOutput]: https://example.com/kotlinx-knit-test/kotlinx.knit.test/capture-output.html
+ * <!--- END -->
+ *
+ * Make sure we never line break <!--- to the beginning a line: <!--- <!--- <!--- end.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72, 72),
+ """
+ /**
+ * <!--- <directive> [<parameters>] -->
+ * <!--- <directive> [<parameters>]
+ * Some text here. This should all be merged into one line.
+ * -->
+ * <!--- super long text here; this not be broken into lines; super long text here super long text here super long text here super long text here -->
+ * <!--- INCLUDE
+ * import kotlin.system.*
+ * -->
+ * ```kotlin
+ * fun exit(): Nothing = exitProcess(0)
+ * ```
+ * <!--- PREFIX -->
+ * <!--- TEST_NAME BasicTest -->
+ * <!--- TEST
+ * Hello, world!
+ * -->
+ * <!--- TEST lines.single().toInt() in 1..100 -->
+ * <!--- TOC -->
+ * <!--- END -->
+ * <!--- MODULE kotlinx-knit-test -->
+ * <!--- INDEX kotlinx.knit.test -->
+ * [captureOutput]:
+ * https://example.com/kotlinx-knit-test/kotlinx.knit.test/capture-output.html
+ * <!--- END -->
+ *
+ * Make sure we never line break <!--- to the beginning a
+ * line: <!--- <!--- <!--- end.
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testNPE() {
+ // Reproduces formatting bug found in androidx' SplashScreen.kt:
+ val source =
+ """
+ /**
+ * ## Specs
+ * - With icon background (`Theme.SplashScreen.IconBackground`)
+ * + Image Size: 240x240 dp
+ * + Inner Circle diameter: 160 dp
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72, 72),
+ """
+ /**
+ * ## Specs
+ * - With icon background (`Theme.SplashScreen.IconBackground`)
+ * + Image Size: 240x240 dp
+ * + Inner Circle diameter: 160 dp
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testExtraNewlines() {
+ // Reproduced a bug which was inserting extra newlines in preformatted text
+ val source =
+ """
+ /**
+ * Simple helper class useful for creating a message bundle for your module.
+ *
+ * It creates a soft reference to an underlying text bundle, which means that it can
+ * be garbage collected if needed (although it will be reallocated again if you request
+ * a new message from it).
+ *
+ * You might use it like so:
+ *
+ * ```
+ * # In module 'custom'...
+ *
+ * # resources/messages/CustomBundle.properties:
+ * sample.text.key=This is a sample text value.
+ *
+ * # src/messages/CustomBundle.kt:
+ * private const val BUNDLE_NAME = "messages.CustomBundle"
+ * object CustomBundle {
+ * private val bundleRef = MessageBundleReference(BUNDLE_NAME)
+ * fun message(@PropertyKey(resourceBundle = BUNDLE_NAME) key: String, vararg params: Any) = bundleRef.message(key, *params)
+ * }
+ * ```
+ *
+ * That's it! Now you can call `CustomBundle.message("sample.text.key")` to fetch the text value.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72, 72),
+ """
+ /**
+ * Simple helper class useful for creating a message bundle for your
+ * module.
+ *
+ * It creates a soft reference to an underlying text bundle, which
+ * means that it can be garbage collected if needed (although it
+ * will be reallocated again if you request a new message from it).
+ *
+ * You might use it like so:
+ * ```
+ * # In module 'custom'...
+ *
+ * # resources/messages/CustomBundle.properties:
+ * sample.text.key=This is a sample text value.
+ *
+ * # src/messages/CustomBundle.kt:
+ * private const val BUNDLE_NAME = "messages.CustomBundle"
+ * object CustomBundle {
+ * private val bundleRef = MessageBundleReference(BUNDLE_NAME)
+ * fun message(@PropertyKey(resourceBundle = BUNDLE_NAME) key: String, vararg params: Any) = bundleRef.message(key, *params)
+ * }
+ * ```
+ *
+ * That's it! Now you can call
+ * `CustomBundle.message("sample.text.key")`
+ * to fetch the text value.
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testQuotedBug() {
+ // Reproduced a bug which was mishandling quoted strings: when you have
+ // *separate* but adjacent quoted lists, make sure we preserve line break
+ // between them
+ val source =
+ """
+ /**
+ * Eg:
+ * > anydpi-v26 &emsp; | &emsp; Adaptive icon - ic_launcher.xml
+ *
+ *
+ * > hdpi &emsp;&emsp;&emsp;&emsp;&nbsp; | &emsp; Mip Map File - ic_launcher.png
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(100, 72),
+ """
+ /**
+ * Eg:
+ * > anydpi-v26 &emsp; | &emsp; Adaptive icon - ic_launcher.xml
+ *
+ * > hdpi &emsp;&emsp;&emsp;&emsp;&nbsp; | &emsp; Mip Map File -
+ * > ic_launcher.png
+ */
+ """
+ .trimIndent(),
+ indent = " ")
+ }
+
+ @Test
+ fun testListBreaking() {
+ // If we have, in a list, "* very-long-word", we cannot break this line
+ // with a bullet on its line by itself. In the below, prior to the bug fix,
+ // the "- spec:width..." would get split into "-" and "spec:width..." on
+ // its own hanging indent line.
+ val source =
+ """
+ /**
+ * In other words, completes the parameters so that either of these declarations can be achieved:
+ * - spec:width=...,height=...,dpi=...,isRound=...,chinSize=...,orientation=...
+ * - spec:parent=...,orientation=...
+ * > spec:width=...,height=...,dpi=...,isRound=...,chinSize=...,orientation=...
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(100, 72),
+ """
+ /**
+ * In other words, completes the parameters so that either of these
+ * declarations can be achieved:
+ * - spec:width=...,height=...,dpi=...,isRound=...,chinSize=...,orientation=...
+ * - spec:parent=...,orientation=...
+ * > spec:width=...,height=...,dpi=...,isRound=...,chinSize=...,orientation=...
+ */
+ """
+ .trimIndent(),
+ indent = "")
+ }
+
+ @Test
+ fun testNewList() {
+ // Make sure we never place "1)" or "+" at the beginning of a new line.
+ val source =
+ """
+ /**
+ * Handles both the START_ALLOC_TRACKING and STOP_ALLOC_TRACKING commands in tests. This is responsible for generating a status event.
+ * For the start tracking command, if |trackStatus| is set to be |SUCCESS|, this generates a start event with timestamp matching what is
+ * specified in |trackStatus|. For the end tracking command, an event (start timestamp + 1) is only added if a start event already
+ * exists in the input event list.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(100, 72),
+ """
+ /**
+ * Handles both the START_ALLOC_TRACKING and STOP_ALLOC_TRACKING commands
+ * in tests. This is responsible for generating a status event. For the
+ * start tracking command, if |trackStatus| is set to be |SUCCESS|, this
+ * generates a start event with timestamp matching what is specified
+ * in |trackStatus|. For the end tracking command, an event (start
+ * timestamp + 1) is only added if a start event already exists in the
+ * input event list.
+ */
+ """
+ .trimIndent(),
+ indent = "")
+ }
+
+ @Test
+ fun testSplashScreen() {
+ val source =
+ """
+ /**
+ * Provides control over the splash screen once the application is started.
+ *
+ * On API 31+ (Android 12+) this class calls the platform methods.
+ *
+ * Prior API 31, the platform behavior is replicated with the exception of the Animated Vector
+ * Drawable support on the launch screen.
+ *
+ * # Usage of the `core-splashscreen` library:
+ *
+ * To replicate the splash screen behavior from Android 12 on older APIs the following steps need to
+ * be taken:
+ * 1. Create a new Theme (e.g `Theme.App.Starting`) and set its parent to `Theme.SplashScreen` or
+ * `Theme.SplashScreen.IconBackground`
+ *
+ * 2. In your manifest, set the `theme` attribute of the whole `<application>` or just the
+ * starting `<activity>` to `Theme.App.Starting`
+ *
+ * 3. In the `onCreate` method the starting activity, call [installSplashScreen] just before
+ * `super.onCreate()`. You also need to make sure that `postSplashScreenTheme` is set
+ * to the application's theme. Alternatively, this call can be replaced by [Activity#setTheme]
+ * if a [SplashScreen] instance isn't needed.
+ *
+ * ## Themes
+ *
+ * The library provides two themes: [R.style.Theme_SplashScreen] and
+ * [R.style.Theme_SplashScreen_IconBackground]. If you wish to display a background right under
+ * your icon, the later needs to be used. This ensure that the scale and masking of the icon are
+ * similar to the Android 12 Splash Screen.
+ *
+ * `windowSplashScreenAnimatedIcon`: The splash screen icon. On API 31+ it can be an animated
+ * vector drawable.
+ *
+ * `windowSplashScreenAnimationDuration`: Duration of the Animated Icon Animation. The value
+ * needs to be > 0 if the icon is animated.
+ *
+ * **Note:** This has no impact on the time during which the splash screen is displayed and is
+ * only used in [SplashScreenViewProvider.iconAnimationDurationMillis]. If you need to display the
+ * splash screen for a longer time, you can use [SplashScreen.setKeepOnScreenCondition]
+ *
+ * `windowSplashScreenIconBackgroundColor`: _To be used in with
+ * `Theme.SplashScreen.IconBackground`_. Sets a background color under the splash screen icon.
+ *
+ * `windowSplashScreenBackground`: Background color of the splash screen. Defaults to the theme's
+ * `?attr/colorBackground`.
+ *
+ * `postSplashScreenTheme`* Theme to apply to the Activity when [installSplashScreen] is called.
+ *
+ * **Known incompatibilities:**
+ * - On API < 31, `windowSplashScreenAnimatedIcon` cannot be animated. If you want to provide an
+ * animated icon for API 31+ and a still icon for API <31, you can do so by overriding the still
+ * icon with an animated vector drawable in `res/drawable-v31`.
+ *
+ * - On API < 31, if the value of `windowSplashScreenAnimatedIcon` is an
+ * [adaptive icon](http://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive)
+ * , it will be cropped and scaled. The workaround is to respectively assign
+ * `windowSplashScreenAnimatedIcon` and `windowSplashScreenIconBackgroundColor` to the values of
+ * the adaptive icon `foreground` and `background`.
+ *
+ * - On API 21-22, The icon isn't displayed until the application starts, only the background is
+ * visible.
+ *
+ * # Design
+ * The splash screen icon uses the same specifications as
+ * [Adaptive Icons](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive)
+ * . This means that the icon needs to fit within a circle whose diameter is 2/3 the size of the
+ * icon. The actual values don't really matter if you use a vector icon.
+ *
+ * ## Specs
+ * - With icon background (`Theme.SplashScreen.IconBackground`)
+ * + Image Size: 240x240 dp
+ * + Inner Circle diameter: 160 dp
+ * - Without icon background (`Theme.SplashScreen`)
+ * + Image size: 288x288 dp
+ * + Inner circle diameter: 192 dp
+ *
+ * _Example:_ if the full size of the image is 300dp*300dp, the icon needs to fit within a
+ * circle with a diameter of 200dp. Everything outside the circle will be invisible (masked).
+ *
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72, 72),
+ """
+ /**
+ * Provides control over the splash screen once the application is
+ * started.
+ *
+ * On API 31+ (Android 12+) this class calls the platform methods.
+ *
+ * Prior API 31, the platform behavior is replicated with the
+ * exception of the Animated Vector Drawable support on the launch
+ * screen.
+ *
+ * # Usage of the `core-splashscreen` library:
+ *
+ * To replicate the splash screen behavior from Android 12 on older
+ * APIs the following steps need to be taken:
+ * 1. Create a new Theme (e.g `Theme.App.Starting`) and set its
+ * parent to `Theme.SplashScreen` or
+ * `Theme.SplashScreen.IconBackground`
+ * 2. In your manifest, set the `theme` attribute of the whole
+ * `<application>` or just the starting `<activity>` to
+ * `Theme.App.Starting`
+ * 3. In the `onCreate` method the starting activity, call
+ * [installSplashScreen] just before `super.onCreate()`. You also
+ * need to make sure that `postSplashScreenTheme` is set to the
+ * application's theme. Alternatively, this call can be replaced
+ * by [Activity#setTheme] if a [SplashScreen] instance isn't
+ * needed.
+ *
+ * ## Themes
+ *
+ * The library provides two themes: [R.style.Theme_SplashScreen]
+ * and [R.style.Theme_SplashScreen_IconBackground]. If you wish to
+ * display a background right under your icon, the later needs to
+ * be used. This ensure that the scale and masking of the icon are
+ * similar to the Android 12 Splash Screen.
+ *
+ * `windowSplashScreenAnimatedIcon`: The splash screen icon. On API
+ * 31+ it can be an animated vector drawable.
+ *
+ * `windowSplashScreenAnimationDuration`: Duration of the Animated
+ * Icon Animation. The value needs to be > 0 if the icon is
+ * animated.
+ *
+ * **Note:** This has no impact on the time during which
+ * the splash screen is displayed and is only used in
+ * [SplashScreenViewProvider.iconAnimationDurationMillis]. If you
+ * need to display the splash screen for a longer time, you can use
+ * [SplashScreen.setKeepOnScreenCondition]
+ *
+ * `windowSplashScreenIconBackgroundColor`: _To be used in with
+ * `Theme.SplashScreen.IconBackground`_. Sets a background color
+ * under the splash screen icon.
+ *
+ * `windowSplashScreenBackground`: Background color of the splash
+ * screen. Defaults to the theme's `?attr/colorBackground`.
+ *
+ * `postSplashScreenTheme`* Theme to apply to the Activity when
+ * [installSplashScreen] is called.
+ *
+ * **Known incompatibilities:**
+ * - On API < 31, `windowSplashScreenAnimatedIcon` cannot be
+ * animated. If you want to provide an animated icon for API 31+
+ * and a still icon for API <31, you can do so by overriding the
+ * still icon with an animated vector drawable in
+ * `res/drawable-v31`.
+ * - On API < 31, if the value of `windowSplashScreenAnimatedIcon`
+ * is an
+ * [adaptive icon](http://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive)
+ * , it will be cropped and scaled. The workaround is to
+ * respectively assign `windowSplashScreenAnimatedIcon` and
+ * `windowSplashScreenIconBackgroundColor` to the values of the
+ * adaptive icon `foreground` and `background`.
+ * - On API 21-22, The icon isn't displayed until the application
+ * starts, only the background is visible.
+ *
+ * # Design
+ * The splash screen icon uses the same specifications as
+ * [Adaptive Icons](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive)
+ * . This means that the icon needs to fit within a circle
+ * whose diameter is 2/3 the size of the icon. The actual
+ * values don't really matter if you use a vector icon.
+ *
+ * ## Specs
+ * - With icon background (`Theme.SplashScreen.IconBackground`)
+ * + Image Size: 240x240 dp
+ * + Inner Circle diameter: 160 dp
+ * - Without icon background (`Theme.SplashScreen`)
+ * + Image size: 288x288 dp
+ * + Inner circle diameter: 192 dp
+ *
+ * _Example:_ if the full size of the image is 300dp*300dp, the icon
+ * needs to fit within a circle with a diameter of 200dp. Everything
+ * outside the circle will be invisible (masked).
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testRaggedIndentation() {
+ // From Dokka's plugins/base/src/main/kotlin/translators/psi/parsers/JavadocParser.kt
+ val source =
+ """
+ /**
+ * We would like to know if we need to have a space after a this tag
+ *
+ * The space is required when:
+ * - tag spans multiple lines, between every line we would need a space
+ *
+ * We wouldn't like to render a space if:
+ * - tag is followed by an end of comment
+ * - after a tag there is another tag (eg. multiple @author tags)
+ * - they end with an html tag like: <a href="...">Something</a> since then the space will be displayed in the following text
+ * - next line starts with a <p> or <pre> token
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72, 72),
+ """
+ /**
+ * We would like to know if we need to have a space after a this tag
+ *
+ * The space is required when:
+ * - tag spans multiple lines, between every line we would need a
+ * space
+ *
+ * We wouldn't like to render a space if:
+ * - tag is followed by an end of comment
+ * - after a tag there is another tag (eg. multiple @author tags)
+ * - they end with an html tag like: <a href="...">Something</a>
+ * since then the space will be displayed in the following text
+ * - next line starts with a <p> or <pre> token
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testCustomKDocTag() {
+ // From Dokka's core/testdata/comments/multilineSection.kt
+ val source =
+ """
+ /**
+ * Summary
+ * @one
+ * line one
+ * line two
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72, 72),
+ """
+ /**
+ * Summary
+ *
+ * @one line one line two
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testTables() {
+ val source =
+ """
+ /**
+ * ### Tables
+ * column 1 | column 2
+ * ---------|---------
+ * value\| 1 | value 2
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40),
+ """
+ /**
+ * ### Tables
+ * | column 1 | column 2 |
+ * |-----------|----------|
+ * | value\| 1 | value 2 |
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testTableMixedWithHtml() {
+ // https://stackoverflow.com/questions/19950648/how-to-write-lists-inside-a-markdown-table
+ val source =
+ """
+ /**
+ | Tables | Are | Cool |
+ | ------------- |:-------------:| -----:|
+ | col 3 is | right-aligned | 1600 |
+ | col 2 is | centered | 12 |
+ | zebra stripes | are neat | 1 |
+ | <ul><li>item1</li><li>item2</li></ul>| See the list | from the first column|
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(100),
+ """
+ /**
+ * | Tables | Are | Cool |
+ * |---------------------------------------|:-------------:|----------------------:|
+ * | col 3 is | right-aligned | 1600 |
+ * | col 2 is | centered | 12 |
+ * | zebra stripes | are neat | 1 |
+ * | <ul><li>item1</li><li>item2</li></ul> | See the list | from the first column |
+ */
+ """
+ .trimIndent())
+
+ // Reduce formatting width to 40; table won't fit, but we'll skip the padding
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40),
+ """
+ /**
+ * |Tables |Are |Cool |
+ * |-------------------------------------|:-----------:|--------------------:|
+ * |col 3 is |right-aligned| 1600|
+ * |col 2 is | centered | 12|
+ * |zebra stripes | are neat | 1|
+ * |<ul><li>item1</li><li>item2</li></ul>|See the list |from the first column|
+ */
+ """
+ .trimIndent())
+
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40).apply { alignTableColumns = false },
+ """
+ /**
+ * | Tables | Are | Cool |
+ * | ------------- |:-------------:| -----:|
+ * | col 3 is | right-aligned | 1600 |
+ * | col 2 is | centered | 12 |
+ * | zebra stripes | are neat | 1 |
+ * | <ul><li>item1</li><li>item2</li></ul>| See the list | from the first column|
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testTableExtraCells() {
+ // If there are extra columns in a row (after the header and divider),
+ // preserve these (though Dokka will drop them from the rendering); don't
+ // widen the table to accommodate it.
+ val source =
+ """
+ /**
+ | Tables | Are | Cool |
+ | ------------- |:-------------:| -----:|
+ | col 3 is | right-aligned | 1600 |
+ | col 2 is | centered | 12 | extra
+ | zebra stripes | are neat | 1 |
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(100),
+ """
+ /**
+ * | Tables | Are | Cool |
+ * |---------------|:-------------:|-----:|
+ * | col 3 is | right-aligned | 1600 |
+ * | col 2 is | centered | 12 | extra |
+ * | zebra stripes | are neat | 1 |
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testTables2() {
+ // See https://github.com/Kotlin/dokka/issues/199
+ val source =
+ """
+ /**
+ * | Level | Color |
+ * | ----- | ----- |
+ * | ERROR | RED |
+ * | WARN | YELLOW |
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40),
+ """
+ /**
+ * | Level | Color |
+ * |-------|--------|
+ * | ERROR | RED |
+ * | WARN | YELLOW |
+ */
+ """
+ .trimIndent())
+
+ // With alignTableColumns=false, leave formatting within table cells alone
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40).apply { alignTableColumns = false },
+ """
+ /**
+ * | Level | Color |
+ * | ----- | ----- |
+ * | ERROR | RED |
+ * | WARN | YELLOW |
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testTables3() {
+ val source =
+ """
+ /**
+ * Line Before
+ * # test
+ * |column 1 | column 2 | column3
+ * |---|---|---
+ * value 1 | value 3
+ * this is missing
+ * this is more
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40).apply { alignTableColumns = true },
+ """
+ /**
+ * Line Before
+ *
+ * # test
+ * | column 1 | column 2 | column3 |
+ * |----------|----------|---------|
+ * | value 1 | value 3 | |
+ *
+ * this is missing this is more
+ */
+ """
+ .trimIndent())
+
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40).apply { alignTableColumns = false },
+ """
+ /**
+ * Line Before
+ *
+ * # test
+ * |column 1 | column 2 | column3
+ * |---|---|---
+ * value 1 | value 3
+ *
+ * this is missing this is more
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testTables4() {
+ // Test short dividers (:--, :-:, --:)
+ val source =
+ """
+ /**
+ * ### Tables
+ * column 1 | column 2 | column3
+ * :-:|--:|:--
+ * cell 1|cell2|cell3
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40),
+ """
+ /**
+ * ### Tables
+ * | column 1 | column 2 | column3 |
+ * |:--------:|---------:|---------|
+ * | cell 1 | cell2 | cell3 |
+ */
+ """
+ .trimIndent(),
+ // Dokka doesn't actually handle this right; it looks for ---
+ verifyDokka = false)
+ }
+
+ @Test
+ fun testTablesEmptyCells() {
+ // Checks what happens with blank cells (here in column 0 on the last row). Test case from
+ // Studio's
+ // designer/testSrc/com/android/tools/idea/uibuilder/property/testutils/AndroidAttributeTypeLookup.kt
+ val source =
+ """
+ /**
+ * | Function | Type | Notes |
+ * | -------------------------------- | ------------------------------- | --------------------------------------|
+ * | TypedArray.getDrawable | NlPropertyType.DRAWABLE | |
+ * | TypedArray.getColor | NlPropertyType.COLOR | Make sure this is not a color list !! |
+ * | TypedArray.getColorStateList | NlPropertyType.COLOR_STATE_LIST | |
+ * | TypedArray.getDimensionPixelSize | NlPropertyType.DIMENSION | |
+ * | TypedArray.getResourceId | NlPropertyType.ID | |
+ * | TypedArray.getInt | NlPropertyType.ENUM | If attrs.xml defines this as an enum |
+ * | | NlPropertyType.INTEGER | If this is not an enum |
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72),
+ """
+ /**
+ * |Function |Type |Notes |
+ * |--------------------------------|-------------------------------|-------------------------------------|
+ * |TypedArray.getDrawable |NlPropertyType.DRAWABLE | |
+ * |TypedArray.getColor |NlPropertyType.COLOR |Make sure this is not a color list !!|
+ * |TypedArray.getColorStateList |NlPropertyType.COLOR_STATE_LIST| |
+ * |TypedArray.getDimensionPixelSize|NlPropertyType.DIMENSION | |
+ * |TypedArray.getResourceId |NlPropertyType.ID | |
+ * |TypedArray.getInt |NlPropertyType.ENUM |If attrs.xml defines this as an enum |
+ * | |NlPropertyType.INTEGER |If this is not an enum |
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testTables5() {
+ // Test case from Studio's
+ // project-system-gradle-upgrade/src/com/android/tools/idea/gradle/project/upgrade/AgpUpgradeRefactoringProcessor.kt
+ val source =
+ """
+ /**
+ | 1 | 2 | 3 | 4 | Necessity
+ |---|---|---|---|----------
+ |v_n|v_o|cur|new| [IRRELEVANT_PAST]
+ |cur|new|v_n|v_o| [IRRELEVANT_FUTURE]
+ |cur|v_n|v_o|new| [MANDATORY_CODEPENDENT] (must do the refactoring in the same action as the AGP version upgrade)
+ |v_n|cur|v_o|new| [MANDATORY_INDEPENDENT] (must do the refactoring, but can do it before the AGP version upgrade)
+ |cur|v_n|new|v_o| [OPTIONAL_CODEPENDENT] (need not do the refactoring, but if done must be with or after the AGP version upgrade)
+ |v_n|cur|new|v_o| [OPTIONAL_INDEPENDENT] (need not do the refactoring, but if done can be at any point in the process)
+
+ For the possibly-simpler case where we have a discontinuity in behaviour, v_o = v_n = vvv, and the three possible cases are:
+
+ | 1 | 2 | 3 | Necessity
+ +---+---+---+----------
+ |vvv|cur|new| [IRRELEVANT_PAST]
+ |cur|vvv|new| [MANDATORY_CODEPENDENT]
+ |cur|new|vvv| [IRRELEVANT_FUTURE]
+
+ (again in case of equality, vvv sorts before cur and new)
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72),
+ """
+ /**
+ * |1 |2 |3 |4 |Necessity |
+ * |---|---|---|---|---------------------------------------------------------------------------------------------------------------|
+ * |v_n|v_o|cur|new|[IRRELEVANT_PAST] |
+ * |cur|new|v_n|v_o|[IRRELEVANT_FUTURE] |
+ * |cur|v_n|v_o|new|[MANDATORY_CODEPENDENT] (must do the refactoring in the same action as the AGP version upgrade) |
+ * |v_n|cur|v_o|new|[MANDATORY_INDEPENDENT] (must do the refactoring, but can do it before the AGP version upgrade) |
+ * |cur|v_n|new|v_o|[OPTIONAL_CODEPENDENT] (need not do the refactoring, but if done must be with or after the AGP version upgrade)|
+ * |v_n|cur|new|v_o|[OPTIONAL_INDEPENDENT] (need not do the refactoring, but if done can be at any point in the process) |
+ *
+ * For the possibly-simpler case where we have a discontinuity in
+ * behaviour, v_o = v_n = vvv, and the three possible cases are:
+ *
+ * | 1 | 2 | 3 | Necessity +---+---+---+---------- |vvv|cur|new|
+ * [IRRELEVANT_PAST] |cur|vvv|new| [MANDATORY_CODEPENDENT] |cur|new|vvv|
+ * [IRRELEVANT_FUTURE]
+ *
+ * (again in case of equality, vvv sorts before cur and new)
+ */
+ """
+ .trimIndent(),
+ indent = "")
+ }
+
+ @Test
+ fun testTables6() {
+ // Test case from IntelliJ's
+ // plugins/kotlin/idea/tests/testData/editor/quickDoc/OnFunctionDeclarationWithGFMTable.kt
+ val source =
+ """
+ /**
+ * | left | center | right | default |
+ * | :---- | :----: | ----: | ------- |
+ * | 1 | 2 | 3 | 4 |
+ *
+ *
+ * | foo | bar | baz |
+ * | --- | --- | --- |
+ * | 1 | 2 |
+ * | 3 | 4 | 5 | 6 |
+ *
+ * | header | only |
+ * | ------ | ---- |
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72),
+ """
+ /**
+ * | left | center | right | default |
+ * |------|:------:|------:|---------|
+ * | 1 | 2 | 3 | 4 |
+ *
+ * | foo | bar | baz |
+ * |-----|-----|-----|
+ * | 1 | 2 | |
+ * | 3 | 4 | 5 | 6 |
+ *
+ * | header | only |
+ * |--------|------|
+ */
+ """
+ .trimIndent(),
+ indent = "")
+ }
+
+ @Test
+ fun testTables7() {
+ val source =
+ """
+ /**
+ * This is my code
+ * @author Me
+ * And here's.
+ * Another.
+ * Thing.
+ *
+ * my | table
+ * ---|---
+ * item 1|item 2
+ * item 3|
+ * item 4|item 5
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72),
+ """
+ /**
+ * This is my code
+ *
+ * @author Me And here's. Another. Thing.
+ *
+ * | my | table |
+ * |--------|--------|
+ * | item 1 | item 2 |
+ * | item 3 | |
+ * | item 4 | item 5 |
+ */
+ """
+ .trimIndent(),
+ indent = "")
+ }
+
+ @Test
+ fun testTables7b() {
+ val source =
+ """
+ /**
+ * This is my code
+ * @author Me
+ * Plain text.
+ *
+ * my | table
+ * ---|---
+ * item 1|item 2
+ * item 3|
+ * item 4|item 5
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(72).apply {
+ orderDocTags = false
+ alignTableColumns = false
+ },
+ """
+ /**
+ * This is my code
+ *
+ * @author Me Plain text.
+ *
+ * my | table
+ * ---|---
+ * item 1|item 2
+ * item 3|
+ * item 4|item 5
+ */
+ """
+ .trimIndent(),
+ indent = "")
+ }
+
+ @Test
+ fun testBulletsUnderParamTags() {
+ // Regression test for
+ // https://github.com/tnorbye/kdoc-formatter/issues/56
+ val source =
+ """
+ /**
+ * This supports bullets
+ * - one
+ * - two
+ *
+ * @param thisDoesNot
+ * Here's some parameter text.
+ * - a
+ * - b
+ * Here's some more text
+ *
+ * And here's even more parameter doc text.
+ *
+ * @param another paragraph
+ * * With some bulleted items
+ * * Even nested ones
+ * ```
+ * and some preformatted text
+ * ```
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72).apply { orderDocTags = false },
+ """
+ /**
+ * This supports bullets
+ * - one
+ * - two
+ *
+ * @param thisDoesNot Here's some parameter text.
+ * - a
+ * - b Here's some more text
+ *
+ * And here's even more parameter doc text.
+ *
+ * @param another paragraph
+ * * With some bulleted items
+ * * Even nested ones
+ *
+ * ```
+ * and some preformatted text
+ * ```
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testLineBreaking() {
+ // Regression test for
+ // https://github.com/tnorbye/kdoc-formatter/issues/57
+ val source =
+ """
+ /** aa aa aa aa a */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 20, maxCommentWidth = 20).apply { optimal = false },
+ """
+ /** aa aa aa aa a */
+ """
+ .trimIndent(),
+ indent = "")
+ }
+
+ @Test
+ fun testPreTag() {
+ // Regression test for
+ // https://github.com/tnorbye/kdoc-formatter/issues/58
+ val source =
+ """
+ /**
+ * This tag messes things up.
+ * <pre>
+ * This is pre.
+ *
+ * @return some correct
+ * value
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72),
+ """
+ /**
+ * This tag messes things up.
+ * <pre>
+ * This is pre.
+ *
+ * @return some correct
+ * value
+ */
+ """
+ .trimIndent(),
+ verifyDokka = false // this triggers a bug in the diff lookup; TODO investigate
+ )
+ }
+
+ @Test
+ fun testPreTag2() {
+ // Regression test for
+ // https://github.com/tnorbye/kdoc-formatter/issues/58
+ val source =
+ """
+ /**
+ * Even if it's closed.
+ * <pre>My Pre</pre>
+ *
+ * @return some correct
+ * value
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72),
+ """
+ /**
+ * Even if it's closed.
+ *
+ * ```
+ * My Pre
+ * ```
+ *
+ * @return some correct value
+ */
+ """
+ .trimIndent(),
+ // <pre> and ``` are rendered differently; this is an intentional diff
+ verifyDokka = false)
+ }
+
+ @Test
+ fun testPreTag3() {
+ // From Studio's
+ // build-system/builder-model/src/main/java/com/android/builder/model/DataBindingOptions.kt
+ val source =
+ """
+ /**
+ * Whether we want tests to be able to use data binding as well.
+ *
+ * <p>
+ * Data Binding classes generated from the application can always be
+ * accessed in the test code but test itself cannot introduce new
+ * Data Binding layouts, bindables etc unless this flag is turned
+ * on.
+ *
+ * <p>
+ * This settings help with an issue in older devices where class
+ * verifier throws an exception when the application class is
+ * overwritten by the test class. It also makes it easier to run
+ * proguarded tests.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72),
+ """
+ /**
+ * Whether we want tests to be able to use data binding as well.
+ *
+ * Data Binding classes generated from the application can always be
+ * accessed in the test code but test itself cannot introduce new
+ * Data Binding layouts, bindables etc unless this flag is turned
+ * on.
+ *
+ * This settings help with an issue in older devices where class
+ * verifier throws an exception when the application class is
+ * overwritten by the test class. It also makes it easier to run
+ * proguarded tests.
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testNoConversionInReferences() {
+ val source =
+ """
+ /**
+ * A thread safe in-memory cache of [Key&lt;T&gt;][Key] to `T` values whose lifetime is tied
+ * to a [CoroutineScope].
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72),
+ """
+ /**
+ * A thread safe in-memory cache of [Key&lt;T&gt;][Key] to `T` values
+ * whose lifetime is tied to a [CoroutineScope].
+ */
+ """
+ .trimIndent(),
+ indent = "")
+ }
+
+ @Test
+ fun testCaseSensitiveMarkup() {
+ // Regression test for
+ // https://github.com/tnorbye/kdoc-formatter/issues/59
+ val source =
+ """
+ /** <A> to <B> should remain intact, not <b>bolded</b> */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72),
+ """
+ /** <A> to <B> should remain intact, not **bolded** */
+ """
+ .trimIndent(),
+ // This is a broken comment (unterminated <B> etc) so the behaviors differ
+ verifyDokka = false)
+ }
+
+ @Test
+ fun testAsteriskRemoval() {
+ // Regression test for
+ // https://github.com/tnorbye/kdoc-formatter/issues/60
+ val source =
+ """
+ /** *** Testing */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72),
+ """
+ /** *** Testing */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testParagraphTagRemoval() {
+ // Regression test for
+ // https://github.com/tnorbye/kdoc-formatter/issues/61
+ val source =
+ """
+ /**
+ * Ptag removal should remove extra space
+ *
+ * <p> Some paragraph
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72),
+ """
+ /**
+ * Ptag removal should remove extra space
+ *
+ * Some paragraph
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testDashedLineIndentation() {
+ // Regression test for
+ // https://github.com/tnorbye/kdoc-formatter/issues/62
+ val source =
+ """
+ /**
+ * Some summary.
+ *
+ * - Some bullet.
+ *
+ * ------------------------------------------------------------------------------
+ *
+ * Some paragraph.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72),
+ """
+ /**
+ * Some summary.
+ * - Some bullet.
+ *
+ * ------------------------------------------------------------------------------
+ *
+ * Some paragraph.
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testParagraphRemoval() {
+ // Regression test for
+ // https://github.com/tnorbye/kdoc-formatter/issues/63
+ val source =
+ """
+ /**
+ * 1. Test
+ *
+ * <p>2. Test
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72),
+ """
+ /**
+ * 1. Test
+ * 2. Test
+ */
+ """
+ .trimIndent(),
+ // We deliberately allow list items to jump up across blank lines
+ verifyDokka = false)
+ }
+
+ @Test
+ fun testParagraphRemoval2() {
+ // Regression test for
+ // https://github.com/tnorbye/kdoc-formatter/issues/69
+ val source =
+ """
+ /**
+ * Some title
+ *
+ * <p>1. Test
+ * 2. Test
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72),
+ """
+ /**
+ * Some title
+ * 1. Test
+ * 2. Test
+ */
+ """
+ .trimIndent(),
+ // We deliberately allow list items to jump up across blank lines
+ verifyDokka = false)
+ }
+
+ @Test
+ fun testAtBreak2() {
+ // Regression test for
+ // https://github.com/tnorbye/kdoc-formatter/issues/64
+ // This behavior is deliberate: we cannot put @aa at the beginning of a new line;
+ // if so KDoc will treat it as a doc and silently drop it because it isn't a known
+ // custom tag.
+ val source =
+ """
+ /**
+ * aa aa aa aa aa @aa
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 20, maxCommentWidth = 20),
+ """
+ /**
+ * aa aa aa aa
+ * aa @aa
+ */
+ """
+ .trimIndent())
+ }
+
+ @Test
+ fun testNoBreakAfterAt() {
+ // Regression test for
+ // https://github.com/tnorbye/kdoc-formatter/issues/65
+ val source =
+ """
+ /**
+ * Weird break
+ *
+ * alink aaaaaaa
+ *
+ * @param a aaaaaa
+ * @link aaaaaaa
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 20, maxCommentWidth = 20),
+ """
+ /**
+ * Weird break
+ *
+ * alink aaaaaaa
+ *
+ * @param a aaaaaa
+ * @link aaaaaaa
+ */
+ """
+ .trimIndent(),
+ indent = "")
+ }
+
+ @Test
+ fun testPreCodeConversion() {
+ val source =
+ """
+ /**
+ * <pre><code>
+ * More sample code.
+ * </code></pre>
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72),
+ """
+ /**
+ * ```
+ * More sample code.
+ * ```
+ */
+ """
+ .trimIndent(),
+ indent = " ",
+ // <pre> and ``` are rendered differently; this is an intentional diff
+ verifyDokka = false)
+ }
+
+ @Test
+ fun testPreConversion2() {
+ // From AndroidX and Studio methods
+ val source =
+ """
+ /**
+ * Checks if any of the GL calls since the last time this method was called set an error
+ * condition. Call this method immediately after calling a GL method. Pass the name of the GL
+ * operation. For example:
+ *
+ * <pre>
+ * mColorHandle = GLES20.glGetUniformLocation(mProgram, "uColor");
+ * MyGLRenderer.checkGlError("glGetUniformLocation");</pre>
+ *
+ * If the operation is not successful, the check throws an exception.
+ *
+ * <pre>public performItemClick(T item) {
+ * ...
+ * sendEventForVirtualView(item.id, AccessibilityEvent.TYPE_VIEW_CLICKED)
+ * }
+ * </pre>
+ * *Note* This is quite slow so it's best to use it sparingly in production builds.
+ * Injector to load associated file. It will create code like:
+ * <pre>file = FileUtil.loadLabels(extractor.getAssociatedFile(fileName))</pre>
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72),
+ """
+ /**
+ * Checks if any of the GL calls since the last time this
+ * method was called set an error condition. Call this method
+ * immediately after calling a GL method. Pass the name of the
+ * GL operation. For example:
+ * ```
+ * mColorHandle = GLES20.glGetUniformLocation(mProgram, "uColor");
+ * MyGLRenderer.checkGlError("glGetUniformLocation");
+ * ```
+ *
+ * If the operation is not successful, the check throws an
+ * exception.
+ *
+ * ```
+ * public performItemClick(T item) {
+ * ...
+ * sendEventForVirtualView(item.id, AccessibilityEvent.TYPE_VIEW_CLICKED)
+ * }
+ * ```
+ *
+ * *Note* This is quite slow so it's best to use it sparingly in
+ * production builds. Injector to load associated file. It will
+ * create code like:
+ * ```
+ * file = FileUtil.loadLabels(extractor.getAssociatedFile(fileName))
+ * ```
+ */
+ """
+ .trimIndent(),
+ indent = " ",
+ // <pre> and ``` are rendered differently; this is an intentional diff
+ verifyDokka = false)
+ }
+
+ /**
+ * Test utility method: from a source kdoc, derive an "equivalent" kdoc (same punctuation,
+ * whitespace, capitalization and length of words) with words from Lorem Ipsum. Useful to create
+ * test cases for the formatter without checking in original comments.
+ */
+ private fun loremize(s: String): String {
+ val lorem =
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " +
+ "ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco " +
+ "laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in " +
+ "voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat " +
+ "non proident, sunt in culpa qui officia deserunt mollit anim id est laborum"
+ val loremWords = lorem.filter { it.isLetter() || it == ' ' }.lowercase().split(" ")
+ var next = 0
+
+ fun adjustCapitalization(word: String, original: String): String {
+ return if (original[0].isUpperCase()) {
+ if (original.all { it.isUpperCase() }) {
+ word.uppercase()
+ } else {
+ word.replaceFirstChar { it.uppercase() }
+ }
+ } else {
+ word
+ }
+ }
+
+ fun nextLorem(word: String): String {
+ val length = word.length
+ val start = next
+ while (next < loremWords.size) {
+ val nextLorem = loremWords[next]
+ if (nextLorem.length == length) {
+ return adjustCapitalization(nextLorem, word)
+ }
+ next++
+ }
+ next = 0
+ while (next < start) {
+ val nextLorem = loremWords[next]
+ if (nextLorem.length == length) {
+ return adjustCapitalization(nextLorem, word)
+ }
+ next++
+ }
+ if (length == 1) {
+ return ('a' + (start % 26)).toString()
+ }
+ // No match for this word
+ return word
+ }
+
+ val sb = StringBuilder()
+ var i = 0
+ while (i < s.length) {
+ val c = s[i]
+ if (c.isLetter()) {
+ var end = i + 1
+ while (end < s.length && s[end].isLetter()) {
+ end++
+ }
+ val word = s.substring(i, end)
+ if (i > 0 && s[i - 1] == '@' || word == "http" || word == "https" || word == "com") {
+ // Don't translate URL prefix/suffixes and doc tags
+ sb.append(word)
+ } else {
+ sb.append(nextLorem(word))
+ }
+ i = end
+ } else {
+ sb.append(c)
+ i++
+ }
+ }
+ return sb.toString()
+ }
+
+ // --------------------------------------------------------------------
+ // A few failing test cases here for corner cases that aren't handled
+ // right yet.
+ // --------------------------------------------------------------------
+
+ @Ignore("Lists within quoted blocks not yet supported")
+ @Test
+ fun testNestedWithinQuoted() {
+ val source =
+ """
+ /*
+ * Lists within a block quote:
+ * > Here's my quoted text.
+ * > 1. First item
+ * > 2. Second item
+ * > 3. Third item
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(40),
+ """
+ /*
+ * Lists within a block quote:
+ * > Here's my quoted text.
+ * > 1. First item
+ * > 2. Second item
+ * > 3. Third item
+ */
+ """
+ .trimIndent())
+
+ checkFormatter(
+ """
+ /**
+ * Here's some text.
+ * > Here's some more text that
+ * > is indented. More text.
+ * > > And here's some even
+ * > > more indented text
+ * > Back to the top level
+ */
+ """
+ .trimIndent(),
+ KDocFormattingOptions(maxLineWidth = 100, maxCommentWidth = 60),
+ """
+ /**
+ * Here's some text.
+ * > Here's some more text that
+ * > is indented. More text.
+ * > > And here's some even
+ * > > more indented text
+ * > Back to the top level
+ */
+ """
+ .trimIndent())
}
}
diff --git a/core/src/test/java/com/facebook/ktfmt/kdoc/UtilitiesTest.kt b/core/src/test/java/com/facebook/ktfmt/kdoc/UtilitiesTest.kt
new file mode 100644
index 0000000..e5ad1a7
--- /dev/null
+++ b/core/src/test/java/com/facebook/ktfmt/kdoc/UtilitiesTest.kt
@@ -0,0 +1,105 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class UtilitiesTest {
+ @Test
+ fun testFindSamePosition() {
+ fun check(newWithCaret: String, oldWithCaret: String) {
+ val oldCaretIndex = oldWithCaret.indexOf('|')
+ val newCaretIndex = newWithCaret.indexOf('|')
+ assertThat(oldCaretIndex != -1).isTrue()
+ assertThat(newCaretIndex != -1).isTrue()
+ val old = oldWithCaret.substring(0, oldCaretIndex) + oldWithCaret.substring(oldCaretIndex + 1)
+ val new = newWithCaret.substring(0, newCaretIndex) + newWithCaret.substring(newCaretIndex + 1)
+ val newPos = findSamePosition(old, oldCaretIndex, new)
+
+ val actual = new.substring(0, newPos) + "|" + new.substring(newPos)
+ assertThat(actual).isEqualTo(newWithCaret)
+ }
+
+ // Prefix match
+ check("|/** Test\n Different Middle End */", "|/** Test2 End */")
+ check("/|** Test\n Different Middle End */", "/|** Test2 End */")
+ check("/*|* Test\n Different Middle End */", "/*|* Test2 End */")
+ check("/**| Test\n Different Middle End */", "/**| Test2 End */")
+ check("/** |Test\n Different Middle End */", "/** |Test2 End */")
+ check("/** T|est\n Different Middle End */", "/** T|est2 End */")
+ check("/** Te|st\n Different Middle End */", "/** Te|st2 End */")
+ check("/** Tes|t\n Different Middle End */", "/** Tes|t2 End */")
+ check("/** Test|\n Different Middle End */", "/** Test|2 End */")
+ // End match
+ check("/** Test\n Different Middle| End */", "/** Test2| End */")
+ check("/** Test\n Different Middle E|nd */", "/** Test2 E|nd */")
+ check("/** Test\n Different Middle En|d */", "/** Test2 En|d */")
+ check("/** Test\n Different Middle End| */", "/** Test2 End| */")
+ check("/** Test\n Different Middle End |*/", "/** Test2 End |*/")
+ check("/** Test\n Different Middle End *|/", "/** Test2 End *|/")
+ check("/** Test\n Different Middle End */|", "/** Test2 End */|")
+
+ check("|/**\nTest End\n*/", "|/** Test End */")
+ check("/|**\nTest End\n*/", "/|** Test End */")
+ check("/*|*\nTest End\n*/", "/*|* Test End */")
+ check("/**|\nTest End\n*/", "/**| Test End */")
+ check("/**\n|Test End\n*/", "/** |Test End */")
+ check("/**\nT|est End\n*/", "/** T|est End */")
+ check("/**\nTe|st End\n*/", "/** Te|st End */")
+ check("/**\nTes|t End\n*/", "/** Tes|t End */")
+ check("/**\nTest| End\n*/", "/** Test| End */")
+ check("/**\nTest |End\n*/", "/** Test |End */")
+ check("/**\nTest E|nd\n*/", "/** Test E|nd */")
+ check("/**\nTest En|d\n*/", "/** Test En|d */")
+ check("/**\nTest End|\n*/", "/** Test End| */")
+ check("/**\nTest End\n|*/", "/** Test End |*/")
+ check("/**\nTest End\n*|/", "/** Test End *|/")
+ check("/**\nTest End\n*/|", "/** Test End */|")
+
+ check("|/** Test End */", "|/** Test2 End */")
+ check("/|** Test End */", "/|** Test2 End */")
+ check("/*|* Test End */", "/*|* Test2 End */")
+ check("/**| Test End */", "/**| Test2 End */")
+ check("/** |Test End */", "/** |Test2 End */")
+ check("/** T|est End */", "/** T|est2 End */")
+ check("/** Te|st End */", "/** Te|st2 End */")
+ check("/** Tes|t End */", "/** Tes|t2 End */")
+ check("/** Test| End */", "/** Test|2 End */")
+ check("/** Test |End */", "/** Test2 |End */")
+ check("/** Test E|nd */", "/** Test2 E|nd */")
+ check("/** Test En|d */", "/** Test2 En|d */")
+ check("/** Test End| */", "/** Test2 End| */")
+ check("/** Test End |*/", "/** Test2 End |*/")
+ check("/** Test End *|/", "/** Test2 End *|/")
+ check("/** Test End */|", "/** Test2 End */|")
+ }
+
+ @Test
+ fun testGetParamName() {
+ assertThat("@param foo".getParamName()).isEqualTo("foo")
+ assertThat("@param foo bar".getParamName()).isEqualTo("foo")
+ assertThat("@param foo;".getParamName()).isEqualTo("foo")
+ assertThat(" \t@param\t foo bar.".getParamName()).isEqualTo("foo")
+ assertThat("@param[foo]".getParamName()).isEqualTo("foo")
+ assertThat("@param [foo]".getParamName()).isEqualTo("foo")
+ assertThat("@param ".getParamName()).isEqualTo(null)
+ }
+}
diff --git a/pom.xml b/pom.xml
index b8a40db..bcfe584 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
<groupId>com.facebook</groupId>
<artifactId>ktfmt-parent</artifactId>
- <version>0.39</version>
+ <version>0.42</version>
<packaging>pom</packaging>
<name>Ktfmt Parent</name>
diff --git a/version.txt b/version.txt
index 751b1ea..f4bb45b 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-0.39
+0.42
diff --git a/website/package-lock.json b/website/package-lock.json
index c178ada..d28214b 100644
--- a/website/package-lock.json
+++ b/website/package-lock.json
@@ -561,7 +561,7 @@
"node_modules/camelcase": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
- "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
+ "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
@@ -2394,9 +2394,9 @@
}
},
"node_modules/json-schema": {
- "version": "0.2.3",
- "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
- "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
+ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"dev": true
},
"node_modules/json-schema-traverse": {
@@ -2418,18 +2418,18 @@
"dev": true
},
"node_modules/jsprim": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
- "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
+ "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==",
"dev": true,
- "engines": [
- "node >=0.6.0"
- ],
"dependencies": {
"assert-plus": "1.0.0",
"extsprintf": "1.3.0",
- "json-schema": "0.2.3",
+ "json-schema": "0.4.0",
"verror": "1.10.0"
+ },
+ "engines": {
+ "node": ">=0.6.0"
}
},
"node_modules/just-debounce": {
@@ -5064,9 +5064,9 @@
"dev": true
},
"node_modules/yargs": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.1.tgz",
- "integrity": "sha512-huO4Fr1f9PmiJJdll5kwoS2e4GqzGSsMT3PPMpOwoVkOK8ckqAewMTZyA6LXVQWflleb/Z8oPBEvNsMft0XE+g==",
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.2.tgz",
+ "integrity": "sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==",
"dev": true,
"dependencies": {
"camelcase": "^3.0.0",
@@ -5081,13 +5081,13 @@
"string-width": "^1.0.2",
"which-module": "^1.0.0",
"y18n": "^3.2.1",
- "yargs-parser": "5.0.0-security.0"
+ "yargs-parser": "^5.0.1"
}
},
"node_modules/yargs-parser": {
- "version": "5.0.0-security.0",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0-security.0.tgz",
- "integrity": "sha512-T69y4Ps64LNesYxeYGYPvfoMTt/7y1XtfpIslUeK4um+9Hu7hlGoRtaDLvdXb7+/tfq4opVa2HRY5xGip022rQ==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz",
+ "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==",
"dev": true,
"dependencies": {
"camelcase": "^3.0.0",
@@ -5545,7 +5545,7 @@
"camelcase": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
- "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
+ "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==",
"dev": true
},
"caseless": {
@@ -7048,9 +7048,9 @@
}
},
"json-schema": {
- "version": "0.2.3",
- "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
- "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
+ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"dev": true
},
"json-schema-traverse": {
@@ -7072,14 +7072,14 @@
"dev": true
},
"jsprim": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
- "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
+ "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==",
"dev": true,
"requires": {
"assert-plus": "1.0.0",
"extsprintf": "1.3.0",
- "json-schema": "0.2.3",
+ "json-schema": "0.4.0",
"verror": "1.10.0"
}
},
@@ -9229,9 +9229,9 @@
"dev": true
},
"yargs": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.1.tgz",
- "integrity": "sha512-huO4Fr1f9PmiJJdll5kwoS2e4GqzGSsMT3PPMpOwoVkOK8ckqAewMTZyA6LXVQWflleb/Z8oPBEvNsMft0XE+g==",
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.2.tgz",
+ "integrity": "sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==",
"dev": true,
"requires": {
"camelcase": "^3.0.0",
@@ -9246,13 +9246,13 @@
"string-width": "^1.0.2",
"which-module": "^1.0.0",
"y18n": "^3.2.1",
- "yargs-parser": "5.0.0-security.0"
+ "yargs-parser": "^5.0.1"
}
},
"yargs-parser": {
- "version": "5.0.0-security.0",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0-security.0.tgz",
- "integrity": "sha512-T69y4Ps64LNesYxeYGYPvfoMTt/7y1XtfpIslUeK4um+9Hu7hlGoRtaDLvdXb7+/tfq4opVa2HRY5xGip022rQ==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz",
+ "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==",
"dev": true,
"requires": {
"camelcase": "^3.0.0",