aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJordan Demeulenaere <jdemeulenaere@google.com>2023-02-23 16:44:36 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2023-02-23 16:44:36 +0000
commita9fa90b3c3eeb707216323ca430d227f15b93d4f (patch)
treeeb485078f187c7dde2061192b197bbdded5f971a
parentf2ef27b6a0dd6c027e814ab4af4ee21f88746fed (diff)
parentc431a78e6c3dff3faea337fb2fa03900cdf148c2 (diff)
downloadktfmt-a9fa90b3c3eeb707216323ca430d227f15b93d4f.tar.gz
Merge tag 'v0.43' into aosp/master am: 732c9702b2 am: c431a78e6c
Original change: https://android-review.googlesource.com/c/platform/external/ktfmt/+/2453026 Change-Id: I2249c74960adb718a8520bd4b0dd52b06163f8f1 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r--core/pom.xml6
-rw-r--r--core/src/main/java/com/facebook/ktfmt/cli/Main.kt2
-rw-r--r--core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt10
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/FenceCommentsOp.kt37
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt126
-rw-r--r--core/src/main/java/com/facebook/ktfmt/format/TypeNameClassifier.kt1
-rw-r--r--core/src/main/java/com/facebook/ktfmt/kdoc/Escaping.kt5
-rw-r--r--core/src/main/java/com/facebook/ktfmt/kdoc/KDocFormattingOptions.kt3
-rw-r--r--core/src/main/java/com/facebook/ktfmt/kdoc/Paragraph.kt114
-rw-r--r--core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphListBuilder.kt50
-rw-r--r--core/src/main/java/com/facebook/ktfmt/kdoc/Utilities.kt8
-rw-r--r--core/src/main/java/com/facebook/ktfmt/kdoc/format.md1
-rw-r--r--core/src/test/java/com/facebook/ktfmt/cli/ParsedArgsTest.kt27
-rw-r--r--core/src/test/java/com/facebook/ktfmt/format/FormatterTest.kt52
-rw-r--r--core/src/test/java/com/facebook/ktfmt/kdoc/KDocFormatterTest.kt364
-rw-r--r--core/src/test/java/com/facebook/ktfmt/kdoc/UtilitiesTest.kt73
-rw-r--r--core/src/test/java/com/facebook/ktfmt/testutil/KtfmtTruth.kt5
-rw-r--r--pom.xml2
-rw-r--r--version.txt2
-rw-r--r--website/index.html4
-rw-r--r--website/package-lock.json36
21 files changed, 764 insertions, 164 deletions
diff --git a/core/pom.xml b/core/pom.xml
index 765c8f1..c5e5119 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -11,7 +11,7 @@
<parent>
<groupId>com.facebook</groupId>
<artifactId>ktfmt-parent</artifactId>
- <version>0.42</version>
+ <version>0.43</version>
</parent>
<properties>
@@ -103,7 +103,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
- <version>2.6</version>
+ <version>3.3.0</version>
<configuration>
<archive>
<manifest>
@@ -116,7 +116,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
- <version>2.6</version>
+ <version>3.3.0</version>
<executions>
<execution>
<id>make-assembly</id>
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 0a08944..980897d 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] [--stdin-name=<name>] File1.kt File2.kt ...")
+ "Usage: ktfmt [--dropbox-style | --google-style | --kotlinlang-style] [--dry-run] [--set-exit-if-changed] [--stdin-name=<name>] [--do-not-remove-unused-imports] File1.kt File2.kt ...")
err.println("Or: ktfmt @file")
return 1
}
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 4c66efd..2c84972 100644
--- a/core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt
+++ b/core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt
@@ -52,6 +52,7 @@ data class ParsedArgs(
var formattingOptions = FormattingOptions()
var dryRun = false
var setExitIfChanged = false
+ var removeUnusedImports = true
var stdinName: String? = null
for (arg in args) {
@@ -61,6 +62,7 @@ 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 == "--do-not-remove-unused-imports" -> removeUnusedImports = false
arg.startsWith("--stdin-name") -> stdinName = parseKeyValueArg(err, "--stdin-name", arg)
arg.startsWith("--") -> err.println("Unexpected option: $arg")
arg.startsWith("@") -> err.println("Unexpected option: $arg")
@@ -68,7 +70,13 @@ data class ParsedArgs(
}
}
- return ParsedArgs(fileNames, formattingOptions, dryRun, setExitIfChanged, stdinName)
+ return ParsedArgs(
+ fileNames,
+ formattingOptions.copy(removeUnusedImports = removeUnusedImports),
+ dryRun,
+ setExitIfChanged,
+ stdinName,
+ )
}
private fun parseKeyValueArg(err: PrintStream, key: String, arg: String): String? {
diff --git a/core/src/main/java/com/facebook/ktfmt/format/FenceCommentsOp.kt b/core/src/main/java/com/facebook/ktfmt/format/FenceCommentsOp.kt
new file mode 100644
index 0000000..721bd05
--- /dev/null
+++ b/core/src/main/java/com/facebook/ktfmt/format/FenceCommentsOp.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+package com.facebook.ktfmt.format
+
+import com.google.common.collect.ImmutableList
+import com.google.googlejavaformat.DocBuilder
+import com.google.googlejavaformat.Op
+
+/**
+ * A dummy [Op] that prevents comments from being moved ahead of it, into parent [Level]s.
+ *
+ * If a comment is the first thing in a [Level], [OpBuilder] moves it outside of that level during
+ * [Doc] building. It does so recursively, until the comment is not the first element of the level.
+ * This behaviour can be very confusing, where comments seem to absorb or ignore expected
+ * indentation.
+ */
+object FenceCommentsOp : Op {
+ val AS_LIST = ImmutableList.of<Op>(FenceCommentsOp)
+
+ override fun add(builder: DocBuilder) {
+ // Do nothing. This Op simply needs to be in the OpsBuilder.
+ }
+}
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 898b70e..433ae1a 100644
--- a/core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt
+++ b/core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt
@@ -121,6 +121,7 @@ import org.jetbrains.kotlin.psi.KtWhenConditionWithExpression
import org.jetbrains.kotlin.psi.KtWhenExpression
import org.jetbrains.kotlin.psi.KtWhileExpression
import org.jetbrains.kotlin.psi.psiUtil.children
+import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespace
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.psi.psiUtil.startsWithComment
@@ -277,7 +278,7 @@ class KotlinInputAstVisitor(
/**
* @param keyword e.g., "fun" or "class".
* @param typeOrDelegationCall for functions, the return typeOrDelegationCall; for classes, the
- * list of supertypes.
+ * list of supertypes.
*/
private fun visitFunctionLikeExpression(
modifierList: KtModifierList?,
@@ -627,6 +628,7 @@ class KotlinInputAstVisitor(
* lastIndexToOpen to track the spot after the last time we stopped
* grouping.
* ```
+ *
* The final expression with groupings:
* ```
* {{a.b}[2]}.{c.d}()
@@ -794,7 +796,7 @@ class KotlinInputAstVisitor(
* 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.
+ * terminate in a negative closing indent. See [visitEachCommaSeparated] for examples.
*/
private fun visitValueArgumentListInternal(list: KtValueArgumentList): BreakTag? {
builder.sync(list)
@@ -844,9 +846,9 @@ class KotlinInputAstVisitor(
* The internal version of [visitLambdaExpression].
*
* @param brokeBeforeBrace used for tracking if a break was taken right before the lambda
- * expression. Useful for scoping functions where we want good looking indentation. For example,
- * here we have correct indentation before `bar()` and `car()` because we can detect the break
- * after the equals:
+ * expression. Useful for scoping functions where we want good looking indentation. For example,
+ * here we have correct indentation before `bar()` and `car()` because we can detect the break
+ * after the equals:
* ```
* fun foo() =
* coroutineScope { x ->
@@ -922,12 +924,9 @@ class KotlinInputAstVisitor(
if (hasParams || hasArrow || hasStatements) {
// If we had to break in the body, ensure there is a break before the closing brace
builder.breakOp(Doc.FillMode.UNIFIED, " ", bracePlusZeroIndent)
- builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO)
}
builder.block(bracePlusZeroIndent) {
- // If there are closing comments, make sure they and the brace are indented together
- // The comments will indent themselves, so consume the previous break as a blank line
- builder.breakOp(Doc.FillMode.INDEPENDENT, "", ZERO)
+ builder.fenceComments()
builder.token("}", blockIndent)
}
}
@@ -993,7 +992,7 @@ class KotlinInputAstVisitor(
* ```
*
* @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.
+ * fit in a single line), and a trailing comma is emitted.
*
* Example:
* ```
@@ -1002,17 +1001,17 @@ class KotlinInputAstVisitor(
* ```
*
* @param wrapInBlock if true, place all the elements in a block. When there's no [leadingBreak],
- * this will be negatively indented. Note that the [prefix] and [postfix] aren't included in the
- * block.
+ * this will be negatively indented. 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.
+ * block.
* @param breakBeforePostfix if true, place a break after the last element. Redundant when
- * [hasTrailingComma] is true.
+ * [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.
+ * terminate in a negative closing indent.
*
* Example 1, this returns a BreakTag which tells you a break wasn't taken:
* ```
@@ -1094,11 +1093,8 @@ class KotlinInputAstVisitor(
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.fenceComments()
builder.token(postfix, expressionBreakIndent)
}
} else {
@@ -1321,54 +1317,55 @@ class KotlinInputAstVisitor(
builder.token(".")
}
builder.token(name)
- builder.op("")
}
}
- if (name != null) {
- builder.open(expressionBreakIndent) // open block for named values
- }
- // For example `: String` in `val thisIsALongName: String` or `fun f(): String`
- if (type != null) {
- if (name != null) {
- builder.token(":")
- builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO)
+ builder.block(expressionBreakIndent, isEnabled = name != null) {
+ // For example `: String` in `val thisIsALongName: String` or `fun f(): String`
+ if (type != null) {
+ if (name != null) {
+ builder.token(":")
+ builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO)
+ }
+ visit(type)
}
- visit(type)
}
- }
-
- // For example `where T : Int` in a generic method
- if (typeConstraintList != null) {
- builder.space()
- visit(typeConstraintList)
- builder.space()
- }
- // for example `by lazy { compute() }`
- if (delegate != null) {
- builder.space()
- builder.token("by")
- if (isLambdaOrScopingFunction(delegate.expression)) {
+ // For example `where T : Int` in a generic method
+ if (typeConstraintList != null) {
+ builder.space()
+ visit(typeConstraintList)
builder.space()
- visit(delegate)
- } else {
- builder.breakOp(Doc.FillMode.UNIFIED, " ", expressionBreakIndent)
- builder.block(expressionBreakIndent) { visit(delegate) }
}
- } else if (initializer != null) {
- builder.space()
- builder.token("=")
- if (isLambdaOrScopingFunction(initializer)) {
- visitLambdaOrScopingFunction(initializer)
- } else {
- builder.breakOp(Doc.FillMode.UNIFIED, " ", expressionBreakIndent)
- builder.block(expressionBreakIndent) { visit(initializer) }
+
+ // for example `by lazy { compute() }`
+ if (delegate != null) {
+ builder.space()
+ builder.token("by")
+ if (isLambdaOrScopingFunction(delegate.expression)) {
+ builder.space()
+ visit(delegate)
+ } else {
+ builder.breakOp(Doc.FillMode.UNIFIED, " ", expressionBreakIndent)
+ builder.block(expressionBreakIndent) {
+ builder.fenceComments()
+ visit(delegate)
+ }
+ }
+ } else if (initializer != null) {
+ builder.space()
+ builder.token("=")
+ if (isLambdaOrScopingFunction(initializer)) {
+ visitLambdaOrScopingFunction(initializer)
+ } else {
+ builder.breakOp(Doc.FillMode.UNIFIED, " ", expressionBreakIndent)
+ builder.block(expressionBreakIndent) {
+ builder.fenceComments()
+ visit(initializer)
+ }
+ }
}
}
- if (name != null) {
- builder.close() // close block for named values
- }
// for example `private set` or `get = 2 * field`
if (accessors?.isNotEmpty() == true) {
builder.block(blockIndent) {
@@ -1419,6 +1416,10 @@ class KotlinInputAstVisitor(
* 2. '... = Runnable @Annotation { ... }' due to the annotation
*/
private fun isLambdaOrScopingFunction(expression: KtExpression?): Boolean {
+ if (expression == null) return false
+ if (expression.getPrevSiblingIgnoringWhitespace() is PsiComment) {
+ return false // Leading comments cause weird indentation.
+ }
if (expression is KtLambdaExpression) {
return true
}
@@ -2479,11 +2480,7 @@ class KotlinInputAstVisitor(
* @param plusIndent the block level to pass to the block
* @param block a code block to be run in this block level
*/
- private inline fun OpsBuilder.block(
- plusIndent: Indent,
- isEnabled: Boolean = true,
- block: () -> Unit
- ) {
+ private fun OpsBuilder.block(plusIndent: Indent, isEnabled: Boolean = true, block: () -> Unit) {
if (isEnabled) {
open(plusIndent)
}
@@ -2498,6 +2495,11 @@ class KotlinInputAstVisitor(
sync(psiElement.startOffset)
}
+ /** Prevent susequent comments from being moved ahead of this point, into parent [Level]s. */
+ private fun OpsBuilder.fenceComments() {
+ addAll(FenceCommentsOp.AS_LIST)
+ }
+
/**
* Throws a formatting error
*
diff --git a/core/src/main/java/com/facebook/ktfmt/format/TypeNameClassifier.kt b/core/src/main/java/com/facebook/ktfmt/format/TypeNameClassifier.kt
index eba3ccc..76fac3e 100644
--- a/core/src/main/java/com/facebook/ktfmt/format/TypeNameClassifier.kt
+++ b/core/src/main/java/com/facebook/ktfmt/format/TypeNameClassifier.kt
@@ -90,7 +90,6 @@ object TypeNameClassifier {
* a type or static field access, or -1 if no such prefix was found.
*
* Examples:
- *
* * ClassName
* * ClassName.staticMemberName
* * com.google.ClassName.InnerClass.staticMemberName
diff --git a/core/src/main/java/com/facebook/ktfmt/kdoc/Escaping.kt b/core/src/main/java/com/facebook/ktfmt/kdoc/Escaping.kt
index 85c9938..d3d3c10 100644
--- a/core/src/main/java/com/facebook/ktfmt/kdoc/Escaping.kt
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/Escaping.kt
@@ -44,10 +44,7 @@ object Escaping {
s.substring(endMarkerIndex)
}
- /**
- *
- * See [escapeKDoc].
- */
+ /** See [escapeKDoc]. */
fun unescapeKDoc(s: String): String =
s.replace(SLASH_STAR_ESCAPE, "/*").replace(STAR_SLASH_ESCAPE, "*/")
}
diff --git a/core/src/main/java/com/facebook/ktfmt/kdoc/KDocFormattingOptions.kt b/core/src/main/java/com/facebook/ktfmt/kdoc/KDocFormattingOptions.kt
index bfe80ea..0d50d47 100644
--- a/core/src/main/java/com/facebook/ktfmt/kdoc/KDocFormattingOptions.kt
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/KDocFormattingOptions.kt
@@ -60,7 +60,8 @@ class KDocFormattingOptions(
/**
* 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.
+ * more here will result in subsequent lines being interpreted as block formatted by IntelliJ (but
+ * not Dokka).
*/
var hangingIndent: Int = 2
diff --git a/core/src/main/java/com/facebook/ktfmt/kdoc/Paragraph.kt b/core/src/main/java/com/facebook/ktfmt/kdoc/Paragraph.kt
index 93e98e5..6d905e5 100644
--- a/core/src/main/java/com/facebook/ktfmt/kdoc/Paragraph.kt
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/Paragraph.kt
@@ -101,10 +101,6 @@ class Paragraph(private val task: FormattingTask) {
return content.isEmpty()
}
- private fun hasClosingPre(): Boolean {
- return content.contains("</pre>", ignoreCase = false) || next?.hasClosingPre() ?: false
- }
-
fun cleanup() {
val original = text
@@ -164,11 +160,23 @@ class Paragraph(private val task: FormattingTask) {
}
private fun convertMarkup(s: String): String {
- if (s.none { it == '<' || it == '&' || it == '{' }) return s
+ // Whether the tag starts with a capital letter and needs to be cleaned, e.g. `@See` -> `@see`.
+ // (isKDocTag only allows the first letter to be capitalized.)
+ val convertKDocTag = s.isKDocTag() && s[1].isUpperCase()
+
+ if (!convertKDocTag && s.none { it == '<' || it == '&' || it == '{' }) {
+ return s
+ }
val sb = StringBuilder(s.length)
var i = 0
val n = s.length
+
+ if (convertKDocTag) {
+ sb.append('@').append(s[1].lowercaseChar())
+ i += 2
+ }
+
var code = false
var brackets = 0
while (i < n) {
@@ -335,8 +343,12 @@ class Paragraph(private val task: FormattingTask) {
return reflow(words, lineWidth, hangingIndentSize)
}
- fun reflow(words: List<String>, lineWidth: Int, hangingIndentSize: Int): List<String> {
- if (options.alternate || !options.optimal || hanging && hangingIndentSize > 0) {
+ private fun reflow(words: List<String>, lineWidth: Int, hangingIndentSize: Int): List<String> {
+ if (options.alternate ||
+ !options.optimal ||
+ hanging && hangingIndentSize > 0 ||
+ // An unbreakable long word may make other lines shorter and won't look good
+ words.any { it.length > lineWidth }) {
// 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
@@ -405,7 +417,8 @@ class Paragraph(private val task: FormattingTask) {
word.startsWith("```") ||
word.isDirectiveMarker() ||
word.startsWith("@") || // interpreted as a tag
- word.isTodo()) {
+ word.isTodo() ||
+ word.startsWith(">")) {
return false
}
@@ -419,7 +432,14 @@ class Paragraph(private val task: FormattingTask) {
return true
}
- private fun computeWords(): List<String> {
+ /**
+ * Split [text] up into individual "words"; in the case where some words are not allowed to span
+ * lines, it will combine these into single word. For example, if we have a sentence which ends
+ * with a number, e.g. "the sum is 5.", we want to make sure "5." is never placed at the beginning
+ * of a new line (which would turn it into a list item), so for this we'll compute the word list
+ * "the", "sum", "is 5.".
+ */
+ fun computeWords(): List<String> {
val words = text.split(Regex("\\s+")).filter { it.isNotBlank() }.map { it.trim() }
if (words.size == 1) {
return words
@@ -438,39 +458,53 @@ class Paragraph(private val task: FormattingTask) {
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++]
- }
+ var from = 0
+ val end = words.size
+ while (from < end) {
+ val start =
+ if (from == 0 && (quoted || hanging && !text.isKDocTag())) {
+ from + 2
+ } else {
+ from + 1
+ }
+ var to = words.size
+ for (i in start until words.size) {
+ val next = words[i]
+ if (next.startsWith("[") && !next.startsWith("[[")) {
+ // find end
+ var j = -1
+ for (k in i until words.size) {
+ if (']' in words[k]) {
+ j = k
+ break
+ }
+ }
+ if (j != -1) {
+ // combine everything in the string; we can't break link text
+ if (start == from + 1 && canBreakAt(words[start])) {
+ combined.add(words[from])
+ from = start
+ }
+ // Maybe not break; what if the next word isn't okay?
+ to = j + 1
+ if (to == words.size || canBreakAt(words[to])) {
+ break
+ }
+ } // else: unterminated [, ignore
+ } else if (canBreakAt(next)) {
+ to = i
+ break
+ }
+ }
- 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
+ if (to == from + 1) {
+ combined.add(words[from])
+ } else if (to > from) {
+ combined.add(words.subList(from, to).joinToString(" "))
}
+ from = to
}
+
return combined
}
@@ -503,7 +537,7 @@ class Paragraph(private val task: FormattingTask) {
}
fun search(pi0: Int, pj0: Int, pi1: Int, pj1: Int) {
- val stack = java.util.ArrayDeque<Quadruple>()
+ val stack = ArrayDeque<Quadruple>()
stack.add(Quadruple(pi0, pj0, pi1, pj1))
while (stack.isNotEmpty()) {
diff --git a/core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphListBuilder.kt b/core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphListBuilder.kt
index cb0891e..928a786 100644
--- a/core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphListBuilder.kt
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphListBuilder.kt
@@ -48,6 +48,7 @@ class ParagraphListBuilder(
private fun closeParagraph(): Paragraph {
val text = paragraph.text
when {
+ paragraph.preformatted -> {}
text.isKDocTag() -> {
paragraph.doc = true
paragraph.hanging = true
@@ -142,14 +143,15 @@ class ParagraphListBuilder(
newParagraph()
var j = i
var foundClose = false
- var customize = true
+ var allowCustomize = 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
+ // Don't convert <pre> tags if we already have nested ``` content; that will lead to
+ // trouble
+ allowCustomize = false
}
val done = (includeStart || j > i) && until(lineWithIndentation)
if (!includeEnd && done) {
@@ -175,7 +177,7 @@ class ParagraphListBuilder(
if (!foundClose && expectClose) {
// Just add a single line as preformatted and then treat the rest in the
// normal way
- customize = false
+ allowCustomize = false
j = lines.size
}
@@ -185,7 +187,7 @@ class ParagraphListBuilder(
appendText(lineWithIndentation)
paragraph.preformatted = true
paragraph.allowEmpty = true
- if (customize) {
+ if (allowCustomize) {
customize(index, paragraph)
}
newParagraph()
@@ -254,8 +256,11 @@ class ParagraphListBuilder(
newParagraph(i - 1).block = true
appendText(lineWithoutIndentation)
newParagraph(i).block = true
- } else if (lineWithoutIndentation.startsWith(
- "#")) { // not isHeader() because <h> is handled separately
+ } else if (lineWithoutIndentation.startsWith("#")
+ // "## X" is a header, "##X" is not
+ &&
+ lineWithoutIndentation.firstOrNull { it != '#' }?.equals(' ') ==
+ true) { // not isHeader() because <h> is handled separately
// ## Header
newParagraph(i - 1).block = true
appendText(lineWithoutIndentation)
@@ -496,17 +501,28 @@ class ParagraphListBuilder(
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 }
+ private fun convertPrefix(text: String): String {
+ return if (options.convertMarkup &&
+ (text.startsWith("<p>", true) || text.startsWith("<p/>", true))) {
+ paragraph.separate = true
+ text.substring(text.indexOf('>') + 1).trim()
+ } else {
+ text
+ }
+ }
+ private fun convertSuffix(trimmedPrefix: String): String {
+ return if (options.convertMarkup &&
+ (trimmedPrefix.endsWith("<p/>", true) || (trimmedPrefix.endsWith("</p>", true)))) {
+ trimmedPrefix.substring(0, trimmedPrefix.length - 4).trimEnd().removeSuffix("*").trimEnd()
+ } else {
+ trimmedPrefix
+ }
+ }
+
+ private fun addPlainText(i: Int, text: String, braceBalance: Int = 0): Int {
+ val trimmed = convertSuffix(convertPrefix(text))
+ val s = trimmed.let { if (options.collapseSpaces) it.collapseSpaces() else it }
appendText(s)
appendText(" ")
diff --git a/core/src/main/java/com/facebook/ktfmt/kdoc/Utilities.kt b/core/src/main/java/com/facebook/ktfmt/kdoc/Utilities.kt
index e034bbe..597a285 100644
--- a/core/src/main/java/com/facebook/ktfmt/kdoc/Utilities.kt
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/Utilities.kt
@@ -114,17 +114,21 @@ fun String.isLine(minCount: Int = 3): Boolean {
fun String.isKDocTag(): Boolean {
// Not using a hardcoded list here since tags can change over time
- if (startsWith("@")) {
+ if (startsWith("@") && length > 1) {
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")) {
+ if (c == '[' && (startsWith("@param") || startsWith("@property"))) {
// @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
+ } else if (i == 1 && c.isLetter() && c.isUpperCase()) {
+ // Allow capitalized tgs, such as @See -- this is normally a typo; convertMarkup
+ // should also fix these.
+ return true
}
return false
}
diff --git a/core/src/main/java/com/facebook/ktfmt/kdoc/format.md b/core/src/main/java/com/facebook/ktfmt/kdoc/format.md
new file mode 100644
index 0000000..0f33bb7
--- /dev/null
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/format.md
@@ -0,0 +1 @@
+kdoc-formatter --greedy --max-line-width=100 --max-comment-width=100 .
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 37cbf57..9f0919b 100644
--- a/core/src/test/java/com/facebook/ktfmt/cli/ParsedArgsTest.kt
+++ b/core/src/test/java/com/facebook/ktfmt/cli/ParsedArgsTest.kt
@@ -108,6 +108,33 @@ class ParsedArgsTest {
}
@Test
+ fun `parseOptions defaults to removing imports`() {
+ val (parsed, _) = parseTestOptions("foo.kt")
+ assertThat(parsed.formattingOptions.removeUnusedImports).isTrue()
+ }
+
+ @Test
+ fun `parseOptions recognizes --do-not-remove-unused-imports to removing imports`() {
+ val (parsed, _) = parseTestOptions("--do-not-remove-unused-imports", "foo.kt")
+ assertThat(parsed.formattingOptions.removeUnusedImports).isFalse()
+ }
+
+ @Test
+ fun `parseOptions handles dropbox style and --do-not-remove-unused-imports`() {
+ val (parsed, _) =
+ parseTestOptions("--do-not-remove-unused-imports", "--dropbox-style", "foo.kt")
+ assertThat(parsed.formattingOptions.removeUnusedImports).isFalse()
+ assertThat(parsed.formattingOptions.style).isEqualTo(FormattingOptions.Style.DROPBOX)
+ }
+
+ @Test
+ fun `parseOptions handles google style and --do-not-remove-unused-imports`() {
+ val (parsed, _) = parseTestOptions("--do-not-remove-unused-imports", "--google-style", "foo.kt")
+ assertThat(parsed.formattingOptions.removeUnusedImports).isFalse()
+ assertThat(parsed.formattingOptions.style).isEqualTo(FormattingOptions.Style.GOOGLE)
+ }
+
+ @Test
fun `parseOptions --stdin-name`() {
val (parsed, _) = parseTestOptions("--stdin-name=my/foo.kt")
assertThat(parsed.stdinName).isEqualTo("my/foo.kt")
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 88b16fe..ae9d05e 100644
--- a/core/src/test/java/com/facebook/ktfmt/format/FormatterTest.kt
+++ b/core/src/test/java/com/facebook/ktfmt/format/FormatterTest.kt
@@ -446,6 +446,58 @@ class FormatterTest {
.trimMargin())
@Test
+ fun `properties with line comment above initializer`() =
+ assertFormatted(
+ """
+ |class Foo {
+ | var x: Int =
+ | // Comment
+ | 0
+ |
+ | var y: Int =
+ | // Comment
+ | scope {
+ | 0 //
+ | }
+ |
+ | var z: Int =
+ | // Comment
+ | if (cond) {
+ | 0
+ | } else {
+ | 1
+ | }
+ |}
+ |"""
+ .trimMargin())
+
+ @Test
+ fun `properties with line comment above delegate`() =
+ assertFormatted(
+ """
+ |class Foo {
+ | var x: Int by
+ | // Comment
+ | 0
+ |
+ | var y: Int by
+ | // Comment
+ | scope {
+ | 0 //
+ | }
+ |
+ | var z: Int by
+ | // Comment
+ | if (cond) {
+ | 0
+ | } else {
+ | 1
+ | }
+ |}
+ |"""
+ .trimMargin())
+
+ @Test
fun `properties with accessors`() =
assertFormatted(
"""
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 bb1b156..af13a25 100644
--- a/core/src/test/java/com/facebook/ktfmt/kdoc/KDocFormatterTest.kt
+++ b/core/src/test/java/com/facebook/ktfmt/kdoc/KDocFormatterTest.kt
@@ -120,10 +120,10 @@ class KDocFormatterTest {
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
+ * 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
@@ -3522,9 +3522,9 @@ class KDocFormatterTest {
* # 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.
+ * . 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`)
@@ -4579,6 +4579,356 @@ class KDocFormatterTest {
verifyDokka = false)
}
+ @Test
+ fun testOpenRange() {
+ // https://github.com/tnorbye/kdoc-formatter/issues/84
+ val source =
+ """
+ /**
+ * This is a line that has the length such that this [link gets
+ * broken across lines]() which is not valid.
+ *
+ * Input is a float in range
+ * [0, 1) where 0 is fully settled and 1 is dismissed. Will be continue to be called after the user's finger has lifted. Will not be called for if not dismissible.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72),
+ """
+ /**
+ * This is a line that has the length such that this
+ * [link gets broken across lines]() which is not valid.
+ *
+ * Input is a float in range [0, 1) where 0 is fully settled and 1 is
+ * dismissed. Will be continue to be called after the user's finger has
+ * lifted. Will not be called for if not dismissible.
+ */
+ """
+ .trimIndent(),
+ indent = "")
+ }
+
+ @Test
+ fun testPropertiesWithBrackets() {
+ val source =
+ // From AOSP
+ // tools/base/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/cxx/prefab/PackageModel.kt
+ """
+ /**
+ * The Android abi.json schema.
+ *
+ * @property[abi] The ABI name of the described library. These names match the tag field for
+ * [com.android.build.gradle.internal.core.Abi].
+ * @property[api] The minimum OS version supported by the library. i.e. the
+ * library's `minSdkVersion`.
+ * @property[ndk] The major version of the NDK that this library was built with.
+ * @property[stl] The STL that this library was built with.
+ * @property[static] If true then the library is .a, if false then .so.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72),
+ """
+ /**
+ * The Android abi.json schema.
+ *
+ * @property[abi] The ABI name of the described library. These names
+ * match the tag field for
+ * [com.android.build.gradle.internal.core.Abi].
+ * @property[api] The minimum OS version supported by the library. i.e.
+ * the library's `minSdkVersion`.
+ * @property[ndk] The major version of the NDK that this library was
+ * built with.
+ * @property[stl] The STL that this library was built with.
+ * @property[static] If true then the library is .a, if false then .so.
+ */
+ """
+ .trimIndent(),
+ indent = "")
+ }
+
+ @Test
+ fun testHandingIndent() {
+ val source =
+ """
+ /**
+ * @param count this is how many you
+ * can fit in a [Bag]
+ * @param weight how heavy this would
+ * be in [Grams]
+ */
+ """
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72),
+ """
+ /**
+ * @param count this is how many you can fit in a [Bag]
+ * @param weight how heavy this would be in [Grams]
+ */
+ """
+ .trimIndent(),
+ indent = "")
+ }
+
+ @Test
+ fun testMarkupAcrossLines() {
+ val source =
+ """
+ /**
+ * Broadcast Action: Indicates the Bluetooth scan mode of the local Adapter
+ * has changed.
+ * <p>Always contains the extra fields {@link #EXTRA_SCAN_MODE} and {@link
+ * #EXTRA_PREVIOUS_SCAN_MODE} containing the new and old scan modes
+ * respectively.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72),
+ """
+ /**
+ * Broadcast Action: Indicates the Bluetooth scan mode of the local
+ * Adapter has changed.
+ *
+ * Always contains the extra fields [EXTRA_SCAN_MODE] and
+ * [EXTRA_PREVIOUS_SCAN_MODE] containing the new and old scan modes
+ * respectively.
+ */
+ """
+ .trimIndent(),
+ indent = "")
+ }
+
+ @Test
+ fun testReferences() {
+ val source =
+ """
+ /**
+ * Construct a rectangle from its left and top edges as well as its width and height.
+ * @param offset Offset to represent the top and left parameters of the Rect
+ * @param size Size to determine the width and height of this [Rect].
+ * @return Rect with [Rect.left] and [Rect.top] configured to [Offset.x] and [Offset.y] as
+ * [Rect.right] and [Rect.bottom] to [Offset.x] + [Size.width] and [Offset.y] + [Size.height]
+ * respectively
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72),
+ """
+ /**
+ * Construct a rectangle from its left and top edges as well as its
+ * width and height.
+ *
+ * @param offset Offset to represent the top and left parameters of the
+ * Rect
+ * @param size Size to determine the width and height of this [Rect].
+ * @return Rect with [Rect.left] and [Rect.top] configured to [Offset.x]
+ * and [Offset.y] as [Rect.right] and [Rect.bottom] to
+ * [Offset.x] + [Size.width] and [Offset.y] + [Size.height]
+ * respectively
+ */
+ """
+ .trimIndent(),
+ indent = "")
+ }
+
+ @Test
+ fun testDecapitalizeKdocTags() {
+ val source =
+ """
+ /**
+ * Represents a component that handles scroll events, so that other components in the hierarchy
+ * can adjust their behaviour.
+ * @See [provideScrollContainerInfo] and [consumeScrollContainerInfo]
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72).apply { convertMarkup = true },
+ """
+ /**
+ * Represents a component that handles scroll events, so that other
+ * components in the hierarchy can adjust their behaviour.
+ *
+ * @see [provideScrollContainerInfo] and [consumeScrollContainerInfo]
+ */
+ """
+ .trimIndent(),
+ indent = "")
+ }
+
+ @Test
+ fun testLineBreak() {
+ // Makes sure a scenario where we used to put "0." at the beginning of a new line.
+ // From AOSP's
+ // frameworks/support/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlWrapper.kt
+ val source =
+ """
+ /**
+ * Updates z order index for [SurfaceControlWrapper]. Note that the z order for a
+ * surface is relative to other surfaces that are siblings of this surface.
+ * Behavior of siblings with the same z order is undefined.
+ *
+ * Z orders can range from Integer.MIN_VALUE to Integer.MAX_VALUE. Default z order
+ * index is 0. [SurfaceControlWrapper] instances are positioned back-to-front. That is
+ * lower z order values are rendered below other [SurfaceControlWrapper] instances with
+ * higher z order values.
+ *
+ * @param surfaceControl surface control to set the z order of.
+ *
+ * @param zOrder desired layer z order to set the surfaceControl.
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72),
+ """
+ /**
+ * Updates z order index for [SurfaceControlWrapper]. Note that
+ * the z order for a surface is relative to other surfaces that
+ * are siblings of this surface. Behavior of siblings with the
+ * same z order is undefined.
+ *
+ * Z orders can range from Integer.MIN_VALUE
+ * to Integer.MAX_VALUE. Default z order index
+ * is 0. [SurfaceControlWrapper] instances are positioned
+ * back-to-front. That is lower z order values are rendered
+ * below other [SurfaceControlWrapper] instances with higher z
+ * order values.
+ *
+ * @param surfaceControl surface control to set the z order of.
+ * @param zOrder desired layer z order to set the
+ * surfaceControl.
+ */
+ """
+ .trimIndent(),
+ indent = " ")
+ }
+
+ @Test
+ fun testDocTagsInsidePreformatted() {
+ // Makes sure we don't treat markup inside preformatted text as potential
+ // doc tags (with the fix to make us flexible recognize @See as a doctag
+ // it revealed we were also looking inside preformatted text and started
+ // treating annotations like @Retention as a doc tag.)
+ val source =
+ """
+ /**
+ * Denotes that the annotated element of integer type, represents
+ * a logical type and that its value should be one of the explicitly
+ * named constants. If the IntDef#flag() attribute is set to true,
+ * multiple constants can be combined.
+ *
+ * Example:
+ * ```
+ * @Retention(SOURCE)
+ * @IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
+ * public @interface NavigationMode {}
+ * public static final int NAVIGATION_MODE_STANDARD = 0;
+ * public static final int NAVIGATION_MODE_LIST = 1;
+ * public static final int NAVIGATION_MODE_TABS = 2;
+ * ...
+ * public abstract void setNavigationMode(@NavigationMode int mode);
+ *
+ * @NavigationMode
+ * public abstract int getNavigationMode();
+ * ```
+ *
+ * For a flag, set the flag attribute:
+ * ```
+ * @IntDef(
+ * flag = true,
+ * value = {NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS}
+ * )
+ * ```
+ *
+ * @see LongDef
+ */
+
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72),
+ """
+ /**
+ * Denotes that the annotated element of integer type, represents a
+ * logical type and that its value should be one of the explicitly named
+ * constants. If the IntDef#flag() attribute is set to true, multiple
+ * constants can be combined.
+ *
+ * Example:
+ * ```
+ * @Retention(SOURCE)
+ * @IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
+ * public @interface NavigationMode {}
+ * public static final int NAVIGATION_MODE_STANDARD = 0;
+ * public static final int NAVIGATION_MODE_LIST = 1;
+ * public static final int NAVIGATION_MODE_TABS = 2;
+ * ...
+ * public abstract void setNavigationMode(@NavigationMode int mode);
+ *
+ * @NavigationMode
+ * public abstract int getNavigationMode();
+ * ```
+ *
+ * For a flag, set the flag attribute:
+ * ```
+ * @IntDef(
+ * flag = true,
+ * value = {NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS}
+ * )
+ * ```
+ *
+ * @see LongDef
+ */
+ """
+ .trimIndent(),
+ indent = "")
+ }
+
+ @Test
+ fun testConvertMarkup2() {
+ // Bug where the markup conversion around <p></p> wasn't working correctly
+ // From AOSP's
+ // frameworks/support/bluetooth/bluetooth-core/src/main/java/androidx/bluetooth/core/BluetoothAdapter.kt
+ val source =
+ """
+ /**
+ * Fundamentally, this is your starting point for all
+ * Bluetooth actions. * </p>
+ * <p>This class is thread safe.</p>
+ *
+ * @hide
+ */
+ """
+ .trimIndent()
+ checkFormatter(
+ source,
+ KDocFormattingOptions(maxLineWidth = 72),
+ """
+ /**
+ * Fundamentally, this is your starting point for all Bluetooth actions.
+ *
+ * This class is thread safe.
+ *
+ * @hide
+ */
+ """
+ .trimIndent(),
+ indent = "")
+ }
+
/**
* 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
diff --git a/core/src/test/java/com/facebook/ktfmt/kdoc/UtilitiesTest.kt b/core/src/test/java/com/facebook/ktfmt/kdoc/UtilitiesTest.kt
index e5ad1a7..0bdc705 100644
--- a/core/src/test/java/com/facebook/ktfmt/kdoc/UtilitiesTest.kt
+++ b/core/src/test/java/com/facebook/ktfmt/kdoc/UtilitiesTest.kt
@@ -100,6 +100,77 @@ class UtilitiesTest {
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)
+ assertThat("@param ".getParamName()).isNull()
+ }
+
+ @Test
+ fun testComputeWords() {
+ fun List<String>.describe(): String {
+ return "listOf(${this.joinToString(", ") { "\"$it\"" }})"
+ }
+ fun check(text: String, expected: List<String>, customizeParagraph: (Paragraph) -> Unit = {}) {
+ val task = FormattingTask(KDocFormattingOptions(12), "/** $text */", "")
+ val paragraph = Paragraph(task)
+ paragraph.content.append(text)
+ customizeParagraph(paragraph)
+ val words = paragraph.computeWords()
+
+ assertThat(words.describe()).isEqualTo(expected.describe())
+ }
+ check("Foo", listOf("Foo"))
+ check("Foo Bar Baz", listOf("Foo", "Bar", "Baz"))
+ check("Foo Bar Baz", listOf("Foo Bar", "Baz")) { it.quoted = true }
+ check("Foo Bar Baz", listOf("Foo Bar", "Baz")) { it.hanging = true }
+ check("1. Foo", listOf("1.", "Foo"))
+ // "1." can't start a word; if it ends up at the beginning of a line it becomes
+ // a numbered element.
+ check("Foo 1.", listOf("Foo 1."))
+ check("Foo bar [Link Text] foo bar.", listOf("Foo", "bar", "[Link Text]", "foo", "bar."))
+ check("Interval [0, 1) foo bar.", listOf("Interval [0, 1)", "foo", "bar."))
+
+ // ">" cannot start a word; it would become quoted text
+ check("if >= 3", listOf("if >=", "3"))
+ check("if >= 3.", listOf("if >= 3."))
+
+ check(
+ "SDK version - [`Partial(Mode.UseIfAvailable)`](Partial) on API 24+",
+ listOf("SDK", "version - [`Partial(Mode.UseIfAvailable)`](Partial)", "on", "API", "24+"))
+
+ check(
+ "Z orders can range from Integer.MIN_VALUE to Integer.MAX_VALUE. Default z order " +
+ " index is 0. [SurfaceControlWrapper] instances are positioned back-to-front.",
+ listOf(
+ "Z",
+ "orders",
+ "can",
+ "range",
+ "from",
+ "Integer.MIN_VALUE",
+ "to",
+ "Integer.MAX_VALUE.",
+ "Default",
+ "z",
+ "order",
+ "index",
+ "is 0. [SurfaceControlWrapper]",
+ "instances",
+ "are",
+ "positioned",
+ "back-to-front."))
+ check(
+ "Equates to `cmd package compile -f -m speed <package>` on API 24+.",
+ listOf(
+ "Equates",
+ "to",
+ "`cmd",
+ "package",
+ "compile",
+ "-f",
+ "-m",
+ "speed",
+ "<package>`",
+ "on",
+ "API",
+ "24+."))
}
}
diff --git a/core/src/test/java/com/facebook/ktfmt/testutil/KtfmtTruth.kt b/core/src/test/java/com/facebook/ktfmt/testutil/KtfmtTruth.kt
index 3a2a620..de67121 100644
--- a/core/src/test/java/com/facebook/ktfmt/testutil/KtfmtTruth.kt
+++ b/core/src/test/java/com/facebook/ktfmt/testutil/KtfmtTruth.kt
@@ -29,14 +29,15 @@ import org.junit.Assert
* Verifies the given code passes through formatting, and stays the same at the end
*
* @param code a code string that continas an optional first line made of "---" in the case
- * [deduceMaxWidth] is true. For example:
+ * [deduceMaxWidth] is true. For example:
* ```
* --------------------
* // exactly 20 `-` above
* fun f()
* ```
+ *
* @param deduceMaxWidth if this is true the code string should start with a line of "-----" in the
- * beginning to indicate the max width to format by
+ * beginning to indicate the max width to format by
*/
fun assertFormatted(
code: String,
diff --git a/pom.xml b/pom.xml
index bcfe584..264e458 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
<groupId>com.facebook</groupId>
<artifactId>ktfmt-parent</artifactId>
- <version>0.42</version>
+ <version>0.43</version>
<packaging>pom</packaging>
<name>Ktfmt Parent</name>
diff --git a/version.txt b/version.txt
index f4bb45b..68f3790 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-0.42
+0.43
diff --git a/website/index.html b/website/index.html
index 7572e39..151769b 100644
--- a/website/index.html
+++ b/website/index.html
@@ -134,7 +134,7 @@
<pre>
# build.gradle.kts
plugins { id("com.diffplug.spotless") }
-
+
// version and style are optional
spotless { kotlin { ktfmt('{{version}}').kotlinlangStyle() } }
</pre
@@ -285,7 +285,7 @@ $ java -jar ktfmt-{{version}}-jar-with-dependencies.jar [--kotlinlang-style] [fi
/>
</a>
<br />
- <small>&copy; {{year}} Facebook, Inc. Based on <a href="https://microsoft.github.io/monaco-editor">https://microsoft.github.io/monaco-editor</a></small>
+ <small>Copyright &copy; Meta Platforms, Inc. Based on <a href="https://microsoft.github.io/monaco-editor">https://microsoft.github.io/monaco-editor</a></small>
</p>
</footer>
diff --git a/website/package-lock.json b/website/package-lock.json
index d28214b..43d1698 100644
--- a/website/package-lock.json
+++ b/website/package-lock.json
@@ -946,9 +946,9 @@
}
},
"node_modules/decode-uri-component": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
- "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
+ "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
"dev": true,
"engines": {
"node": ">=0.10"
@@ -2750,9 +2750,9 @@
}
},
"node_modules/minimatch": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
- "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -3466,9 +3466,9 @@
}
},
"node_modules/qs": {
- "version": "6.5.2",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
- "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+ "version": "6.5.3",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
+ "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
"dev": true,
"engines": {
"node": ">=0.6"
@@ -5867,9 +5867,9 @@
"dev": true
},
"decode-uri-component": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
- "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
+ "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
"dev": true
},
"deep-is": {
@@ -7340,9 +7340,9 @@
}
},
"minimatch": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
- "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
@@ -7910,9 +7910,9 @@
"dev": true
},
"qs": {
- "version": "6.5.2",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
- "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+ "version": "6.5.3",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
+ "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
"dev": true
},
"read-pkg": {