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 ranges) } @Override - public void reformatTextWithContext( - @NotNull PsiFile file, @NotNull Collection 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 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": {