diff --git a/idea/src/org/jetbrains/kotlin/idea/formatter/TrailingCommaHelper.kt b/idea/src/org/jetbrains/kotlin/idea/formatter/TrailingCommaHelper.kt new file mode 100644 index 00000000000..4f36133a4ea --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/formatter/TrailingCommaHelper.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.formatter + +import com.intellij.openapi.util.registry.Registry +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiErrorElement +import com.intellij.psi.PsiWhiteSpace +import com.intellij.psi.codeStyle.CodeStyleSettings +import com.intellij.psi.tree.TokenSet +import com.intellij.psi.util.PsiUtil.getElementType +import org.jetbrains.kotlin.config.LanguageFeature +import org.jetbrains.kotlin.idea.project.languageVersionSettings +import org.jetbrains.kotlin.idea.util.* +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespaceAndComments +import org.jetbrains.kotlin.psi.psiUtil.nextLeaf +import org.jetbrains.kotlin.psi.psiUtil.prevLeaf +import org.jetbrains.kotlin.psi.psiUtil.siblings +import org.jetbrains.kotlin.utils.addToStdlib.cast + +object TrailingCommaHelper { + fun findInvalidCommas(commaOwner: KtElement): List = commaOwner.firstChild + ?.siblings(withItself = false) + ?.filter { it.isComma } + ?.filter { + it.prevLeaf(true)?.isLineBreak() == true || it.leafIgnoringWhitespace(false) != it.leafIgnoringWhitespaceAndComments(false) + }?.toList().orEmpty() + + fun needComma( + commaOwner: KtElement, + settings: CodeStyleSettings?, + checkExistingTrailingComma: Boolean = true, + ): Boolean = when { + commaOwner is KtWhenEntry -> + commaOwner.needTrailingComma(settings, checkExistingTrailingComma) + + commaOwner.parent is KtFunctionLiteral -> + commaOwner.parent.cast().needTrailingComma(settings, checkExistingTrailingComma) + + commaOwner is KtDestructuringDeclaration -> + commaOwner.needTrailingComma(settings, checkExistingTrailingComma) + + else -> (checkExistingTrailingComma && + trailingCommaOrLastElement(commaOwner)?.isComma == true || + settings?.kotlinCustomSettings?.addTrailingCommaIsAllowedFor(commaOwner) != false) && + commaOwner.isMultiline() + } + + fun trailingCommaOrLastElement(commaOwner: KtElement): PsiElement? { + val lastChild = commaOwner.lastSignificantChild ?: return null + val withSelf = when (getElementType(lastChild)) { + KtTokens.COMMA -> return lastChild + in RIGHT_BARRIERS -> false + else -> true + } + + return lastChild.getPrevSiblingIgnoringWhitespaceAndComments(withSelf)?.takeIf { + getElementType(it) !in LEFT_BARRIERS + }?.takeIfIsNotError() + } + + fun trailingCommaAllowedInModule(source: PsiElement): Boolean = + Registry.`is`("kotlin.formatter.allowTrailingCommaInAnyProject", false) || + source.module?.languageVersionSettings?.supportsFeature(LanguageFeature.TrailingCommas) == true + + fun elementBeforeFirstElement(commaOwner: KtElement): PsiElement? = when (commaOwner) { + is KtParameterList -> { + val parent = commaOwner.parent + if (parent is KtFunctionLiteral) parent.lBrace else commaOwner.leftParenthesis + } + is KtWhenEntry -> commaOwner.parent.cast().openBrace + is KtDestructuringDeclaration -> commaOwner.lPar + else -> commaOwner.firstChild?.takeIfIsNotError() + } + + fun elementAfterLastElement(commaOwner: KtElement): PsiElement? = when (commaOwner) { + is KtParameterList -> { + val parent = commaOwner.parent + if (parent is KtFunctionLiteral) parent.arrow else commaOwner.rightParenthesis + } + is KtWhenEntry -> commaOwner.arrow + is KtDestructuringDeclaration -> commaOwner.rPar + else -> commaOwner.lastChild?.takeIfIsNotError() + } + + private fun PsiElement.takeIfIsNotError(): PsiElement? = takeIf { it !is PsiErrorElement } + + private val RIGHT_BARRIERS = TokenSet.create(KtTokens.RBRACKET, KtTokens.RPAR, KtTokens.RBRACE, KtTokens.GT, KtTokens.ARROW) + private val LEFT_BARRIERS = TokenSet.create(KtTokens.LBRACKET, KtTokens.LPAR, KtTokens.LBRACE, KtTokens.LT) + private val PsiElement.lastSignificantChild: PsiElement? + get() = when (this) { + is KtWhenEntry -> arrow + is KtDestructuringDeclaration -> rPar + else -> lastChild + } +} + +fun PsiElement.leafIgnoringWhitespace(forward: Boolean = true, skipEmptyElements: Boolean = true) = + leaf(forward) { (!skipEmptyElements || it.textLength != 0) && it !is PsiWhiteSpace } + +fun PsiElement.leafIgnoringWhitespaceAndComments(forward: Boolean = true, skipEmptyElements: Boolean = true) = + leaf(forward) { (!skipEmptyElements || it.textLength != 0) && it !is PsiWhiteSpace && it !is PsiComment } + +fun PsiElement.leaf(forward: Boolean = true, filter: (PsiElement) -> Boolean): PsiElement? = + if (forward) nextLeaf(filter) + else prevLeaf(filter) + +val PsiElement.isComma: Boolean get() = getElementType(this) == KtTokens.COMMA \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/formatter/TrailingCommaPostFormatProcessor.kt b/idea/src/org/jetbrains/kotlin/idea/formatter/TrailingCommaPostFormatProcessor.kt index 84dc66166fa..2eed2b597a8 100644 --- a/idea/src/org/jetbrains/kotlin/idea/formatter/TrailingCommaPostFormatProcessor.kt +++ b/idea/src/org/jetbrains/kotlin/idea/formatter/TrailingCommaPostFormatProcessor.kt @@ -7,26 +7,25 @@ package org.jetbrains.kotlin.idea.formatter import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.util.TextRange -import com.intellij.openapi.util.registry.Registry -import com.intellij.psi.* +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiWhiteSpace import com.intellij.psi.codeStyle.CodeStyleManager import com.intellij.psi.codeStyle.CodeStyleSettings import com.intellij.psi.impl.source.PostprocessReformattingAspect import com.intellij.psi.impl.source.codeStyle.PostFormatProcessor import com.intellij.psi.impl.source.codeStyle.PostFormatProcessorHelper -import com.intellij.psi.tree.TokenSet import com.intellij.psi.util.PsiUtil.getElementType -import org.jetbrains.kotlin.config.LanguageFeature -import org.jetbrains.kotlin.idea.formatter.TrailingCommaPostFormatProcessor.Companion.findInvalidCommas -import org.jetbrains.kotlin.idea.formatter.TrailingCommaPostFormatProcessor.Companion.needComma -import org.jetbrains.kotlin.idea.formatter.TrailingCommaPostFormatProcessor.Companion.trailingCommaAllowedInModule -import org.jetbrains.kotlin.idea.formatter.TrailingCommaPostFormatProcessor.Companion.trailingCommaOrLastElement -import org.jetbrains.kotlin.idea.project.languageVersionSettings -import org.jetbrains.kotlin.idea.util.* +import org.jetbrains.kotlin.idea.formatter.TrailingCommaHelper.findInvalidCommas +import org.jetbrains.kotlin.idea.formatter.TrailingCommaHelper.needComma +import org.jetbrains.kotlin.idea.formatter.TrailingCommaHelper.trailingCommaAllowedInModule +import org.jetbrains.kotlin.idea.formatter.TrailingCommaHelper.trailingCommaOrLastElement import org.jetbrains.kotlin.lexer.KtTokens -import org.jetbrains.kotlin.psi.* -import org.jetbrains.kotlin.psi.psiUtil.* -import org.jetbrains.kotlin.utils.addToStdlib.cast +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.psi.KtPsiFactory +import org.jetbrains.kotlin.psi.psiUtil.createSmartPointer +import org.jetbrains.kotlin.psi.psiUtil.siblings class TrailingCommaPostFormatProcessor : PostFormatProcessor { override fun processElement(source: PsiElement, settings: CodeStyleSettings): PsiElement = @@ -34,76 +33,8 @@ class TrailingCommaPostFormatProcessor : PostFormatProcessor { override fun processText(source: PsiFile, rangeToReformat: TextRange, settings: CodeStyleSettings): TextRange = TrailingCommaPostFormatVisitor(settings).processText(source, rangeToReformat) - - companion object { - fun findInvalidCommas(commaOwner: KtElement): List = commaOwner.firstChild - ?.siblings(withItself = false) - ?.filter { it.isComma } - ?.filter { - it.prevLeaf(true)?.isLineBreak() == true || it.leafIgnoringWhitespace(false) != it.leafIgnoringWhitespaceAndComments(false) - }?.toList().orEmpty() - - fun needComma( - commaOwner: KtElement, - settings: CodeStyleSettings?, - checkExistingTrailingComma: Boolean = true, - ): Boolean = when { - commaOwner is KtWhenEntry -> - commaOwner.needTrailingComma(settings, checkExistingTrailingComma) - - commaOwner.parent is KtFunctionLiteral -> - commaOwner.parent.cast().needTrailingComma(settings, checkExistingTrailingComma) - - commaOwner is KtDestructuringDeclaration -> - commaOwner.needTrailingComma(settings, checkExistingTrailingComma) - - else -> (checkExistingTrailingComma && - trailingCommaOrLastElement(commaOwner)?.isComma == true || - settings?.kotlinCustomSettings?.addTrailingCommaIsAllowedFor(commaOwner) != false) && - commaOwner.isMultiline() - } - - fun trailingCommaOrLastElement(commaOwner: KtElement): PsiElement? { - val lastChild = commaOwner.lastSignificantChild ?: return null - val withSelf = when (getElementType(lastChild)) { - KtTokens.COMMA -> return lastChild - in RIGHT_BARRIERS -> false - else -> true - } - - return lastChild.getPrevSiblingIgnoringWhitespaceAndComments(withSelf)?.takeIf { - getElementType(it) !in LEFT_BARRIERS - }?.takeIfIsNotError() - } - - fun trailingCommaAllowedInModule(source: PsiElement): Boolean = - Registry.`is`("kotlin.formatter.allowTrailingCommaInAnyProject", false) || - source.module?.languageVersionSettings?.supportsFeature(LanguageFeature.TrailingCommas) == true - - fun elementBeforeFirstElement(commaOwner: KtElement): PsiElement? = when (commaOwner) { - is KtParameterList -> { - val parent = commaOwner.parent - if (parent is KtFunctionLiteral) parent.lBrace else commaOwner.leftParenthesis - } - is KtWhenEntry -> commaOwner.parent.cast().openBrace - is KtDestructuringDeclaration -> commaOwner.lPar - else -> commaOwner.firstChild?.takeIfIsNotError() - } - - fun elementAfterLastElement(commaOwner: KtElement): PsiElement? = when (commaOwner) { - is KtParameterList -> { - val parent = commaOwner.parent - if (parent is KtFunctionLiteral) parent.arrow else commaOwner.rightParenthesis - } - is KtWhenEntry -> commaOwner.arrow - is KtDestructuringDeclaration -> commaOwner.rPar - else -> commaOwner.lastChild?.takeIfIsNotError() - } - } } -private fun PsiElement.takeIfIsNotError(): PsiElement? = takeIf { it !is PsiErrorElement } - private class TrailingCommaPostFormatVisitor(val settings: CodeStyleSettings) : TrailingCommaVisitor() { private val myPostProcessor = PostFormatProcessorHelper(settings.kotlinCommonSettings) @@ -199,24 +130,3 @@ private fun correctComma(comma: PsiElement) { commentOwner.deleteChildRange(firstElement, prevWithComment) } } - -private val RIGHT_BARRIERS = TokenSet.create(KtTokens.RBRACKET, KtTokens.RPAR, KtTokens.RBRACE, KtTokens.GT, KtTokens.ARROW) -private val LEFT_BARRIERS = TokenSet.create(KtTokens.LBRACKET, KtTokens.LPAR, KtTokens.LBRACE, KtTokens.LT) -private val PsiElement.lastSignificantChild: PsiElement? - get() = when (this) { - is KtWhenEntry -> arrow - is KtDestructuringDeclaration -> rPar - else -> lastChild - } - -fun PsiElement.leafIgnoringWhitespace(forward: Boolean = true, skipEmptyElements: Boolean = true) = - leaf(forward) { (!skipEmptyElements || it.textLength != 0) && it !is PsiWhiteSpace } - -fun PsiElement.leafIgnoringWhitespaceAndComments(forward: Boolean = true, skipEmptyElements: Boolean = true) = - leaf(forward) { (!skipEmptyElements || it.textLength != 0) && it !is PsiWhiteSpace && it !is PsiComment } - -fun PsiElement.leaf(forward: Boolean = true, filter: (PsiElement) -> Boolean): PsiElement? = - if (forward) nextLeaf(filter) - else prevLeaf(filter) - -val PsiElement.isComma: Boolean get() = getElementType(this) == KtTokens.COMMA \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/TrailingCommaInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/TrailingCommaInspection.kt index a3b45fbb011..34b4e0ff16a 100644 --- a/idea/src/org/jetbrains/kotlin/idea/inspections/TrailingCommaInspection.kt +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/TrailingCommaInspection.kt @@ -18,10 +18,6 @@ import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor import com.intellij.psi.codeStyle.CodeStyleManager import org.jetbrains.kotlin.idea.formatter.* -import org.jetbrains.kotlin.idea.formatter.TrailingCommaPostFormatProcessor.Companion.findInvalidCommas -import org.jetbrains.kotlin.idea.formatter.TrailingCommaPostFormatProcessor.Companion.needComma -import org.jetbrains.kotlin.idea.formatter.TrailingCommaPostFormatProcessor.Companion.trailingCommaAllowedInModule -import org.jetbrains.kotlin.idea.formatter.TrailingCommaPostFormatProcessor.Companion.trailingCommaOrLastElement import org.jetbrains.kotlin.idea.util.isLineBreak import org.jetbrains.kotlin.psi.KtElement import org.jetbrains.kotlin.psi.psiUtil.* @@ -44,7 +40,7 @@ class TrailingCommaInspection( } private fun checkLineBreaks(commaOwner: KtElement) { - val first = TrailingCommaPostFormatProcessor.elementBeforeFirstElement(commaOwner) + val first = TrailingCommaHelper.elementBeforeFirstElement(commaOwner) if (first?.nextLeaf(true)?.isLineBreak() == false) { first.nextSibling?.let { registerProblemForLineBreak( @@ -59,7 +55,7 @@ class TrailingCommaInspection( } - val last = TrailingCommaPostFormatProcessor.elementAfterLastElement(commaOwner) + val last = TrailingCommaHelper.elementAfterLastElement(commaOwner) if (last?.prevLeaf(true)?.isLineBreak() == false) { registerProblemForLineBreak( commaOwner, @@ -70,15 +66,15 @@ class TrailingCommaInspection( } private fun checkCommaPosition(commaOwner: KtElement) { - for (invalidComma in findInvalidCommas(commaOwner)) { + for (invalidComma in TrailingCommaHelper.findInvalidCommas(commaOwner)) { reportProblem(invalidComma, "Comma loses the advantages in this position", "Fix comma position") } } private fun checkTrailingComma(commaOwner: KtElement, action: TrailingCommaAction) { - val trailingCommaOrLastElement = trailingCommaOrLastElement(commaOwner) ?: return + val trailingCommaOrLastElement = TrailingCommaHelper.trailingCommaOrLastElement(commaOwner) ?: return if (action == TrailingCommaAction.ADD) { - if (!trailingCommaAllowedInModule(commaOwner) || trailingCommaOrLastElement.isComma) return + if (!TrailingCommaHelper.trailingCommaAllowedInModule(commaOwner) || trailingCommaOrLastElement.isComma) return reportProblem( trailingCommaOrLastElement, "Missing trailing comma", @@ -147,8 +143,8 @@ class TrailingCommaInspection( } private fun createFormatterTextRange(commaOwner: KtElement): TextRange { - val startElement = TrailingCommaPostFormatProcessor.elementBeforeFirstElement(commaOwner) ?: commaOwner - val endElement = TrailingCommaPostFormatProcessor.elementAfterLastElement(commaOwner) ?: commaOwner + val startElement = TrailingCommaHelper.elementBeforeFirstElement(commaOwner) ?: commaOwner + val endElement = TrailingCommaHelper.elementAfterLastElement(commaOwner) ?: commaOwner return TextRange.create(startElement.startOffset, endElement.endOffset) } @@ -175,8 +171,8 @@ private enum class TrailingCommaAction { companion object { fun create(commaOwner: KtElement): TrailingCommaAction = when { - needComma(commaOwner, null, checkExistingTrailingComma = false) -> ADD - needComma(commaOwner, null, checkExistingTrailingComma = true) -> REFORMAT + TrailingCommaHelper.needComma(commaOwner, null, checkExistingTrailingComma = false) -> ADD + TrailingCommaHelper.needComma(commaOwner, null, checkExistingTrailingComma = true) -> REFORMAT else -> REMOVE } }