diff --git a/README.md b/README.md
index 71b7c8be..b4863c36 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,11 @@ For comparison, the same code formatted by [`ktlint`](https://github.com/pintere
| ------ | --------|
| ![ktlint](docs/images/ktlint.png) | ![IntelliJ](docs/images/intellij.png) |
+## Playground
+
+We have a [live playground](https://facebook.github.io/ktfmt/) where you can easily see how ktfmt would format your code.
+Give it a try! https://facebook.github.io/ktfmt/
+
## Using the formatter
### IntelliJ, Android Studio, and other JetBrains IDEs
diff --git a/core/pom.xml b/core/pom.xml
index 62e841ef..267689e1 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -11,12 +11,12 @@
com.facebook
ktfmt-parent
- 0.45-SNAPSHOT
+ 0.47-SNAPSHOT
0.10.1
- 1.6.10
+ 1.8.22
true
1.8
com.facebook.ktfmt.cli.Main
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 814c3db8..4e0debd4 100644
--- a/core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt
+++ b/core/src/main/java/com/facebook/ktfmt/format/KotlinInputAstVisitor.kt
@@ -52,6 +52,7 @@ import org.jetbrains.kotlin.psi.KtCollectionLiteralExpression
import org.jetbrains.kotlin.psi.KtConstantExpression
import org.jetbrains.kotlin.psi.KtConstructorDelegationCall
import org.jetbrains.kotlin.psi.KtContainerNode
+import org.jetbrains.kotlin.psi.KtContextReceiverList
import org.jetbrains.kotlin.psi.KtContinueExpression
import org.jetbrains.kotlin.psi.KtDelegatedSuperTypeEntry
import org.jetbrains.kotlin.psi.KtDestructuringDeclaration
@@ -94,6 +95,7 @@ import org.jetbrains.kotlin.psi.KtQualifiedExpression
import org.jetbrains.kotlin.psi.KtReferenceExpression
import org.jetbrains.kotlin.psi.KtReturnExpression
import org.jetbrains.kotlin.psi.KtScript
+import org.jetbrains.kotlin.psi.KtScriptInitializer
import org.jetbrains.kotlin.psi.KtSecondaryConstructor
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
@@ -121,9 +123,12 @@ 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.getNextSiblingIgnoringWhitespace
import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespace
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.psi.psiUtil.startsWithComment
+import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes
+import org.jetbrains.kotlin.psi.stubs.impl.KotlinPlaceHolderStubImpl
/** An AST visitor that builds a stream of {@link Op}s to format. */
class KotlinInputAstVisitor(
@@ -162,16 +167,17 @@ class KotlinInputAstVisitor(
builder.sync(function)
builder.block(ZERO) {
visitFunctionLikeExpression(
- function.modifierList,
- "fun",
- function.typeParameterList,
- function.receiverTypeReference,
- function.nameIdentifier?.text,
- true,
- function.valueParameterList,
- function.typeConstraintList,
- function.bodyBlockExpression ?: function.bodyExpression,
- function.typeReference,
+ contextReceiverList =
+ function.getStubOrPsiChild(KtStubElementTypes.CONTEXT_RECEIVER_LIST),
+ modifierList = function.modifierList,
+ keyword = "fun",
+ typeParameters = function.typeParameterList,
+ receiverTypeReference = function.receiverTypeReference,
+ name = function.nameIdentifier?.text,
+ parameterList = function.valueParameterList,
+ typeConstraintList = function.typeConstraintList,
+ bodyExpression = function.bodyBlockExpression ?: function.bodyExpression,
+ typeOrDelegationCall = function.typeReference,
)
}
}
@@ -282,22 +288,40 @@ class KotlinInputAstVisitor(
* list of supertypes.
*/
private fun visitFunctionLikeExpression(
+ contextReceiverList: KtContextReceiverList?,
modifierList: KtModifierList?,
- keyword: String,
+ keyword: String?,
typeParameters: KtTypeParameterList?,
receiverTypeReference: KtTypeReference?,
name: String?,
- emitParenthesis: Boolean,
parameterList: KtParameterList?,
typeConstraintList: KtTypeConstraintList?,
bodyExpression: KtExpression?,
typeOrDelegationCall: KtElement?,
) {
- builder.block(ZERO) {
+ fun emitTypeOrDelegationCall(block: () -> Unit) {
+ if (typeOrDelegationCall != null) {
+ builder.block(ZERO) {
+ if (typeOrDelegationCall is KtConstructorDelegationCall) {
+ builder.space()
+ }
+ builder.token(":")
+ block()
+ }
+ }
+ }
+
+ val forceTrailingBreak = name != null
+ builder.block(ZERO, isEnabled = forceTrailingBreak) {
+ if (contextReceiverList != null) {
+ visitContextReceiverList(contextReceiverList)
+ }
if (modifierList != null) {
visitModifierList(modifierList)
}
- builder.token(keyword)
+ if (keyword != null) {
+ builder.token(keyword)
+ }
if (typeParameters != null) {
builder.space()
builder.block(ZERO) { visit(typeParameters) }
@@ -316,46 +340,35 @@ class KotlinInputAstVisitor(
builder.token(name)
}
}
- if (emitParenthesis) {
- builder.token("(")
- }
- var paramBlockNeedsClosing = false
- builder.block(ZERO) {
- if (parameterList != null && parameterList.parameters.isNotEmpty()) {
- paramBlockNeedsClosing = true
- builder.open(expressionBreakIndent)
- builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakIndent)
- visit(parameterList)
- }
- if (emitParenthesis) {
- if (parameterList != null && parameterList.parameters.isNotEmpty()) {
- builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakNegativeIndent)
- }
+
+ if (parameterList != null && parameterList.hasEmptyParens()) {
+ builder.block(ZERO) {
+ builder.token("(")
builder.token(")")
- } else {
- if (paramBlockNeedsClosing) {
- builder.close()
+ emitTypeOrDelegationCall {
+ builder.breakOp(Doc.FillMode.INDEPENDENT, " ", expressionBreakIndent)
+ builder.block(expressionBreakIndent) { visit(typeOrDelegationCall) }
}
}
- if (typeOrDelegationCall != null) {
- builder.block(ZERO) {
- if (typeOrDelegationCall is KtConstructorDelegationCall) {
- builder.space()
- }
- builder.token(":")
- if (parameterList?.parameters.isNullOrEmpty()) {
- builder.breakOp(Doc.FillMode.INDEPENDENT, " ", expressionBreakIndent)
- builder.block(expressionBreakIndent) { visit(typeOrDelegationCall) }
- } else {
- builder.space()
- builder.block(expressionBreakNegativeIndent) { visit(typeOrDelegationCall) }
- }
+ } else {
+ builder.block(expressionBreakIndent) {
+ if (parameterList != null) {
+ visitEachCommaSeparated(
+ list = parameterList.parameters,
+ hasTrailingComma = parameterList.trailingComma != null,
+ prefix = "(",
+ postfix = ")",
+ wrapInBlock = false,
+ breakBeforePostfix = true,
+ )
+ }
+ emitTypeOrDelegationCall {
+ builder.space()
+ builder.block(expressionBreakNegativeIndent) { visit(typeOrDelegationCall) }
}
}
}
- if (paramBlockNeedsClosing) {
- builder.close()
- }
+
if (typeConstraintList != null) {
builder.space()
visit(typeConstraintList)
@@ -379,7 +392,7 @@ class KotlinInputAstVisitor(
}
builder.guessToken(";")
}
- if (name != null) {
+ if (forceTrailingBreak) {
builder.forcedBreak()
}
}
@@ -718,6 +731,20 @@ class KotlinInputAstVisitor(
return extractCallExpression(this)?.lambdaArguments?.isNotEmpty() ?: false
}
+ /** Does this list have parens with only whitespace between them? */
+ private fun KtParameterList.hasEmptyParens(): Boolean {
+ val left = this.leftParenthesis ?: return false
+ val right = this.rightParenthesis ?: return false
+ return left.getNextSiblingIgnoringWhitespace() == right
+ }
+
+ /** Does this list have parens with only whitespace between them? */
+ private fun KtValueArgumentList.hasEmptyParens(): Boolean {
+ val left = this.leftParenthesis ?: return false
+ val right = this.rightParenthesis ?: return false
+ return left.getNextSiblingIgnoringWhitespace() == right
+ }
+
/**
* emitQualifiedExpression formats call expressions that are either part of a qualified
* expression, or standing alone. This method makes it easier to handle both cases uniformly.
@@ -803,26 +830,26 @@ class KotlinInputAstVisitor(
arguments.first().getArgumentExpression() is KtLambdaExpression &&
arguments.first().getArgumentName() == null
val hasTrailingComma = list.trailingComma != null
+ val hasEmptyParens = list.hasEmptyParens()
val wrapInBlock: Boolean
val breakBeforePostfix: Boolean
val leadingBreak: Boolean
val breakAfterPrefix: Boolean
-
if (isSingleUnnamedLambda) {
wrapInBlock = true
breakBeforePostfix = false
- leadingBreak = arguments.isNotEmpty() && hasTrailingComma
+ leadingBreak = !hasEmptyParens && hasTrailingComma
breakAfterPrefix = false
} else {
wrapInBlock = !isGoogleStyle
- breakBeforePostfix = isGoogleStyle && arguments.isNotEmpty()
- leadingBreak = arguments.isNotEmpty()
- breakAfterPrefix = arguments.isNotEmpty()
+ breakBeforePostfix = isGoogleStyle && !hasEmptyParens
+ leadingBreak = !hasEmptyParens
+ breakAfterPrefix = !hasEmptyParens
}
return visitEachCommaSeparated(
- list.arguments,
+ arguments,
hasTrailingComma,
wrapInBlock = wrapInBlock,
breakBeforePostfix = breakBeforePostfix,
@@ -1199,20 +1226,31 @@ class KotlinInputAstVisitor(
val leftMostExpression = parts.first()
visit(leftMostExpression.left)
for (leftExpression in parts) {
- when (leftExpression.operationToken) {
- KtTokens.RANGE -> {}
- KtTokens.ELVIS -> builder.breakOp(Doc.FillMode.INDEPENDENT, " ", expressionBreakIndent)
- else -> builder.space()
- }
- builder.token(leftExpression.operationReference.text)
val isFirst = leftExpression === leftMostExpression
- if (isFirst) {
- builder.open(expressionBreakIndent)
- }
+
when (leftExpression.operationToken) {
- KtTokens.RANGE -> {}
- KtTokens.ELVIS -> builder.space()
- else -> builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO)
+ KtTokens.RANGE -> {
+ if (isFirst) {
+ builder.open(expressionBreakIndent)
+ }
+ builder.token(leftExpression.operationReference.text)
+ }
+ KtTokens.ELVIS -> {
+ if (isFirst) {
+ builder.open(expressionBreakIndent)
+ }
+ builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO)
+ builder.token(leftExpression.operationReference.text)
+ builder.space()
+ }
+ else -> {
+ builder.space()
+ if (isFirst) {
+ builder.open(expressionBreakIndent)
+ }
+ builder.token(leftExpression.operationReference.text)
+ builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO)
+ }
}
visit(leftExpression.right)
}
@@ -1372,16 +1410,16 @@ class KotlinInputAstVisitor(
builder.block(ZERO) {
visitFunctionLikeExpression(
- accessor.modifierList,
- accessor.namePlaceholder.text,
- null,
- null,
- null,
- accessor.bodyExpression != null || accessor.bodyBlockExpression != null,
- accessor.parameterList,
- null,
- accessor.bodyBlockExpression ?: accessor.bodyExpression,
- accessor.returnTypeReference,
+ contextReceiverList = null,
+ modifierList = accessor.modifierList,
+ keyword = accessor.namePlaceholder.text,
+ typeParameters = null,
+ receiverTypeReference = null,
+ name = null,
+ parameterList = getParameterListWithBugFixes(accessor),
+ typeConstraintList = null,
+ bodyExpression = accessor.bodyBlockExpression ?: accessor.bodyExpression,
+ typeOrDelegationCall = accessor.returnTypeReference,
)
}
}
@@ -1397,6 +1435,33 @@ class KotlinInputAstVisitor(
return 0
}
+ // Bug in Kotlin 1.9.10: KtProperyAccessor is the direct parent of the left and right paren
+ // elements. Also parameterList is always null for getters. As a workaround, we create our own
+ // fake KtParameterList.
+ private fun getParameterListWithBugFixes(accessor: KtPropertyAccessor): KtParameterList? {
+ if (accessor.bodyExpression == null && accessor.bodyBlockExpression == null) return null
+
+ return object :
+ KtParameterList(
+ KotlinPlaceHolderStubImpl(accessor.stub, KtStubElementTypes.VALUE_PARAMETER_LIST)) {
+ override fun getParameters(): List {
+ return accessor.valueParameters
+ }
+
+ override fun getTrailingComma(): PsiElement? {
+ return accessor.parameterList?.trailingComma
+ }
+
+ override fun getLeftParenthesis(): PsiElement? {
+ return accessor.leftParenthesis
+ }
+
+ override fun getRightParenthesis(): PsiElement? {
+ return accessor.rightParenthesis
+ }
+ }
+ }
+
/**
* Returns whether an expression is a lambda or initializer expression in which case we will want
* to avoid indenting the lambda block
@@ -1461,8 +1526,13 @@ class KotlinInputAstVisitor(
override fun visitClassOrObject(classOrObject: KtClassOrObject) {
builder.sync(classOrObject)
+ val contextReceiverList =
+ classOrObject.getStubOrPsiChild(KtStubElementTypes.CONTEXT_RECEIVER_LIST)
val modifierList = classOrObject.modifierList
builder.block(ZERO) {
+ if (contextReceiverList != null) {
+ visitContextReceiverList(contextReceiverList)
+ }
if (modifierList != null) {
visitModifierList(modifierList)
}
@@ -1506,44 +1576,42 @@ class KotlinInputAstVisitor(
builder.sync(constructor)
builder.block(ZERO) {
if (constructor.hasConstructorKeyword()) {
- builder.open(ZERO)
builder.breakOp(Doc.FillMode.UNIFIED, " ", ZERO)
- visit(constructor.modifierList)
- builder.token("constructor")
- }
-
- builder.block(ZERO) {
- builder.token("(")
- builder.block(expressionBreakIndent) {
- builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakIndent)
- visit(constructor.valueParameterList)
- builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakNegativeIndent)
- if (constructor.hasConstructorKeyword()) {
- builder.close()
- }
- }
- builder.token(")")
}
+ visitFunctionLikeExpression(
+ contextReceiverList = null,
+ modifierList = constructor.modifierList,
+ keyword = if (constructor.hasConstructorKeyword()) "constructor" else null,
+ typeParameters = null,
+ receiverTypeReference = null,
+ name = null,
+ parameterList = constructor.valueParameterList,
+ typeConstraintList = null,
+ bodyExpression = constructor.bodyExpression,
+ typeOrDelegationCall = null,
+ )
}
}
/** Example `private constructor(n: Int) : this(4, 5) { ... }` inside a class's body */
override fun visitSecondaryConstructor(constructor: KtSecondaryConstructor) {
builder.sync(constructor)
-
- val delegationCall = constructor.getDelegationCall()
- visitFunctionLikeExpression(
- constructor.modifierList,
- "constructor",
- null,
- null,
- null,
- true,
- constructor.valueParameterList,
- null,
- constructor.bodyExpression,
- if (!delegationCall.isImplicit) delegationCall else null,
- )
+ builder.block(ZERO) {
+ val delegationCall = constructor.getDelegationCall()
+ visitFunctionLikeExpression(
+ contextReceiverList =
+ constructor.getStubOrPsiChild(KtStubElementTypes.CONTEXT_RECEIVER_LIST),
+ modifierList = constructor.modifierList,
+ keyword = "constructor",
+ typeParameters = null,
+ receiverTypeReference = null,
+ name = null,
+ parameterList = constructor.valueParameterList,
+ typeConstraintList = null,
+ bodyExpression = constructor.bodyExpression,
+ typeOrDelegationCall = if (!delegationCall.isImplicit) delegationCall else null,
+ )
+ }
}
override fun visitConstructorDelegationCall(call: KtConstructorDelegationCall) {
@@ -1638,6 +1706,20 @@ class KotlinInputAstVisitor(
builder.forcedBreak()
}
+ /** Example `context(Logger, Raise)` */
+ override fun visitContextReceiverList(contextReceiverList: KtContextReceiverList) {
+ builder.sync(contextReceiverList)
+ builder.token("context")
+ visitEachCommaSeparated(
+ contextReceiverList.contextReceivers(),
+ prefix = "(",
+ postfix = ")",
+ breakAfterPrefix = false,
+ breakBeforePostfix = false,
+ )
+ builder.forcedBreak()
+ }
+
/** For example `@Magic private final` */
override fun visitModifierList(list: KtModifierList) {
builder.sync(list)
@@ -2057,17 +2139,14 @@ class KotlinInputAstVisitor(
/** Example `` */
override fun visitTypeParameterList(list: KtTypeParameterList) {
builder.sync(list)
- builder.block(ZERO) {
- builder.token("<")
- val parameters = list.parameters
- if (parameters.isNotEmpty()) {
- // Break before args.
- builder.breakOp(Doc.FillMode.UNIFIED, "", expressionBreakIndent)
- builder.block(expressionBreakIndent) {
- visitEachCommaSeparated(list.parameters, list.trailingComma != null, wrapInBlock = true)
- }
- }
- builder.token(">")
+ builder.block(expressionBreakIndent) {
+ visitEachCommaSeparated(
+ list = list.parameters,
+ hasTrailingComma = list.trailingComma != null,
+ prefix = "<",
+ postfix = ">",
+ wrapInBlock = !isGoogleStyle,
+ )
}
}
@@ -2386,7 +2465,7 @@ class KotlinInputAstVisitor(
* @throws FormattingError
*/
override fun visitElement(element: PsiElement) {
- inExpression.addLast(element is KtExpression || inExpression.peekLast())
+ inExpression.addLast(element is KtExpression || inExpression.last())
val previous = builder.depth()
try {
super.visitElement(element)
@@ -2413,7 +2492,7 @@ class KotlinInputAstVisitor(
builder.blankLineWanted(
when {
isFirst -> OpsBuilder.BlankLineWanted.NO
- child is PsiComment -> OpsBuilder.BlankLineWanted.NO
+ child is PsiComment -> continue
child is KtScript && importListEmpty -> OpsBuilder.BlankLineWanted.PRESERVE
else -> OpsBuilder.BlankLineWanted.YES
})
@@ -2427,6 +2506,7 @@ class KotlinInputAstVisitor(
override fun visitScript(script: KtScript) {
markForPartialFormat()
var lastChildHadBlankLineBefore = false
+ var lastChildIsContextReceiver = false
var first = true
for (child in script.blockExpression.children) {
if (child.text.isBlank()) {
@@ -2436,6 +2516,8 @@ class KotlinInputAstVisitor(
val childGetsBlankLineBefore = child !is KtProperty
if (first) {
builder.blankLineWanted(OpsBuilder.BlankLineWanted.PRESERVE)
+ } else if (lastChildIsContextReceiver) {
+ builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO)
} else if (child !is PsiComment &&
(childGetsBlankLineBefore || lastChildHadBlankLineBefore)) {
builder.blankLineWanted(OpsBuilder.BlankLineWanted.YES)
@@ -2443,15 +2525,14 @@ class KotlinInputAstVisitor(
visit(child)
builder.guessToken(";")
lastChildHadBlankLineBefore = childGetsBlankLineBefore
+ lastChildIsContextReceiver =
+ child is KtScriptInitializer &&
+ child.firstChild?.firstChild?.firstChild?.text == "context"
first = false
}
markForPartialFormat()
}
- private fun inExpression(): Boolean {
- return inExpression.peekLast()
- }
-
/**
* markForPartialFormat is used to delineate the smallest areas of code that must be formatted
* together.
@@ -2460,7 +2541,7 @@ class KotlinInputAstVisitor(
* covered by an area marked by this method.
*/
private fun markForPartialFormat() {
- if (!inExpression()) {
+ if (!inExpression.last()) {
builder.markForPartialFormat()
}
}
diff --git a/core/src/main/java/com/facebook/ktfmt/format/RedundantImportDetector.kt b/core/src/main/java/com/facebook/ktfmt/format/RedundantImportDetector.kt
index 7efa893e..ba285f6d 100644
--- a/core/src/main/java/com/facebook/ktfmt/format/RedundantImportDetector.kt
+++ b/core/src/main/java/com/facebook/ktfmt/format/RedundantImportDetector.kt
@@ -112,11 +112,10 @@ internal class RedundantImportDetector(val enabled: Boolean) {
importCleanUpCandidates =
importList.imports
.filter { import ->
+ val identifier = import.identifier ?: return@filter false
import.isValidImport &&
- !import.isAllUnder &&
- import.identifier != null &&
- requireNotNull(import.identifier) !in OPERATORS &&
- !COMPONENT_OPERATOR_REGEX.matches(import.identifier.orEmpty())
+ identifier !in OPERATORS &&
+ !COMPONENT_OPERATOR_REGEX.matches(identifier)
}
.toSet()
@@ -160,20 +159,20 @@ internal class RedundantImportDetector(val enabled: Boolean) {
fun getRedundantImportElements(): List {
if (!enabled) return emptyList()
- val redundantImports = mutableListOf()
+ val identifierCounts =
+ importCleanUpCandidates.groupBy { it.identifier }.mapValues { it.value.size }
- // Collect unused imports
- for (import in importCleanUpCandidates) {
- val isUnused = import.aliasName !in usedReferences && import.identifier !in usedReferences
- val isFromSamePackage = import.importedFqName?.parent() == thisPackage && import.alias == null
- if (isUnused || isFromSamePackage) {
- redundantImports += import
- }
+ return importCleanUpCandidates.filter {
+ val isUsed = it.identifier in usedReferences
+ val isFromThisPackage = it.importedFqName?.parent() == thisPackage
+ val hasAlias = it.alias != null
+ val isOverload = requireNotNull(identifierCounts[it.identifier]) > 1
+ // Remove if...
+ !isUsed || (isFromThisPackage && !hasAlias && !isOverload)
}
-
- return redundantImports
}
+ /** The imported short name, possibly an alias name, if any. */
private inline val KtImportDirective.identifier: String?
get() = importPath?.importedName?.identifier?.trim('`')
}
diff --git a/core/src/main/java/com/facebook/ktfmt/format/TypeNameClassifier.kt b/core/src/main/java/com/facebook/ktfmt/format/TypeNameClassifier.kt
deleted file mode 100644
index 76fac3e2..00000000
--- a/core/src/main/java/com/facebook/ktfmt/format/TypeNameClassifier.kt
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Copyright 2015 Google Inc.
- *
- * 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 converted to Kotlin,
-// because the original is package-private.
-
-package com.facebook.ktfmt.format
-
-import com.google.common.base.Verify
-import java.util.Optional
-
-/** Heuristics for classifying qualified names as types. */
-object TypeNameClassifier {
-
- /** A state machine for classifying qualified names. */
- private enum class TyParseState(val isSingleUnit: Boolean) {
-
- /** The start state. */
- START(false) {
- override fun next(n: JavaCaseFormat): TyParseState {
- return when (n) {
- JavaCaseFormat.UPPERCASE ->
- // if we see an UpperCamel later, assume this was a class
- // e.g. com.google.FOO.Bar
- AMBIGUOUS
- JavaCaseFormat.LOWER_CAMEL -> REJECT
- JavaCaseFormat.LOWERCASE ->
- // could be a package
- START
- JavaCaseFormat.UPPER_CAMEL -> TYPE
- }
- }
- },
-
- /** The current prefix is a type. */
- TYPE(true) {
- override fun next(n: JavaCaseFormat): TyParseState {
- return when (n) {
- JavaCaseFormat.UPPERCASE,
- JavaCaseFormat.LOWER_CAMEL,
- JavaCaseFormat.LOWERCASE -> FIRST_STATIC_MEMBER
- JavaCaseFormat.UPPER_CAMEL -> TYPE
- }
- }
- },
-
- /** The current prefix is a type, followed by a single static member access. */
- FIRST_STATIC_MEMBER(true) {
- override fun next(n: JavaCaseFormat): TyParseState {
- return REJECT
- }
- },
-
- /** Anything not represented by one of the other states. */
- REJECT(false) {
- override fun next(n: JavaCaseFormat): TyParseState {
- return REJECT
- }
- },
-
- /** An ambiguous type prefix. */
- AMBIGUOUS(false) {
- override fun next(n: JavaCaseFormat): TyParseState {
- return when (n) {
- JavaCaseFormat.UPPERCASE -> AMBIGUOUS
- JavaCaseFormat.LOWER_CAMEL,
- JavaCaseFormat.LOWERCASE -> REJECT
- JavaCaseFormat.UPPER_CAMEL -> TYPE
- }
- }
- };
-
- /** Transition function. */
- abstract fun next(n: JavaCaseFormat): TyParseState
- }
-
- /**
- * Returns the end index (inclusive) of the longest prefix that matches the naming conventions of
- * a type or static field access, or -1 if no such prefix was found.
- *
- * Examples:
- * * ClassName
- * * ClassName.staticMemberName
- * * com.google.ClassName.InnerClass.staticMemberName
- */
- internal fun typePrefixLength(nameParts: List): Optional {
- var state = TyParseState.START
- var typeLength = Optional.empty()
- for (i in nameParts.indices) {
- state = state.next(JavaCaseFormat.from(nameParts[i]))
- if (state === TyParseState.REJECT) {
- break
- }
- if (state.isSingleUnit) {
- typeLength = Optional.of(i)
- }
- }
- return typeLength
- }
-
- /** Case formats used in Java identifiers. */
- enum class JavaCaseFormat {
- UPPERCASE,
- LOWERCASE,
- UPPER_CAMEL,
- LOWER_CAMEL;
-
- companion object {
-
- /** Classifies an identifier's case format. */
- internal fun from(name: String): JavaCaseFormat {
- Verify.verify(name.isNotEmpty())
- var firstUppercase = false
- var hasUppercase = false
- var hasLowercase = false
- var first = true
- for (char in name) {
- if (!Character.isAlphabetic(char.code)) {
- continue
- }
- if (first) {
- firstUppercase = Character.isUpperCase(char)
- first = false
- }
- hasUppercase = hasUppercase or Character.isUpperCase(char)
- hasLowercase = hasLowercase or Character.isLowerCase(char)
- }
- return if (firstUppercase) {
- if (hasLowercase) UPPER_CAMEL else UPPERCASE
- } else {
- if (hasUppercase) LOWER_CAMEL else LOWERCASE
- }
- }
- }
- }
-}
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 928a7868..3d26e8d4 100644
--- a/core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphListBuilder.kt
+++ b/core/src/main/java/com/facebook/ktfmt/kdoc/ParagraphListBuilder.kt
@@ -232,7 +232,7 @@ class ParagraphListBuilder(
}
if (lineWithIndentation.startsWith(" ") && // markdown preformatted text
- (i == 1 || lineContent(lines[i - 2]).isBlank()) && // we've already ++'ed i above
+ (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 >=
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 af193bb4..082fdddf 100644
--- a/core/src/test/java/com/facebook/ktfmt/format/FormatterTest.kt
+++ b/core/src/test/java/com/facebook/ktfmt/format/FormatterTest.kt
@@ -1179,31 +1179,121 @@ class FormatterTest {
}
@Test
- fun `imports from the same package are removed`() {
+ fun `used imports from this package are removed`() {
val code =
"""
- |package com.example
- |
- |import com.example.Sample
- |import com.example.Sample.CONSTANT
- |import com.example.a.foo
- |
- |fun test() {
- | foo(CONSTANT, Sample())
- |}
- |"""
+ |package com.example
+ |
+ |import com.example.Sample
+ |import com.example.Sample.CONSTANT
+ |import com.example.a.foo
+ |
+ |fun test() {
+ | foo(CONSTANT, Sample())
+ |}
+ |"""
.trimMargin()
val expected =
"""
- |package com.example
- |
- |import com.example.Sample.CONSTANT
- |import com.example.a.foo
- |
- |fun test() {
- | foo(CONSTANT, Sample())
- |}
- |"""
+ |package com.example
+ |
+ |import com.example.Sample.CONSTANT
+ |import com.example.a.foo
+ |
+ |fun test() {
+ | foo(CONSTANT, Sample())
+ |}
+ |"""
+ .trimMargin()
+ assertThatFormatting(code).isEqualTo(expected)
+ }
+
+ @Test
+ fun `potentially unused imports from this package are kept if they are overloaded`() {
+ val code =
+ """
+ |package com.example
+ |
+ |import com.example.a
+ |import com.example.b
+ |import com.example.c
+ |import com.notexample.a
+ |import com.notexample.b
+ |import com.notexample.notC as c
+ |
+ |fun test() {
+ | a("hello")
+ | c("hello")
+ |}
+ |"""
+ .trimMargin()
+ val expected =
+ """
+ |package com.example
+ |
+ |import com.example.a
+ |import com.example.c
+ |import com.notexample.a
+ |import com.notexample.notC as c
+ |
+ |fun test() {
+ | a("hello")
+ | c("hello")
+ |}
+ |"""
+ .trimMargin()
+ assertThatFormatting(code).isEqualTo(expected)
+ }
+
+ @Test
+ fun `used imports from this package are kept if they are aliased`() {
+ val code =
+ """
+ |package com.example
+ |
+ |import com.example.b as a
+ |import com.example.c
+ |
+ |fun test() {
+ | a("hello")
+ |}
+ |"""
+ .trimMargin()
+ val expected =
+ """
+ |package com.example
+ |
+ |import com.example.b as a
+ |
+ |fun test() {
+ | a("hello")
+ |}
+ |"""
+ .trimMargin()
+ assertThatFormatting(code).isEqualTo(expected)
+ }
+
+ @Test
+ fun `unused imports are computed using only the alias name if present`() {
+ val code =
+ """
+ |package com.example
+ |
+ |import com.notexample.a as b
+ |
+ |fun test() {
+ | a("hello")
+ |}
+ |"""
+ .trimMargin()
+ val expected =
+ """
+ |package com.example
+ |
+ |fun test() {
+ | a("hello")
+ |}
+ |"""
.trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -1212,66 +1302,66 @@ class FormatterTest {
fun `keep import elements only mentioned in kdoc`() {
val code =
"""
- |package com.example.kdoc
- |
- |import com.example.Bar
- |import com.example.Example
- |import com.example.Foo
- |import com.example.JavaDocLink
- |import com.example.Param
- |import com.example.R
- |import com.example.ReturnedValue
- |import com.example.Sample
- |import com.example.unused
- |import com.example.exception.AnException
- |import com.example.kdoc.Doc
- |
- |/**
- | * [Foo] is something only mentioned here, just like [R.layout.test] and [Doc].
- | *
- | * Old {@link JavaDocLink} that gets removed.
- | *
- | * @throws AnException
- | * @exception Sample.SampleException
- | * @param unused [Param]
- | * @property JavaDocLink [Param]
- | * @return [Unit] as [ReturnedValue]
- | * @sample Example
- | * @see Bar for more info
- | * @throws AnException
- | */
- |class Dummy
- |"""
+ |package com.example.kdoc
+ |
+ |import com.example.Bar
+ |import com.example.Example
+ |import com.example.Foo
+ |import com.example.JavaDocLink
+ |import com.example.Param
+ |import com.example.R
+ |import com.example.ReturnedValue
+ |import com.example.Sample
+ |import com.example.unused
+ |import com.example.exception.AnException
+ |import com.example.kdoc.Doc
+ |
+ |/**
+ | * [Foo] is something only mentioned here, just like [R.layout.test] and [Doc].
+ | *
+ | * Old {@link JavaDocLink} that gets removed.
+ | *
+ | * @throws AnException
+ | * @exception Sample.SampleException
+ | * @param unused [Param]
+ | * @property JavaDocLink [Param]
+ | * @return [Unit] as [ReturnedValue]
+ | * @sample Example
+ | * @see Bar for more info
+ | * @throws AnException
+ | */
+ |class Dummy
+ |"""
.trimMargin()
val expected =
"""
- |package com.example.kdoc
- |
- |import com.example.Bar
- |import com.example.Example
- |import com.example.Foo
- |import com.example.Param
- |import com.example.R
- |import com.example.ReturnedValue
- |import com.example.Sample
- |import com.example.exception.AnException
- |
- |/**
- | * [Foo] is something only mentioned here, just like [R.layout.test] and [Doc].
- | *
- | * Old {@link JavaDocLink} that gets removed.
- | *
- | * @param unused [Param]
- | * @return [Unit] as [ReturnedValue]
- | * @property JavaDocLink [Param]
- | * @throws AnException
- | * @throws AnException
- | * @exception Sample.SampleException
- | * @sample Example
- | * @see Bar for more info
- | */
- |class Dummy
- |"""
+ |package com.example.kdoc
+ |
+ |import com.example.Bar
+ |import com.example.Example
+ |import com.example.Foo
+ |import com.example.Param
+ |import com.example.R
+ |import com.example.ReturnedValue
+ |import com.example.Sample
+ |import com.example.exception.AnException
+ |
+ |/**
+ | * [Foo] is something only mentioned here, just like [R.layout.test] and [Doc].
+ | *
+ | * Old {@link JavaDocLink} that gets removed.
+ | *
+ | * @param unused [Param]
+ | * @return [Unit] as [ReturnedValue]
+ | * @property JavaDocLink [Param]
+ | * @throws AnException
+ | * @throws AnException
+ | * @exception Sample.SampleException
+ | * @sample Example
+ | * @see Bar for more info
+ | */
+ |class Dummy
+ |"""
.trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -1280,15 +1370,15 @@ class FormatterTest {
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)
- |"""
+ |import com.shopping.Bag
+ |
+ |/**
+ | * Some summary.
+ | *
+ | * @param count you can fit this many in a [Bag]
+ | */
+ |fun fetchBananas(count: Int)
+ |"""
.trimMargin())
}
@@ -1296,16 +1386,16 @@ class FormatterTest {
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)
- |"""
+ |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())
}
@@ -1313,69 +1403,69 @@ class FormatterTest {
fun `keep component imports`() =
assertFormatted(
"""
- |import com.example.component1
- |import com.example.component10
- |import com.example.component120
- |import com.example.component2
- |import com.example.component3
- |import com.example.component4
- |import com.example.component5
- |"""
+ |import com.example.component1
+ |import com.example.component10
+ |import com.example.component120
+ |import com.example.component2
+ |import com.example.component3
+ |import com.example.component4
+ |import com.example.component5
+ |"""
.trimMargin())
@Test
fun `keep operator imports`() =
assertFormatted(
"""
- |import com.example.and
- |import com.example.compareTo
- |import com.example.contains
- |import com.example.dec
- |import com.example.div
- |import com.example.divAssign
- |import com.example.equals
- |import com.example.get
- |import com.example.getValue
- |import com.example.hasNext
- |import com.example.inc
- |import com.example.invoke
- |import com.example.iterator
- |import com.example.minus
- |import com.example.minusAssign
- |import com.example.mod
- |import com.example.modAssign
- |import com.example.next
- |import com.example.not
- |import com.example.or
- |import com.example.plus
- |import com.example.plusAssign
- |import com.example.provideDelegate
- |import com.example.rangeTo
- |import com.example.rem
- |import com.example.remAssign
- |import com.example.set
- |import com.example.setValue
- |import com.example.times
- |import com.example.timesAssign
- |import com.example.unaryMinus
- |import com.example.unaryPlus
- |"""
+ |import com.example.and
+ |import com.example.compareTo
+ |import com.example.contains
+ |import com.example.dec
+ |import com.example.div
+ |import com.example.divAssign
+ |import com.example.equals
+ |import com.example.get
+ |import com.example.getValue
+ |import com.example.hasNext
+ |import com.example.inc
+ |import com.example.invoke
+ |import com.example.iterator
+ |import com.example.minus
+ |import com.example.minusAssign
+ |import com.example.mod
+ |import com.example.modAssign
+ |import com.example.next
+ |import com.example.not
+ |import com.example.or
+ |import com.example.plus
+ |import com.example.plusAssign
+ |import com.example.provideDelegate
+ |import com.example.rangeTo
+ |import com.example.rem
+ |import com.example.remAssign
+ |import com.example.set
+ |import com.example.setValue
+ |import com.example.times
+ |import com.example.timesAssign
+ |import com.example.unaryMinus
+ |import com.example.unaryPlus
+ |"""
.trimMargin())
@Test
fun `keep unused imports when formatting options has feature turned off`() {
val code =
"""
- |import com.unused.FooBarBaz as Baz
- |import com.unused.Sample
- |import com.unused.a as `when`
- |import com.unused.a as wow
- |import com.unused.a.*
- |import com.unused.b as `if`
- |import com.unused.b as we
- |import com.unused.bar // test
- |import com.unused.`class`
- |"""
+ |import com.unused.FooBarBaz as Baz
+ |import com.unused.Sample
+ |import com.unused.a as `when`
+ |import com.unused.a as wow
+ |import com.unused.a.*
+ |import com.unused.b as `if`
+ |import com.unused.b as we
+ |import com.unused.bar // test
+ |import com.unused.`class`
+ |"""
.trimMargin()
assertThatFormatting(code)
@@ -1387,34 +1477,34 @@ class FormatterTest {
fun `comments between imports are moved above import list`() {
val code =
"""
- |package com.facebook.ktfmt
- |
- |/* leading comment */
- |import com.example.abc
- |/* internal comment 1 */
- |import com.example.bcd
- |// internal comment 2
- |import com.example.Sample
- |// trailing comment
- |
- |val x = Sample(abc, bcd)
- |"""
+ |package com.facebook.ktfmt
+ |
+ |/* leading comment */
+ |import com.example.abc
+ |/* internal comment 1 */
+ |import com.example.bcd
+ |// internal comment 2
+ |import com.example.Sample
+ |// trailing comment
+ |
+ |val x = Sample(abc, bcd)
+ |"""
.trimMargin()
val expected =
"""
- |package com.facebook.ktfmt
- |
- |/* leading comment */
- |/* internal comment 1 */
- |// internal comment 2
- |import com.example.Sample
- |import com.example.abc
- |import com.example.bcd
- |
- |// trailing comment
- |
- |val x = Sample(abc, bcd)
- |"""
+ |package com.facebook.ktfmt
+ |
+ |/* leading comment */
+ |/* internal comment 1 */
+ |// internal comment 2
+ |import com.example.Sample
+ |import com.example.abc
+ |import com.example.bcd
+ |
+ |// trailing comment
+ |
+ |val x = Sample(abc, bcd)
+ |"""
.trimMargin()
assertThatFormatting(code).isEqualTo(expected)
}
@@ -1423,12 +1513,12 @@ class FormatterTest {
fun `no redundant newlines when there are no imports`() =
assertFormatted(
"""
- |package foo123
- |
- |/*
- |bar
- |*/
- |"""
+ |package foo123
+ |
+ |/*
+ |bar
+ |*/
+ |"""
.trimMargin())
@Test
@@ -2161,6 +2251,7 @@ class FormatterTest {
fun `a few variations of constructors`() =
assertFormatted(
"""
+ |------------------------------------------------------
|class Foo constructor(number: Int) {}
|
|class Foo2 private constructor(number: Int) {}
@@ -2179,8 +2270,19 @@ class FormatterTest {
| number5: Int,
| number6: Int
|) {}
+ |
+ |class Foo6
+ |@Inject
+ |private constructor(hasSpaceForAnnos: Innnt) {
+ | // @Inject
+ |}
+ |
+ |class FooTooLongForCtorAndSupertypes
+ |@Inject
+ |private constructor(x: Int) : NoooooooSpaceForAnnos {}
|"""
- .trimMargin())
+ .trimMargin(),
+ deduceMaxWidth = true)
@Test
fun `a primary constructor without a class body `() =
@@ -3520,9 +3622,9 @@ class FormatterTest {
.trimMargin())
@Test
- fun `handle file annotations`() =
- assertFormatted(
- """
+ fun `handle file annotations`() {
+ assertFormatted(
+ """
|@file:JvmName("DifferentName")
|
|package com.somecompany.example
@@ -3533,7 +3635,53 @@ class FormatterTest {
| val a = example2("and 1")
|}
|"""
- .trimMargin())
+ .trimMargin())
+
+ assertFormatted(
+ """
+ |@file:JvmName("DifferentName") // Comment
+ |
+ |package com.somecompany.example
+ |
+ |import com.somecompany.example2
+ |
+ |class Foo {
+ | val a = example2("and 1")
+ |}
+ |"""
+ .trimMargin())
+
+ assertFormatted(
+ """
+ |@file:JvmName("DifferentName")
+ |
+ |// Comment
+ |
+ |package com.somecompany.example
+ |
+ |import com.somecompany.example2
+ |
+ |class Foo {
+ | val a = example2("and 1")
+ |}
+ |"""
+ .trimMargin())
+
+ assertFormatted(
+ """
+ |@file:JvmName("DifferentName")
+ |
+ |// Comment
+ |package com.somecompany.example
+ |
+ |import com.somecompany.example2
+ |
+ |class Foo {
+ | val a = example2("and 1")
+ |}
+ |"""
+ .trimMargin())
+ }
@Test
fun `handle init block`() =
@@ -4289,6 +4437,52 @@ class FormatterTest {
.trimMargin(),
deduceMaxWidth = true)
+ @Test
+ fun `chain of Elvis operator`() =
+ assertFormatted(
+ """
+ |---------------------------
+ |fun f() {
+ | return option1()
+ | ?: option2()
+ | ?: option3()
+ | ?: option4()
+ | ?: option5()
+ |}
+ |"""
+ .trimMargin(),
+ deduceMaxWidth = true)
+
+ @Test
+ fun `Elvis operator mixed with plus operator breaking on plus`() =
+ assertFormatted(
+ """
+ |------------------------
+ |fun f() {
+ | return option1()
+ | ?: option2() +
+ | option3()
+ | ?: option4() +
+ | option5()
+ |}
+ |"""
+ .trimMargin(),
+ deduceMaxWidth = true)
+
+ @Test
+ fun `Elvis operator mixed with plus operator breaking on elvis`() =
+ assertFormatted(
+ """
+ |---------------------------------
+ |fun f() {
+ | return option1()
+ | ?: option2() + option3()
+ | ?: option4() + option5()
+ |}
+ |"""
+ .trimMargin(),
+ deduceMaxWidth = true)
+
@Test
fun `handle comments in the middle of calling chain`() =
assertFormatted(
@@ -6674,6 +6868,168 @@ class FormatterTest {
assertThatFormatting(code).isEqualTo(expected)
}
+ @Test
+ fun `context receivers`() {
+ val code =
+ """
+ |context(Something)
+ |
+ |class A {
+ | context(
+ | // Test comment.
+ | Logger, Raise)
+ |
+ | @SomeAnnotation
+ |
+ | fun doNothing() {}
+ |
+ | context(SomethingElse)
+ |
+ | private class NestedClass {}
+ |}
+ |"""
+ .trimMargin()
+
+ val expected =
+ """
+ |context(Something)
+ |class A {
+ | context(
+ | // Test comment.
+ | Logger,
+ | Raise)
+ | @SomeAnnotation
+ | fun doNothing() {}
+ |
+ | context(SomethingElse)
+ | private class NestedClass {}
+ |}
+ |"""
+ .trimMargin()
+
+ assertThatFormatting(code).isEqualTo(expected)
+ }
+
+ @Test
+ fun `trailing comment after function in class`() =
+ assertFormatted(
+ """
+ |class Host {
+ | fun fooBlock() {
+ | return
+ | } // Trailing after fn
+ | // Hanging after fn
+ |
+ | // End of class
+ |}
+ |
+ |class Host {
+ | fun fooExpr() = 0 // Trailing after fn
+ | // Hanging after fn
+ |
+ | // End of class
+ |}
+ |
+ |class Host {
+ | constructor() {} // Trailing after fn
+ | // Hanging after fn
+ |
+ | // End of class
+ |}
+ |
+ |class Host
+ |// Primary constructor
+ |constructor() // Trailing after fn
+ | // Hanging after fn
+ |{
+ | // End of class
+ |}
+ |
+ |class Host {
+ | fun fooBlock() {
+ | return
+ | }
+ |
+ | // Between elements
+ |
+ | fun fooExpr() = 0
+ |
+ | // Between elements
+ |
+ | fun fooBlock() {
+ | return
+ | }
+ |}
+ |"""
+ .trimMargin())
+
+ @Test
+ fun `trailing comment after function top-level`() {
+ assertFormatted(
+ """
+ |fun fooBlock() {
+ | return
+ |} // Trailing after fn
+ |// Hanging after fn
+ |
+ |// End of file
+ |"""
+ .trimMargin())
+
+ assertFormatted(
+ """
+ |fun fooExpr() = 0 // Trailing after fn
+ |// Hanging after fn
+ |
+ |// End of file
+ |"""
+ .trimMargin())
+
+ assertFormatted(
+ """
+ |fun fooBlock() {
+ | return
+ |}
+ |
+ |// Between elements
+ |
+ |fun fooExpr() = 0
+ |
+ |// Between elements
+ |
+ |fun fooBlock() {
+ | return
+ |}
+ |"""
+ .trimMargin())
+ }
+
+ @Test
+ fun `line break on base class`() =
+ assertFormatted(
+ """
+ |---------------------------
+ |class Basket() :
+ | WovenObject {
+ | // some body
+ |}
+ |"""
+ .trimMargin(),
+ deduceMaxWidth = true)
+
+ @Test
+ fun `line break on type specifier`() =
+ assertFormatted(
+ """
+ |---------------------------
+ |class Basket() where
+ |T : Fruit {
+ | // some body
+ |}
+ |"""
+ .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 3d1c3ce6..c87e3718 100644
--- a/core/src/test/java/com/facebook/ktfmt/format/GoogleStyleFormatterKtTest.kt
+++ b/core/src/test/java/com/facebook/ktfmt/format/GoogleStyleFormatterKtTest.kt
@@ -105,7 +105,7 @@ class GoogleStyleFormatterKtTest {
}
@Test
- fun `class params are placed each in their own line`() =
+ fun `class value params are placed each in their own line`() =
assertFormatted(
"""
|-----------------------------------------
@@ -147,6 +147,58 @@ class GoogleStyleFormatterKtTest {
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
+ @Test
+ fun `class type params are placed each in their own line`() =
+ assertFormatted(
+ """
+ |------------------------------------
+ |class Foo<
+ | TypeA : Int,
+ | TypeC : String
+ |> {
+ | // Class name + type params too long for one line
+ | // Type params could fit on one line but break
+ |}
+ |
+ |class Foo<
+ | TypeA : Int,
+ | TypeB : Double,
+ | TypeC : String
+ |> {
+ | // Type params can't fit on one line
+ |}
+ |
+ |class Foo<
+ | TypeA : Int,
+ | TypeB : Double,
+ | TypeC : String
+ |>
+ |
+ |class Foo<
+ | TypeA : Int,
+ | TypeB : Double,
+ | TypeC : String
+ |>() {
+ | //
+ |}
+ |
+ |class Bi<
+ | TypeA : Int,
+ | TypeB : Double,
+ | TypeC : String
+ |>(a: Int, var b: Int, val c: Int) {
+ | // TODO: Breaking the type param list
+ | // should propagate to the value param list
+ |}
+ |
+ |class C {
+ | // Class name + type params fit on one line
+ |}
+ |"""
+ .trimMargin(),
+ formattingOptions = Formatter.GOOGLE_FORMAT,
+ deduceMaxWidth = true)
+
@Test
fun `function params are placed each in their own line`() =
assertFormatted(
@@ -1309,6 +1361,108 @@ class GoogleStyleFormatterKtTest {
formattingOptions = Formatter.GOOGLE_FORMAT,
deduceMaxWidth = true)
+ @Test
+ fun `leading and trailing comments in block-like lists`() =
+ assertFormatted(
+ """
+ |--------------------------------
+ |@Anno(
+ | array =
+ | [
+ | // Comment
+ | someItem
+ | // Comment
+ | ]
+ |)
+ |class Host(
+ | // Comment
+ | val someItem: Int
+ | // Comment
+ |) {
+ | constructor(
+ | // Comment
+ | someItem: Int
+ | // Comment
+ | ) : this(
+ | // Comment
+ | someItem
+ | // Comment
+ | )
+ |
+ | fun foo(
+ | // Comment
+ | someItem: Int
+ | // Comment
+ | ): Int {
+ | foo(
+ | // Comment
+ | someItem
+ | // Comment
+ | )
+ | }
+ |
+ | var x: Int = 0
+ | set(
+ | // Comment
+ | someItem: Int
+ | // Comment
+ | ) = Unit
+ |
+ | fun <
+ | // Comment
+ | someItem : Int
+ | // Comment
+ | > bar(): Int {
+ | bar<
+ | // Comment
+ | someItem
+ | // Comment
+ | >()
+ | }
+ |}
+ |"""
+ .trimMargin(),
+ formattingOptions = Formatter.GOOGLE_FORMAT,
+ deduceMaxWidth = true)
+
+ @Test
+ fun `comments in empty block-like lists`() =
+ assertFormatted(
+ """
+ |--------------------------------
+ |@Anno(
+ | array =
+ | [
+ | // Comment
+ | ]
+ |)
+ |class Host(
+ | // Comment
+ |) {
+ | constructor(
+ | // Comment
+ | ) : this(
+ | // Comment
+ | )
+ |
+ | val x: Int
+ | get(
+ | // Comment
+ | ) = 0
+ |
+ | fun foo(
+ | // Comment
+ | ): Int {
+ | foo(
+ | // Comment
+ | )
+ | }
+ |}
+ |"""
+ .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/format/TokenizerTest.kt b/core/src/test/java/com/facebook/ktfmt/format/TokenizerTest.kt
index 3f8fe71e..27b18377 100644
--- a/core/src/test/java/com/facebook/ktfmt/format/TokenizerTest.kt
+++ b/core/src/test/java/com/facebook/ktfmt/format/TokenizerTest.kt
@@ -110,4 +110,109 @@ class TokenizerTest {
.containsExactly(0, -1, 1, 2, 3, -1, 4, -1, 5, 6, 7)
.inOrder()
}
+
+ @Test
+ fun `Context receivers are parsed correctly`() {
+ val code =
+ """
+ |context(Something)
+ |class A {
+ | context(
+ | // Test comment.
+ | Logger, Raise)
+ | fun test() {}
+ |}
+ |"""
+ .trimMargin()
+ .trimMargin()
+
+ val file = Parser.parse(code)
+ val tokenizer = Tokenizer(code, file)
+ file.accept(tokenizer)
+
+ assertThat(tokenizer.toks.map { it.originalText })
+ .containsExactly(
+ "context",
+ "(",
+ "Something",
+ ")",
+ "\n",
+ "class",
+ " ",
+ "A",
+ " ",
+ "{",
+ "\n",
+ " ",
+ "context",
+ "(",
+ "\n",
+ " ",
+ "// Test comment.",
+ "\n",
+ " ",
+ "Logger",
+ ",",
+ " ",
+ "Raise",
+ "<",
+ "Error",
+ ">",
+ ")",
+ "\n",
+ " ",
+ "fun",
+ " ",
+ "test",
+ "(",
+ ")",
+ " ",
+ "{",
+ "}",
+ "\n",
+ "}")
+ .inOrder()
+ assertThat(tokenizer.toks.map { it.index })
+ .containsExactly(
+ 0,
+ 1,
+ 2,
+ 3,
+ -1,
+ 4,
+ -1,
+ 5,
+ -1,
+ 6,
+ -1,
+ -1,
+ 7,
+ 8,
+ -1,
+ -1,
+ 9,
+ -1,
+ -1,
+ 10,
+ 11,
+ -1,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ -1,
+ -1,
+ 17,
+ -1,
+ 18,
+ 19,
+ 20,
+ -1,
+ 21,
+ 22,
+ -1,
+ 23)
+ .inOrder()
+ }
}
diff --git a/docs/editorconfig/.editorconfig-kotlinlang b/docs/editorconfig/.editorconfig-kotlinlang
index 2f558675..85d6e057 100644
--- a/docs/editorconfig/.editorconfig-kotlinlang
+++ b/docs/editorconfig/.editorconfig-kotlinlang
@@ -42,10 +42,11 @@ ij_kotlin_continuation_indent_in_supertype_lists = false
ij_kotlin_else_on_new_line = false
ij_kotlin_enum_constants_wrap = off
ij_kotlin_extends_list_wrap = normal
-ij_kotlin_field_annotation_wrap = split_into_lines
+ij_kotlin_field_annotation_wrap = off
ij_kotlin_finally_on_new_line = false
ij_kotlin_if_rparen_on_new_line = false
ij_kotlin_import_nested_classes = false
+ij_kotlin_imports_layout = *
ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
ij_kotlin_keep_blank_lines_before_right_brace = 2
ij_kotlin_keep_blank_lines_in_code = 2
diff --git a/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtCodeStyleManager.java b/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtCodeStyleManager.java
index 223caaf8..427dc763 100644
--- a/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtCodeStyleManager.java
+++ b/ktfmt_idea_plugin/src/main/java/com/facebook/ktfmt/intellij/KtfmtCodeStyleManager.java
@@ -27,10 +27,13 @@
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
+import com.intellij.psi.codeStyle.ChangedRangesInfo;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.impl.CheckUtil;
import com.intellij.util.IncorrectOperationException;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
@@ -68,8 +71,18 @@ public void reformatText(PsiFile file, Collection extends TextRange> ranges)
}
@Override
- public void reformatTextWithContext(
- @NotNull PsiFile file, @NotNull Collection extends TextRange> ranges) {
+ public void reformatTextWithContext(@NotNull PsiFile file, @NotNull ChangedRangesInfo info)
+ throws IncorrectOperationException {
+ List ranges = new ArrayList<>();
+ if (info.insertedRanges != null) {
+ ranges.addAll(info.insertedRanges);
+ }
+ ranges.addAll(info.allChangedRanges);
+ reformatTextWithContext(file, ranges);
+ }
+
+ @Override
+ public void reformatTextWithContext(PsiFile file, Collection extends TextRange> ranges) {
if (overrideFormatterForFile(file)) {
formatInternal(file, ranges);
} else {
diff --git a/online_formatter/build.gradle.kts b/online_formatter/build.gradle.kts
index 140c3384..60d07a40 100644
--- a/online_formatter/build.gradle.kts
+++ b/online_formatter/build.gradle.kts
@@ -14,9 +14,7 @@
* limitations under the License.
*/
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-
-plugins { kotlin("jvm") version "1.5.0" }
+plugins { kotlin("jvm") version "1.8.22" }
repositories {
mavenLocal()
@@ -35,11 +33,11 @@ dependencies {
testImplementation(kotlin("test-junit"))
}
+kotlin { jvmToolchain(11) }
+
tasks {
test { useJUnit() }
- withType() { kotlinOptions.jvmTarget = "11" }
-
val packageFat by
creating(Zip::class) {
from(compileKotlin)
diff --git a/online_formatter/gradle/wrapper/gradle-wrapper.properties b/online_formatter/gradle/wrapper/gradle-wrapper.properties
index be52383e..db9a6b82 100644
--- a/online_formatter/gradle/wrapper/gradle-wrapper.properties
+++ b/online_formatter/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/pom.xml b/pom.xml
index 4fe51f1b..f1c1df7d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,11 +1,11 @@
-
4.0.0
com.facebook
ktfmt-parent
- 0.45-SNAPSHOT
+ 0.47-SNAPSHOT
pom
Ktfmt Parent
diff --git a/version.txt b/version.txt
index b443d321..d7bf00b2 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-0.45-SNAPSHOT
+0.47-SNAPSHOT
diff --git a/website/package-lock.json b/website/package-lock.json
index 43d1698b..90b7647f 100644
--- a/website/package-lock.json
+++ b/website/package-lock.json
@@ -5000,9 +5000,9 @@
"dev": true
},
"node_modules/word-wrap": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
- "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
+ "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
@@ -9174,9 +9174,9 @@
"dev": true
},
"word-wrap": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
- "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
+ "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true
},
"wrap-ansi": {