org.cqfn.diktat.ruleset.rules.chapter3.LineLength.kt Maven / Gradle / Ivy
package org.cqfn.diktat.ruleset.rules.chapter3
import org.cqfn.diktat.common.config.rules.RuleConfiguration
import org.cqfn.diktat.common.config.rules.RulesConfig
import org.cqfn.diktat.common.config.rules.getRuleConfig
import org.cqfn.diktat.ruleset.constants.Warnings.LONG_LINE
import org.cqfn.diktat.ruleset.rules.DiktatRule
import org.cqfn.diktat.ruleset.utils.KotlinParser
import org.cqfn.diktat.ruleset.utils.appendNewlineMergingWhiteSpace
import org.cqfn.diktat.ruleset.utils.calculateLineColByOffset
import org.cqfn.diktat.ruleset.utils.findAllNodesWithConditionOnLine
import org.cqfn.diktat.ruleset.utils.findParentNodeWithSpecificType
import org.cqfn.diktat.ruleset.utils.getAllChildrenWithType
import org.cqfn.diktat.ruleset.utils.getFirstChildWithType
import org.cqfn.diktat.ruleset.utils.getLineNumber
import org.cqfn.diktat.ruleset.utils.hasChildOfType
import com.pinterest.ktlint.core.ast.ElementType.ANDAND
import com.pinterest.ktlint.core.ast.ElementType.ARROW
import com.pinterest.ktlint.core.ast.ElementType.BINARY_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.BLOCK
import com.pinterest.ktlint.core.ast.ElementType.BOOLEAN_CONSTANT
import com.pinterest.ktlint.core.ast.ElementType.CHARACTER_CONSTANT
import com.pinterest.ktlint.core.ast.ElementType.COMMA
import com.pinterest.ktlint.core.ast.ElementType.DOT
import com.pinterest.ktlint.core.ast.ElementType.DOT_QUALIFIED_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.ELVIS
import com.pinterest.ktlint.core.ast.ElementType.EOL_COMMENT
import com.pinterest.ktlint.core.ast.ElementType.EQ
import com.pinterest.ktlint.core.ast.ElementType.EQEQ
import com.pinterest.ktlint.core.ast.ElementType.EQEQEQ
import com.pinterest.ktlint.core.ast.ElementType.EXCL
import com.pinterest.ktlint.core.ast.ElementType.EXCLEQ
import com.pinterest.ktlint.core.ast.ElementType.EXCLEQEQEQ
import com.pinterest.ktlint.core.ast.ElementType.FILE
import com.pinterest.ktlint.core.ast.ElementType.FLOAT_CONSTANT
import com.pinterest.ktlint.core.ast.ElementType.FUN
import com.pinterest.ktlint.core.ast.ElementType.FUNCTION_LITERAL
import com.pinterest.ktlint.core.ast.ElementType.GT
import com.pinterest.ktlint.core.ast.ElementType.GTEQ
import com.pinterest.ktlint.core.ast.ElementType.IMPORT_LIST
import com.pinterest.ktlint.core.ast.ElementType.INTEGER_CONSTANT
import com.pinterest.ktlint.core.ast.ElementType.KDOC_MARKDOWN_INLINE_LINK
import com.pinterest.ktlint.core.ast.ElementType.KDOC_TEXT
import com.pinterest.ktlint.core.ast.ElementType.LBRACE
import com.pinterest.ktlint.core.ast.ElementType.LITERAL_STRING_TEMPLATE_ENTRY
import com.pinterest.ktlint.core.ast.ElementType.LONG_STRING_TEMPLATE_ENTRY
import com.pinterest.ktlint.core.ast.ElementType.LPAR
import com.pinterest.ktlint.core.ast.ElementType.LT
import com.pinterest.ktlint.core.ast.ElementType.LTEQ
import com.pinterest.ktlint.core.ast.ElementType.NULL
import com.pinterest.ktlint.core.ast.ElementType.OPERATION_REFERENCE
import com.pinterest.ktlint.core.ast.ElementType.OROR
import com.pinterest.ktlint.core.ast.ElementType.PACKAGE_DIRECTIVE
import com.pinterest.ktlint.core.ast.ElementType.PARENTHESIZED
import com.pinterest.ktlint.core.ast.ElementType.POSTFIX_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.PREFIX_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.PROPERTY
import com.pinterest.ktlint.core.ast.ElementType.RBRACE
import com.pinterest.ktlint.core.ast.ElementType.REFERENCE_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.RPAR
import com.pinterest.ktlint.core.ast.ElementType.SAFE_ACCESS
import com.pinterest.ktlint.core.ast.ElementType.SAFE_ACCESS_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.SHORT_STRING_TEMPLATE_ENTRY
import com.pinterest.ktlint.core.ast.ElementType.STRING_TEMPLATE
import com.pinterest.ktlint.core.ast.ElementType.VALUE_ARGUMENT_LIST
import com.pinterest.ktlint.core.ast.ElementType.WHEN_CONDITION_WITH_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.WHEN_ENTRY
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
import com.pinterest.ktlint.core.ast.isWhiteSpace
import com.pinterest.ktlint.core.ast.isWhiteSpaceWithNewline
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl
import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType
import java.net.MalformedURLException
import java.net.URL
/**
* The rule checks for lines in the file that exceed the maximum length.
* Rule ignores URL in KDoc. This rule can also fix some particular corner cases.
* This inspection can fix long binary expressions in condition inside `if`,
* in property declarations and in single line functions.
*/
@Suppress("ForbiddenComment")
class LineLength(configRules: List) : DiktatRule(
NAME_ID,
configRules,
listOf(LONG_LINE)
) {
private val configuration by lazy {
LineLengthConfiguration(
configRules.getRuleConfig(LONG_LINE)?.configuration ?: emptyMap()
)
}
private lateinit var positionByOffset: (Int) -> Pair
override fun logic(node: ASTNode) {
if (node.elementType == FILE) {
node.getChildren(null).forEach {
if (it.elementType != PACKAGE_DIRECTIVE && it.elementType != IMPORT_LIST) {
checkLength(it, configuration)
}
}
}
}
@Suppress("UnsafeCallOnNullableType", "TOO_LONG_FUNCTION")
private fun checkLength(node: ASTNode, configuration: LineLengthConfiguration) {
var offset = 0
node.text.lines().forEach { line ->
if (line.length > configuration.lineLength) {
val newNode = node.psi.findElementAt(offset + configuration.lineLength.toInt() - 1)!!.node
if ((newNode.elementType != KDOC_TEXT && newNode.elementType != KDOC_MARKDOWN_INLINE_LINK) || !isKdocValid(newNode)) {
positionByOffset = node.treeParent.calculateLineColByOffset()
val fixableType = isFixable(newNode, configuration)
LONG_LINE.warnAndFix(
configRules, emitWarn, isFixMode,
"max line length ${configuration.lineLength}, but was ${line.length}",
offset + node.startOffset, node, fixableType !is None
) {
// we should keep in mind, that in the course of fixing we change the offset
val textLenBeforeFix = node.textLength
fixableType.fix()
val textLenAfterFix = node.textLength
// offset for all next nodes changed to this delta
offset += (textLenAfterFix - textLenBeforeFix)
}
}
}
offset += line.length + 1
}
}
@Suppress(
"TOO_LONG_FUNCTION",
"LongMethod",
"ComplexMethod",
"GENERIC_VARIABLE_WRONG_DECLARATION",
)
private fun isFixable(wrongNode: ASTNode, configuration: LineLengthConfiguration): LongLineFixableCases {
var parent = wrongNode
var stringOrDot: ASTNode? = null
do {
when (parent.elementType) {
BINARY_EXPRESSION, PARENTHESIZED -> {
val parentIsValArgListOrFunLitOrWhenEntry = listOf(VALUE_ARGUMENT_LIST, FUNCTION_LITERAL, WHEN_CONDITION_WITH_EXPRESSION)
findParentNodeMatching(parent, parentIsValArgListOrFunLitOrWhenEntry)?.let {
parent = it
} ?: run {
val splitOffset = searchRightSplitAfterOperationReference(parent, configuration)?.second
splitOffset?.let {
val parentIsBiExprOrParenthesized = parent.treeParent.elementType in listOf(BINARY_EXPRESSION, PARENTHESIZED)
val parentIsFunOrProperty = parent.treeParent.elementType in listOf(FUN, PROPERTY)
if (parentIsBiExprOrParenthesized || (parentIsFunOrProperty && splitOffset > configuration.lineLength)) {
parent = parent.treeParent
} else {
return checkBinaryExpression(parent, configuration)
}
}
?: run {
stringOrDot?.let {
val returnElem = checkStringTemplateAndDotQualifiedExpression(it, configuration)
if (returnElem !is None) {
return returnElem
}
}
parent = parent.treeParent
}
}
}
FUN, PROPERTY -> return checkFunAndProperty(parent)
VALUE_ARGUMENT_LIST -> parent.findParentNodeWithSpecificType(BINARY_EXPRESSION)?.let {
parent = it
} ?: return checkArgumentsList(parent, configuration)
WHEN_ENTRY -> return WhenEntry(parent)
WHEN_CONDITION_WITH_EXPRESSION -> return None()
EOL_COMMENT -> return checkComment(parent, configuration)
FUNCTION_LITERAL -> return Lambda(parent)
STRING_TEMPLATE, DOT_QUALIFIED_EXPRESSION, SAFE_ACCESS_EXPRESSION -> {
stringOrDot = parent
val parentIsBinExpOrValArgListOrWhenEntry = listOf(BINARY_EXPRESSION, VALUE_ARGUMENT_LIST, WHEN_CONDITION_WITH_EXPRESSION)
findParentNodeMatching(parent, parentIsBinExpOrValArgListOrWhenEntry)?.let {
parent = it
} ?: run {
val returnElem = checkStringTemplateAndDotQualifiedExpression(parent, configuration)
if (returnElem !is None) {
return returnElem
}
parent = parent.treeParent
}
}
else -> parent = parent.treeParent
}
} while (parent.treeParent != null)
return None()
}
private fun findParentNodeMatching(node: ASTNode, listType: List): ASTNode? {
listType.forEach { type ->
node.findParentNodeWithSpecificType(type)?.let {
return it
}
}
return null
}
private fun checkArgumentsList(node: ASTNode, configuration: LineLengthConfiguration): LongLineFixableCases {
node.findParentNodeWithSpecificType(WHEN_ENTRY)?.let {
it.findChildByType(BLOCK)?.run {
return ValueArgumentList(node, configuration, positionByOffset)
} ?: return WhenEntry(it)
}
return ValueArgumentList(node, configuration, positionByOffset)
}
/**
* Parses the existing binary expression and passes the necessary parameters to the fix function for splitting
*/
private fun checkBinaryExpression(node: ASTNode, configuration: LineLengthConfiguration): LongLineFixableCases {
val leftOffset = positionByOffset(node.firstChildNode.startOffset).second
val binList: MutableList = mutableListOf()
searchBinaryExpression(node, binList)
if (binList.size == 1) {
return BinaryExpression(node)
}
return LongBinaryExpression(node, configuration, leftOffset, binList, positionByOffset)
}
@Suppress("TOO_MANY_LINES_IN_LAMBDA", "GENERIC_VARIABLE_WRONG_DECLARATION")
private fun checkStringTemplateAndDotQualifiedExpression(
node: ASTNode,
configuration: LineLengthConfiguration
): LongLineFixableCases {
val isPropertyOrFun = listOf(PROPERTY, FUN)
val funOrPropertyNode = findParentNodeMatching(node, isPropertyOrFun)
funOrPropertyNode?.let {
if (it.hasChildOfType(EQ)) {
val positionByOffset = positionByOffset(it.getFirstChildWithType(EQ)?.startOffset ?: 0).second
if (positionByOffset < configuration.lineLength / 2) {
val returnedClass = parserStringAndDot(node, configuration)
if (returnedClass !is None) {
return returnedClass
}
}
return FunAndProperty(it)
}
return parserStringAndDot(node, configuration)
} ?: return parserStringAndDot(node, configuration)
}
private fun parserStringAndDot(node: ASTNode, configuration: LineLengthConfiguration) =
if (node.elementType == STRING_TEMPLATE) {
parserStringTemplate(node, configuration)
} else {
parserDotQualifiedExpression(node, configuration)
}
/**
* This class finds where the string can be split
*
* @return StringTemplate - if the string can be split,
* BinaryExpression - if there is two concatenated strings and new line should be inserted after `+`
* None - if the string can't be split
*/
@Suppress("TOO_LONG_FUNCTION", "UnsafeCallOnNullableType")
private fun parserStringTemplate(node: ASTNode, configuration: LineLengthConfiguration): LongLineFixableCases {
var multiLineOffset = 0
val leftOffset = if (node.text.lines().size > 1) {
node
.text
.lines()
.takeWhile { it.length < configuration.lineLength }
.forEach { multiLineOffset += it.length }
node
.text
.lines()
.first { it.length > configuration.lineLength }
.takeWhile { it.isWhitespace() }
.count()
} else {
positionByOffset(node.startOffset).second
}
val delimiterIndex =
node.text.substring(0, multiLineOffset + configuration.lineLength.toInt() - leftOffset).lastIndexOf(' ')
if (delimiterIndex == -1) {
// we can't split this string, however may be we can move it entirely:
// case when new line should be inserted after `+`. Example: "first" + "second"
node.treeParent.findChildByType(OPERATION_REFERENCE)?.let {
return BinaryExpression(node.treeParent)
}
// can't fix this case
return None()
}
// check, that space to split is a part of text - not code
// If the space split is part of the code, then there is a chance of breaking the code when fixing, that why we should ignore it
val isSpaceIsWhiteSpace = node.psi
.findElementAt(delimiterIndex)!!
.node
.isWhiteSpace()
if (isSpaceIsWhiteSpace) {
return None()
}
// minus 2 here as we are inserting ` +` and we don't want it to exceed line length
val shouldAddTwoSpaces =
(multiLineOffset == 0) && (leftOffset + delimiterIndex > configuration.lineLength.toInt() - 2)
val correcterDelimiter = if (shouldAddTwoSpaces) {
node.text.substring(0, delimiterIndex - 2).lastIndexOf(' ')
} else {
delimiterIndex
}
if (correcterDelimiter == -1) {
return None()
}
return StringTemplate(node, correcterDelimiter, multiLineOffset == 0)
}
private fun parserDotQualifiedExpression(
wrongNode: ASTNode,
configuration: LineLengthConfiguration
): LongLineFixableCases {
val nodeDot = searchRightSplitBeforeDotOrSafeAccess(wrongNode, configuration, DOT)
val nodeSafeAccess = searchRightSplitBeforeDotOrSafeAccess(wrongNode, configuration, SAFE_ACCESS)
return nodeDot?.let {
DotQualifiedExpression(wrongNode)
} ?: nodeSafeAccess?.let {
DotQualifiedExpression(wrongNode)
} ?: None()
}
private fun checkFunAndProperty(wrongNode: ASTNode) =
if (wrongNode.hasChildOfType(EQ)) FunAndProperty(wrongNode) else None()
private fun checkComment(wrongNode: ASTNode, configuration: LineLengthConfiguration): LongLineFixableCases {
val leftOffset = positionByOffset(wrongNode.startOffset).second
val stringBeforeCommentContent = wrongNode.text.takeWhile { it == ' ' || it == '/' }
if (stringBeforeCommentContent.length >= configuration.lineLength.toInt() - leftOffset) {
return None()
}
val indexLastSpace = wrongNode.text.substring(stringBeforeCommentContent.length, configuration.lineLength.toInt() - leftOffset).lastIndexOf(' ')
val isNewLine = wrongNode.treePrev?.isWhiteSpaceWithNewline() ?: wrongNode.treeParent?.treePrev?.isWhiteSpaceWithNewline() ?: false
if (isNewLine && indexLastSpace == -1) {
return None()
}
return Comment(wrongNode, isNewLine, indexLastSpace + stringBeforeCommentContent.length)
}
// fixme json method
private fun isKdocValid(node: ASTNode) = try {
if (node.elementType == KDOC_TEXT) {
URL(node.text.split("\\s".toRegex()).last { it.isNotEmpty() })
} else {
URL(node.text.substring(node.text.indexOfFirst { it == ']' } + 2, node.textLength - 1))
}
true
} catch (e: MalformedURLException) {
false
}
/**
* This method uses recursion to store binary node in the order in which they are located
* Also binList contains nodes with PREFIX_EXPRESSION element type ( !isFoo(), !isValid)
*
*@param node node in which to search
*@param binList mutable list of ASTNode to store nodes
*/
private fun searchBinaryExpression(node: ASTNode, binList: MutableList) {
if (node.hasChildOfType(BINARY_EXPRESSION) || node.hasChildOfType(PARENTHESIZED) || node.hasChildOfType(POSTFIX_EXPRESSION)) {
node.getChildren(null)
.forEach {
searchBinaryExpression(it, binList)
}
}
if (node.elementType == BINARY_EXPRESSION) {
binList.add(node)
binList.add(node.treeParent.findChildByType(PREFIX_EXPRESSION) ?: return)
}
}
/**
* This method uses recursion to store dot qualified expression node in the order in which they are located
* Also dotList contains nodes with PREFIX_EXPRESSION element type ( !isFoo(), !isValid))
*
*@param node node in which to search
*@param dotList mutable list of ASTNode to store nodes
*/
private fun searchDotOrSafeAccess(node: ASTNode, dotList: MutableList) {
if (node.elementType == DOT_QUALIFIED_EXPRESSION || node.elementType == SAFE_ACCESS_EXPRESSION || node.elementType == POSTFIX_EXPRESSION) {
node.getChildren(null)
.forEach {
searchDotOrSafeAccess(it, dotList)
}
if (node.elementType != POSTFIX_EXPRESSION) {
dotList.add(node)
}
}
}
/**
* Finds the first binary expression closer to the separator
*/
@Suppress("UnsafeCallOnNullableType")
private fun searchRightSplitAfterOperationReference(
parent: ASTNode,
configuration: LineLengthConfiguration,
): Pair? {
val list: MutableList = mutableListOf()
searchBinaryExpression(parent, list)
return list.asSequence()
.map {
it to positionByOffset(it.getFirstChildWithType(OPERATION_REFERENCE)!!.startOffset).second
}
.sortedBy { it.second }
.lastOrNull { (it, offset) ->
offset + (it.getFirstChildWithType(OPERATION_REFERENCE)?.text?.length ?: 0) <= configuration.lineLength + 1
}
}
/**
* Finds the first dot or safe access closer to the separator
*/
@Suppress("MAGIC_NUMBER", "MagicNumber")
private fun searchRightSplitBeforeDotOrSafeAccess(
parent: ASTNode,
configuration: LineLengthConfiguration,
type: IElementType
): Pair? {
val list: MutableList = mutableListOf()
searchDotOrSafeAccess(parent, list)
val offsetFromMaximum = 10
return list.asSequence()
.map {
val offset = it.getFirstChildWithType(type)?.run {
positionByOffset(this.startOffset).second
} ?: run {
configuration.lineLength.toInt() + offsetFromMaximum
}
it to offset
}
.sortedBy { it.second }
.lastOrNull { (_, offset) ->
offset <= configuration.lineLength + 1
}
}
/**
*
* [RuleConfiguration] for maximum line length
*/
class LineLengthConfiguration(config: Map) : RuleConfiguration(config) {
/**
* Maximum allowed line length
*/
val lineLength = config["lineLength"]?.toLongOrNull() ?: MAX_LENGTH
}
/**
* Class LongLineFixableCases is parent class for several specific error classes
*/
@Suppress("KDOC_NO_CONSTRUCTOR_PROPERTY", "MISSING_KDOC_CLASS_ELEMENTS") // todo add proper docs
abstract class LongLineFixableCases(val node: ASTNode) {
/**
* Abstract fix - fix anything nodes
*/
abstract fun fix()
}
/**
* Class None show error long line have unidentified type or something else that we can't analyze
*/
private class None : LongLineFixableCases(KotlinParser().createNode("ERROR")) {
@Suppress("EmptyFunctionBlock")
override fun fix() {}
}
/**
* Class Comment show that long line should be split in comment
* @property hasNewLineBefore flag to handle type of comment: ordinary comment (long part of which should be moved to the next line)
* and inline comments (which should be moved entirely to the previous line)
* @property indexLastSpace index of last space to substring comment
*/
private class Comment(
node: ASTNode,
val hasNewLineBefore: Boolean,
val indexLastSpace: Int = 0
) : LongLineFixableCases(node) {
override fun fix() {
if (this.hasNewLineBefore) {
val indexLastSpace = this.indexLastSpace
val nodeText = "//${node.text.substring(indexLastSpace, node.text.length)}"
node.treeParent.apply {
addChild(LeafPsiElement(EOL_COMMENT, node.text.substring(0, indexLastSpace)), node)
addChild(PsiWhiteSpaceImpl("\n"), node)
addChild(LeafPsiElement(EOL_COMMENT, nodeText), node)
removeChild(node)
}
} else {
if (node.treePrev.isWhiteSpace()) {
node.treeParent.removeChild(node.treePrev)
}
val newLineNodeOnPreviousLine = node.findAllNodesWithConditionOnLine(node.getLineNumber() - 1) {
it.elementType == WHITE_SPACE && it.textContains('\n')
}?.lastOrNull()
newLineNodeOnPreviousLine?.let {
val parent = node.treeParent
parent.removeChild(node)
newLineNodeOnPreviousLine.treeParent.addChild(node, newLineNodeOnPreviousLine.treeNext)
newLineNodeOnPreviousLine.treeParent.addChild(PsiWhiteSpaceImpl("\n"), newLineNodeOnPreviousLine.treeNext.treeNext)
}
}
}
}
/**
* Class StringTemplate show that long line should be split in string template
* @property delimiterIndex
* @property isOneLineString
*/
private class StringTemplate(
node: ASTNode,
val delimiterIndex: Int,
val isOneLineString: Boolean
) : LongLineFixableCases(node) {
override fun fix() {
val incorrectText = node.text
val firstPart = incorrectText.substring(0, delimiterIndex)
val secondPart = incorrectText.substring(delimiterIndex, incorrectText.length)
val textBetweenParts =
if (isOneLineString) {
"\" +\n\""
} else {
"\n"
}
val correctNode = KotlinParser().createNode("$firstPart$textBetweenParts$secondPart")
node.treeParent.replaceChild(node, correctNode)
}
}
/**
* Class BinaryExpression show that long line should be split in short binary expression? after operation reference
*/
private class BinaryExpression(node: ASTNode) : LongLineFixableCases(node) {
override fun fix() {
val binNode = if (node.elementType == PARENTHESIZED) {
node.findChildByType(BINARY_EXPRESSION)
} else {
node
}
val nodeOperationReference = binNode?.findChildByType(OPERATION_REFERENCE)
val nextNode = if (nodeOperationReference?.firstChildNode?.elementType != ELVIS) {
nodeOperationReference?.treeNext
} else {
if (nodeOperationReference.treePrev.elementType == WHITE_SPACE) {
nodeOperationReference.treePrev
} else {
nodeOperationReference
}
}
binNode?.appendNewlineMergingWhiteSpace(nextNode, nextNode)
}
}
/**
* Class LongBinaryExpression show that long line should be split between other parts long binary expression,
* after one of operation reference
* @property maximumLineLength is number of maximum line length
* @property leftOffset is offset before start [node]
* @property binList is list of Binary Expression which are children of [node]
* @property positionByOffset
*/
private class LongBinaryExpression(
node: ASTNode,
val maximumLineLength: LineLengthConfiguration,
val leftOffset: Int,
val binList: MutableList,
var positionByOffset: (Int) -> Pair
) : LongLineFixableCases(node) {
/**
* Fix a binary expression -
* - If the transfer is done on the Elvis operator, then transfers it to a new line
* - If not on the Elvis operator, then transfers it to a new line after the operation reference
*/
@Suppress("UnsafeCallOnNullableType")
override fun fix() {
val anySplitNode = searchSomeSplitInBinaryExpression(node, maximumLineLength)
val rightSplitNode = anySplitNode[0] ?: anySplitNode[1] ?: anySplitNode[2]
val nodeOperationReference = rightSplitNode?.first?.getFirstChildWithType(OPERATION_REFERENCE)
rightSplitNode?.let {
val nextNode = if (nodeOperationReference?.firstChildNode?.elementType != ELVIS) {
nodeOperationReference?.treeNext
} else {
if (nodeOperationReference.treePrev.elementType == WHITE_SPACE) {
nodeOperationReference.treePrev
} else {
nodeOperationReference
}
}
if (!nextNode?.text?.contains(("\n"))!!) {
rightSplitNode.first.appendNewlineMergingWhiteSpace(nextNode, nextNode)
}
}
}
/**
* This method stored all the nodes that have [BINARY_EXPRESSION] or [PREFIX_EXPRESSION] element type.
* - First elem in List - Logic Binary Expression (`&&`, `||`)
* - Second elem in List - Comparison Binary Expression (`>`, `<`, `==`, `>=`, `<=`, `!=`, `===`, `!==`)
* - Other types (Arithmetical and Bitwise operation) (`+`, `-`, `*`, `/`, `%`, `>>`, `<<`, `&`, `|`, `~`, `^`, `>>>`, `<<<`,
* `*=`, `+=`, `-=`, `/=`, `%=`, `++`, `--`, `in` `!in`, etc.)
*
* @return the list of node-to-offset pairs.
*/
@Suppress("TYPE_ALIAS")
private fun searchSomeSplitInBinaryExpression(parent: ASTNode, configuration: LineLengthConfiguration): List?> {
val logicListOperationReference = listOf(OROR, ANDAND)
val compressionListOperationReference = listOf(GT, LT, EQEQ, GTEQ, LTEQ, EXCLEQ, EQEQEQ, EXCLEQEQEQ)
val binList: MutableList = mutableListOf()
searchBinaryExpression(parent, binList)
val rightBinList = binList.map {
it to positionByOffset(it.getFirstChildWithType(OPERATION_REFERENCE)?.startOffset ?: 0).second
}
.sortedBy { it.second }
.reversed()
val returnList: MutableList?> = mutableListOf()
addInSmartListBinExpression(returnList, rightBinList, logicListOperationReference, configuration)
addInSmartListBinExpression(returnList, rightBinList, compressionListOperationReference, configuration)
val expression = rightBinList.firstOrNull { (it, offset) ->
val binOperationReference = it.getFirstChildWithType(OPERATION_REFERENCE)?.firstChildNode?.elementType
offset + (it.getFirstChildWithType(OPERATION_REFERENCE)?.text?.length ?: 0) <= configuration.lineLength + 1 &&
binOperationReference !in logicListOperationReference && binOperationReference !in compressionListOperationReference && binOperationReference != EXCL
}
returnList.add(expression)
return returnList
}
private fun searchBinaryExpression(node: ASTNode, binList: MutableList) {
if (node.hasChildOfType(BINARY_EXPRESSION) || node.hasChildOfType(PARENTHESIZED) || node.hasChildOfType(POSTFIX_EXPRESSION)) {
node.getChildren(null)
.forEach {
searchBinaryExpression(it, binList)
}
}
if (node.elementType == BINARY_EXPRESSION) {
binList.add(node)
binList.add(node.treeParent.findChildByType(PREFIX_EXPRESSION) ?: return)
}
}
/**
* Runs through the sorted list [rightBinList], finds its last element, the type of which is included in the set [typesList] and adds it in the list [returnList]
*/
@Suppress("TYPE_ALIAS")
private fun addInSmartListBinExpression(
returnList: MutableList?>,
rightBinList: List>,
typesList: List,
configuration: LineLengthConfiguration
) {
val expression = rightBinList.firstOrNull { (it, offset) ->
val binOperationReference = it.getFirstChildWithType(OPERATION_REFERENCE)
offset + (it.getFirstChildWithType(OPERATION_REFERENCE)?.text?.length ?: 0) <= configuration.lineLength + 1 &&
binOperationReference?.firstChildNode?.elementType in typesList
}
returnList.add(expression)
}
}
/**
* Class FunAndProperty show that long line should be split in Fun Or Property: after EQ (between head and body this function)
*/
private class FunAndProperty(node: ASTNode) : LongLineFixableCases(node) {
override fun fix() {
node.appendNewlineMergingWhiteSpace(null, node.findChildByType(EQ)?.treeNext)
}
}
/**
* Class Lambda show that long line should be split in Lambda: in space after [LBRACE] node and before [RBRACE] node
*/
private class Lambda(node: ASTNode) : LongLineFixableCases(node) {
/**
* Splits Lambda expressions - add splits lines, thereby making the lambda expression a separate line
*/
override fun fix() {
node.appendNewlineMergingWhiteSpace(node.findChildByType(LBRACE)?.treeNext, node.findChildByType(LBRACE)?.treeNext)
node.appendNewlineMergingWhiteSpace(node.findChildByType(RBRACE)?.treePrev, node.findChildByType(RBRACE)?.treePrev)
}
}
/**
* Class DotQualifiedExpression show that line should be split in DotQualifiedExpression
*/
private class DotQualifiedExpression(node: ASTNode) : LongLineFixableCases(node) {
override fun fix() {
val dot = node.getFirstChildWithType(DOT)
val safeAccess = node.getFirstChildWithType(SAFE_ACCESS)
val splitNode = if ((dot?.startOffset ?: 0) > (safeAccess?.startOffset ?: 0)) {
dot
} else {
safeAccess
}
val nodeBeforeDot = splitNode?.treePrev
node.appendNewlineMergingWhiteSpace(nodeBeforeDot, splitNode)
}
}
/**
* Class ValueArgumentList show that line should be split in ValueArgumentList:
* @property maximumLineLength - max line length
* @property positionByOffset
*/
private class ValueArgumentList(
node: ASTNode,
val maximumLineLength: LineLengthConfiguration,
var positionByOffset: (Int) -> Pair
) : LongLineFixableCases(node) {
override fun fix() {
val lineLength = maximumLineLength.lineLength
val offset = fixFirst()
val listComma = node.getAllChildrenWithType(COMMA).map {
it to positionByOffset(it.startOffset - offset).second
}.sortedBy { it.second }
var lineNumber = 1
listComma.forEachIndexed { index, pair ->
if (pair.second >= lineNumber * lineLength) {
lineNumber++
val commaSplit = if (index > 0) {
listComma[index - 1].first
} else {
pair.first
}
node.appendNewlineMergingWhiteSpace(commaSplit.treeNext, commaSplit.treeNext)
}
}
node.getFirstChildWithType(RPAR)?.let {
if (positionByOffset(it.treePrev.startOffset).second + it.treePrev.text.length - offset > lineLength * lineNumber && listComma.isNotEmpty()) {
listComma.last().first.let {
node.appendNewlineMergingWhiteSpace(it.treeNext, it.treeNext)
}
}
}
}
private fun fixFirst(): Int {
val lineLength = maximumLineLength.lineLength
var startOffset = 0
node.getFirstChildWithType(COMMA)?.let {
if (positionByOffset(it.startOffset).second > lineLength) {
node.appendNewlineMergingWhiteSpace(node.findChildByType(LPAR)?.treeNext, node.findChildByType(LPAR)?.treeNext)
node.appendNewlineMergingWhiteSpace(node.findChildByType(RPAR), node.findChildByType(RPAR))
startOffset = this.maximumLineLength.lineLength.toInt()
}
} ?: node.getFirstChildWithType(RPAR)?.let {
node.appendNewlineMergingWhiteSpace(node.findChildByType(LPAR)?.treeNext, node.findChildByType(LPAR)?.treeNext)
node.appendNewlineMergingWhiteSpace(node.findChildByType(RPAR), node.findChildByType(RPAR))
startOffset = this.maximumLineLength.lineLength.toInt()
}
return startOffset
}
}
/**
* Class WhenEntry show that line should be split in WhenEntry node:
* - Added [LBRACE] and [RBRACE] nodes
* - Split line in space after [LBRACE] node and before [RBRACE] node
*/
private class WhenEntry(node: ASTNode) : LongLineFixableCases(node) {
override fun fix() {
node.getFirstChildWithType(ARROW)?.let {
node.appendNewlineMergingWhiteSpace(it.treeNext, it.treeNext)
}
}
}
/**
* val text = "first part" +
* "second part" +
* "third part"
* STRING_PART_OFFSET equal to the left offset of first string part("first part") =
* white space + close quote (open quote removed by trim) + white space + plus sign
*/
companion object {
private const val MAX_LENGTH = 120L
const val NAME_ID = "line-length"
private const val STRING_PART_OFFSET = 4
private val propertyList = listOf(INTEGER_CONSTANT, LITERAL_STRING_TEMPLATE_ENTRY, FLOAT_CONSTANT,
CHARACTER_CONSTANT, REFERENCE_EXPRESSION, BOOLEAN_CONSTANT, LONG_STRING_TEMPLATE_ENTRY,
SHORT_STRING_TEMPLATE_ENTRY, NULL)
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy