diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationAmount.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationAmount.kt new file mode 100644 index 0000000000..81ef4910a3 --- /dev/null +++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationAmount.kt @@ -0,0 +1,59 @@ +package org.cqfn.diktat.ruleset.rules.chapter3.files + +import org.cqfn.diktat.ruleset.utils.indentation.IndentationConfig + +/** + * Encapsulates the change in the indentation level. + */ +@Suppress("WRONG_DECLARATIONS_ORDER") +internal enum class IndentationAmount { + /** + * The indent should be preserved at the current level. + */ + NONE, + + /** + * The indent should be increased or decreased by 1 (regular single indent). + */ + SINGLE, + + /** + * Extended, or _continuation_ indent. Applicable when any of + * [`extendedIndent*`][IndentationConfig] flags is **on**. + */ + EXTENDED, + ; + + /** + * @return the indentation level. To get the actual indentation (the amount + * of space characters), the value needs to be multiplied by + * [IndentationConfig.indentationSize]. + * @see IndentationConfig.indentationSize + */ + fun level(): Int = + ordinal + + /** + * @return whether this amount represents the change in the indentation + * level, i.e. whether the element should be indented or un-indented. + */ + fun isNonZero(): Boolean = + level() > 0 + + companion object { + /** + * A convenience factory method. + * + * @param extendedIndent the actual value of ony of the `extendedIndent*` + * flags. + * @return the corresponding indentation amount, either [SINGLE] or + * [EXTENDED]. + */ + @JvmStatic + fun valueOf(extendedIndent: Boolean): IndentationAmount = + when { + extendedIndent -> EXTENDED + else -> SINGLE + } + } +} diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationAware.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationAware.kt new file mode 100644 index 0000000000..730edc039c --- /dev/null +++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationAware.kt @@ -0,0 +1,11 @@ +package org.cqfn.diktat.ruleset.rules.chapter3.files + +/** + * A contract for types which encapsulate the indentation level. + */ +internal interface IndentationAware { + /** + * @return the indentation (the amount of space characters) of this element. + */ + val indentation: Int +} diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationConfigAware.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationConfigAware.kt new file mode 100644 index 0000000000..8ecf3c5504 --- /dev/null +++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationConfigAware.kt @@ -0,0 +1,173 @@ +package org.cqfn.diktat.ruleset.rules.chapter3.files + +import org.cqfn.diktat.ruleset.utils.indentation.IndentationConfig + +/** + * Higher-level abstractions on top of the [indentation size][IndentationConfig.indentationSize]. + */ +internal interface IndentationConfigAware { + /** + * The configuration this instance encapsulates. + */ + val configuration: IndentationConfig + + /** + * Increases the indentation level by [level] * [IndentationConfig.indentationSize]. + * + * This extension doesn't modify the receiver. + * + * @receiver the previous indentation level (in space characters), not + * modified by the function call. + * @param level the indentation level, 1 by default. + * @return the new indentation level. + * @see unindent + * @see IndentationConfig.indentationSize + */ + fun Int.indent(level: Int = 1): Int = + this + level * configuration.indentationSize + + /** + * Decreases the indentation level by [level] * [IndentationConfig.indentationSize]. + * + * This extension doesn't modify the receiver. + * + * @receiver the previous indentation level (in space characters), not + * modified by the function call. + * @param level the indentation level, 1 by default. + * @return the new indentation level. + * @see indent + * @see IndentationConfig.indentationSize + */ + fun Int.unindent(level: Int = 1): Int = + indent(-level) + + /** + * @receiver the previous indentation level (in space characters), not + * modified by the function call. + * @param amount the indentation amount. + * @return the new (increased) indentation level. + * @see minus + */ + operator fun Int.plus(amount: IndentationAmount): Int = + indent(level = amount.level()) + + /** + * @receiver the previous indentation level (in space characters), not + * modified by the function call. + * @param amount the indentation amount. + * @return the new (decreased) indentation level. + * @see plus + */ + operator fun Int.minus(amount: IndentationAmount): Int = + unindent(level = amount.level()) + + /** + * Allows the `+` operation between an Int and an IndentationAmount to be + * commutative. Now, the following are equivalent: + * + * ```kotlin + * val i = 42 + IndentationAmount.SINGLE + * val j = IndentationAmount.SINGLE + 42 + * ``` + * + * — as are these: + * + * ```kotlin + * val i = 42 + IndentationAmount.SINGLE + * val j = IndentationAmount.SINGLE + 42 + * ``` + * + * @receiver the indentation amount. + * @param indentationSpaces the indentation level (in space characters). + * @return the new (increased) indentation level. + * @see IndentationAmount.minus + */ + operator fun IndentationAmount.plus(indentationSpaces: Int): Int = + indentationSpaces + this + + /** + * Allows expressions like this: + * + * ```kotlin + * 42 - IndentationAmount.SINGLE + 4 + * ``` + * + * to be rewritten this way: + * + * ```kotlin + * 42 - (IndentationAmount.SINGLE - 4) + * ``` + * + * @receiver the indentation amount. + * @param indentationSpaces the indentation level (in space characters). + * @return the new (decreased) indentation level. + * @see IndentationAmount.plus + */ + operator fun IndentationAmount.minus(indentationSpaces: Int): Int = + this + (-indentationSpaces) + + /** + * @receiver the 1st term. + * @param other the 2nd term. + * @return the two indentation amounts combined, as the indentation level + * (in space characters). + * @see IndentationAmount.minus + */ + operator fun IndentationAmount.plus(other: IndentationAmount): Int = + this + (+other) + + /** + * @receiver the minuend. + * @param other the subtrahend. + * @return one amount subtracted from the other, as the indentation level + * (in space characters). + * @see IndentationAmount.plus + */ + operator fun IndentationAmount.minus(other: IndentationAmount): Int = + this + (-other) + + /** + * @receiver the indentation amount. + * @return the indentation level (in space characters). + * @see IndentationAmount.unaryMinus + */ + operator fun IndentationAmount.unaryPlus(): Int = + level() * configuration.indentationSize + + /** + * @receiver the indentation amount. + * @return the negated indentation level (in space characters). + * @see IndentationAmount.unaryPlus + */ + operator fun IndentationAmount.unaryMinus(): Int = + -(+this) + + companion object Factory { + /** + * Creates a new instance. + * + * While you may call this function directly, consider using + * [withIndentationConfig] instead. + * + * @param configuration the configuration this instance will wrap. + * @return the newly created instance. + * @see withIndentationConfig + */ + operator fun invoke(configuration: IndentationConfig): IndentationConfigAware = + object : IndentationConfigAware { + override val configuration = configuration + } + + /** + * Calls the specified function [block] with [IndentationConfigAware] as + * its receiver and returns its result. + * + * @param configuration the indentation configuration. + * @param block the function block to call. + * @return the result returned by the function block. + */ + inline fun withIndentationConfig(configuration: IndentationConfig, + block: IndentationConfigAware.() -> T): T = + with(IndentationConfigAware(configuration), block) + } +} diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationRule.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationRule.kt index 58863a1741..b1ebd5c8ef 100644 --- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationRule.kt +++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationRule.kt @@ -9,6 +9,9 @@ import org.cqfn.diktat.common.config.rules.getRuleConfig import org.cqfn.diktat.common.utils.loggerWithKtlintConfig import org.cqfn.diktat.ruleset.constants.Warnings.WRONG_INDENTATION import org.cqfn.diktat.ruleset.rules.DiktatRule +import org.cqfn.diktat.ruleset.rules.chapter3.files.IndentationAmount.NONE +import org.cqfn.diktat.ruleset.rules.chapter3.files.IndentationAmount.SINGLE +import org.cqfn.diktat.ruleset.rules.chapter3.files.IndentationConfigAware.Factory.withIndentationConfig import org.cqfn.diktat.ruleset.utils.NEWLINE import org.cqfn.diktat.ruleset.utils.SPACE import org.cqfn.diktat.ruleset.utils.TAB @@ -68,7 +71,6 @@ import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace 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 org.jetbrains.kotlin.com.intellij.util.containers.Stack import org.jetbrains.kotlin.konan.file.File import org.jetbrains.kotlin.psi.KtDotQualifiedExpression import org.jetbrains.kotlin.psi.KtIfExpression @@ -77,6 +79,8 @@ import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf import org.jetbrains.kotlin.psi.psiUtil.startOffset +import java.util.ArrayDeque as Stack + import kotlin.math.abs import kotlin.reflect.KCallable @@ -162,7 +166,7 @@ class IndentationRule(configRules: List) : DiktatRule( // however, the text length does not consider it, since it's blank and line appeared only because of `\n` // But ktlint synthetically increase length in aim to have ability to point to this line, so in this case // offset will be `node.textLength`, otherwise we will point to the last symbol, i.e `node.textLength - 1` - val offset = if (lastChild.elementType == WHITE_SPACE && lastChild.textContains(NEWLINE)) node.textLength else node.textLength - 1 + val offset = if (lastChild.isMultilineWhitespace()) node.textLength else node.textLength - 1 WRONG_INDENTATION.warnAndFix(configRules, emitWarn, isFixMode, "$warnText at the end of file $fileName", offset, node) { if (lastChild.elementType != WHITE_SPACE) { node.addChild(PsiWhiteSpaceImpl(NEWLINE.toString()), null) @@ -177,21 +181,21 @@ class IndentationRule(configRules: List) : DiktatRule( /** * Traverses the tree, keeping track of regular and exceptional indentations */ - private fun checkIndentation(node: ASTNode) { - val context = IndentContext(configuration) - node.visit { astNode -> - context.checkAndReset(astNode) - if (astNode.isIndentIncrementing()) { - context.storeIncrementingToken(astNode.elementType) - } else if (astNode.isIndentDecrementing() && !astNode.treePrev.let { it.elementType == WHITE_SPACE && it.textContains(NEWLINE) }) { - // if decreasing token is after WHITE_SPACE with \n, indents are corrected in visitWhiteSpace method - context.dec(astNode.elementType) - } else if (astNode.elementType == WHITE_SPACE && astNode.textContains(NEWLINE) && astNode.treeNext != null) { - // we check only WHITE_SPACE nodes with newlines, other than the last line in file; correctness of newlines should be checked elsewhere - visitWhiteSpace(astNode, context) + private fun checkIndentation(node: ASTNode) = + with(IndentContext(configuration)) { + node.visit { astNode -> + checkAndReset(astNode) + if (astNode.getIndentationIncrement().isNonZero()) { + storeIncrementingToken(astNode.elementType) + } else if (astNode.getIndentationDecrement().isNonZero() && !astNode.treePrev.isMultilineWhitespace()) { + // if decreasing token is after WHITE_SPACE with \n, indents are corrected in visitWhiteSpace method + this -= astNode.elementType + } else if (astNode.isMultilineWhitespace() && astNode.treeNext != null) { + // we check only WHITE_SPACE nodes with newlines, other than the last line in file; correctness of newlines should be checked elsewhere + visitWhiteSpace(astNode) + } } } - } private fun isCloseAndOpenQuoterOffset(nodeWhiteSpace: ASTNode, expectedIndent: Int): Boolean { val nextNode = nodeWhiteSpace.treeNext @@ -209,15 +213,19 @@ class IndentationRule(configRules: List) : DiktatRule( } @Suppress("ForbiddenComment") - private fun visitWhiteSpace(astNode: ASTNode, context: IndentContext) { - context.maybeIncrement() + private fun IndentContext.visitWhiteSpace(astNode: ASTNode) { + require(astNode.isMultilineWhitespace()) { + "The node is $astNode while a multi-line $WHITE_SPACE expected" + } + + maybeIncrement() val whiteSpace = astNode.psi as PsiWhiteSpace - if (astNode.treeNext.isIndentDecrementing()) { + if (astNode.treeNext.getIndentationDecrement().isNonZero()) { // if newline is followed by closing token, it should already be indented less - context.dec(astNode.treeNext.elementType) + this -= astNode.treeNext.elementType } - val indentError = IndentationError(context.indent(), astNode.text.lastIndent()) + val indentError = IndentationError(indentation, astNode.text.lastIndent()) val checkResult = customIndentationCheckers.firstNotNullOfOrNull { it.checkNode(whiteSpace, indentError) @@ -226,11 +234,11 @@ class IndentationRule(configRules: List) : DiktatRule( val expectedIndent = checkResult?.expectedIndent ?: indentError.expected if (checkResult?.adjustNext == true && astNode.parents().none { it.elementType == LONG_STRING_TEMPLATE_ENTRY }) { val exceptionInitiatorNode = astNode.getExceptionalIndentInitiator() - context.addException(exceptionInitiatorNode, expectedIndent - indentError.expected, checkResult.includeLastChild) + addException(exceptionInitiatorNode, expectedIndent - indentError.expected, checkResult.includeLastChild) } if (astNode.treeParent.elementType == LONG_STRING_TEMPLATE_ENTRY && indentError.expected != indentError.actual) { - context.addException(astNode.treeParent, abs(indentError.expected - indentError.actual), false) + addException(astNode.treeParent, abs(indentError.expected - indentError.actual), false) } val difOffsetCloseAndOpenQuote = isCloseAndOpenQuoterOffset(astNode, indentError.actual) @@ -340,8 +348,8 @@ class IndentationRule(configRules: List) : DiktatRule( index == 0 || templateEntryFollowingNewline -> { fixFirstTemplateEntries( templateEntry, - expectedIndent = expectedIndent, - actualIndent = actualIndent) + expectedIndentation = expectedIndent, + actualIndentation = actualIndent) /* * Re-set the flag. @@ -367,15 +375,15 @@ class IndentationRule(configRules: List) : DiktatRule( * Also, it considers `$foo` insertions in a string. * * @param templateEntry a [LITERAL_STRING_TEMPLATE_ENTRY] node. - * @param expectedIndent the expected indent level, as returned by + * @param expectedIndentation the expected indentation level, as returned by * [IndentationError.expected]. - * @param actualIndent the actual indent level, as returned by + * @param actualIndentation the actual indentation level, as returned by * [IndentationError.actual]. */ private fun fixFirstTemplateEntries( templateEntry: ASTNode, - expectedIndent: Int, - actualIndent: Int + expectedIndentation: Int, + actualIndentation: Int ) { require(templateEntry.elementType == LITERAL_STRING_TEMPLATE_ENTRY) { "The elementType of this node is ${templateEntry.elementType} while $LITERAL_STRING_TEMPLATE_ENTRY expected" @@ -384,35 +392,37 @@ class IndentationRule(configRules: List) : DiktatRule( /* * Quite possible, do nothing in this case. */ - if (expectedIndent == actualIndent) { + if (expectedIndentation == actualIndentation) { return } - /* - * A `REGULAR_STRING_PART`. - */ - val regularStringPart = templateEntry.firstChildNode as LeafPsiElement - val regularStringPartText = regularStringPart.checkRegularStringPart().text - // shift of the node depending on its initial string template indent - val nodeStartIndent = (regularStringPartText.leadingSpaceCount() - actualIndent).unindent().zeroIfNegative() + withIndentationConfig(configuration) { + /* + * A `REGULAR_STRING_PART`. + */ + val regularStringPart = templateEntry.firstChildNode as LeafPsiElement + val regularStringPartText = regularStringPart.checkRegularStringPart().text + // shift of the node depending on its initial string template indentation + val nodeStartIndent = (regularStringPartText.leadingSpaceCount() - actualIndentation - SINGLE).zeroIfNegative() - val isPrevStringTemplate = templateEntry.treePrev.elementType in stringLiteralTokens - val isNextStringTemplate = templateEntry.treeNext.elementType in stringLiteralTokens + val isPrevStringTemplate = templateEntry.treePrev.elementType in stringLiteralTokens + val isNextStringTemplate = templateEntry.treeNext.elementType in stringLiteralTokens - val correctedText = when { - isPrevStringTemplate -> when { - isNextStringTemplate -> regularStringPartText + val correctedText = when { + isPrevStringTemplate -> when { + isNextStringTemplate -> regularStringPartText - // if string template is before literal_string - else -> regularStringPartText.trimEnd() + // if string template is before literal_string + else -> regularStringPartText.trimEnd() + } + + // if string template is after literal_string + // or if there is no string template in literal_string + else -> (expectedIndentation + SINGLE + nodeStartIndent).spaces + regularStringPartText.trimStart() } - // if string template is after literal_string - // or if there is no string template in literal_string - else -> (expectedIndent.indent() + nodeStartIndent).spaces + regularStringPartText.trimStart() + regularStringPart.rawReplaceWithText(correctedText) } - - regularStringPart.rawReplaceWithText(correctedText) } private fun ASTNode.getExceptionalIndentInitiator() = treeParent.let { parent -> @@ -427,97 +437,132 @@ class IndentationRule(configRules: List) : DiktatRule( } /** - * Increases the indentation level by [level] * [IndentationConfig.indentationSize]. + * Holds a mutable state needed to calculate the indentation and keep track + * of exceptions. * - * @param level the indentation level, 1 by default. - * @see unindent - * @see IndentationConfig.indentationSize - * @see IndentContext.maybeIncrement - * @see IndentContext.dec - */ - private fun Int.indent(level: Int = 1): Int = - this + level * configuration.indentationSize - - /** - * Decreases the indentation level by [level] * [IndentationConfig.indentationSize]. - * - * @param level the indentation level, 1 by default. - * @see indent - * @see IndentationConfig.indentationSize - * @see IndentContext.maybeIncrement - * @see IndentContext.dec - */ - private fun Int.unindent(level: Int = 1): Int = - this - level * configuration.indentationSize - - /** - * Class that contains state needed to calculate indent and keep track of exceptional indents. * Tokens from [increasingTokens] are stored in stack [activeTokens]. When [WHITE_SPACE] with line break is encountered, * if stack is not empty, indentation is increased. When token from [decreasingTokens] is encountered, it's counterpart is removed * from stack. If there has been a [WHITE_SPACE] with line break between them, indentation is decreased. + * + * @see IndentationConfigAware */ - private class IndentContext(private val config: IndentationConfig) { + private class IndentContext(config: IndentationConfig) : IndentationAware, IndentationConfigAware by IndentationConfigAware(config) { private var regularIndent = 0 private val exceptionalIndents: MutableList = mutableListOf() private val activeTokens: Stack = Stack() /** - * @param token a token that caused indentation increment, for example an opening brace - * @return Unit + * @return full current indentation. */ - fun storeIncrementingToken(token: IElementType) = token - .also { require(it in increasingTokens) { "Only tokens that increase indentation should be passed to this method" } } - .let(activeTokens::push) + @Suppress( + "CUSTOM_GETTERS_SETTERS", + "WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR", // #1464 + ) + override val indentation: Int + get() = + regularIndent + exceptionalIndents.sumOf(ExceptionalIndent::indentation) /** - * Checks whether indentation needs to be incremented and increments in this case. + * Pushes [token] onto the [stack][activeTokens], but doesn't increment + * the indentation. The indentation is incremented separately, see + * [maybeIncrement]. + * + * A call to this method **may or may not** be followed by a single call + * to [maybeIncrement]. * - * @see dec - * @see Int.indent - * @see Int.unindent + * @param token a token that caused indentation increment, any of + * [increasingTokens] (e.g.: an [opening brace][LPAR]). + * @see maybeIncrement + */ + fun storeIncrementingToken(token: IElementType) { + require(token in increasingTokens) { + "The token is $token while any of $increasingTokens expected" + } + + activeTokens.push(token) + } + + /** + * Increments the indentation if a multi-line [WHITE_SPACE] is + * encountered after an opening brace. + * + * A call to this method **always** has a preceding call to + * [storeIncrementingToken]. + * + * @see minusAssign */ fun maybeIncrement() { + val headOrNull: IElementType? = activeTokens.peek() + check(headOrNull == null || + headOrNull == WHITE_SPACE || + headOrNull in increasingTokens) { + "The head of the stack is $headOrNull while only $WHITE_SPACE or any of $increasingTokens expected" + } + if (activeTokens.isNotEmpty() && activeTokens.peek() != WHITE_SPACE) { - regularIndent += config.indentationSize + regularIndent += SINGLE activeTokens.push(WHITE_SPACE) } } /** - * @param token a token that caused indentation decrement, for example a closing brace + * Pops tokens from the [stack][activeTokens] and decrements the + * indentation accordingly. * + * @param token a token that caused indentation decrement, any of + * [decreasingTokens] (e.g.: a [closing brace][RPAR]). * @see maybeIncrement - * @see Int.indent - * @see Int.unindent */ - fun dec(token: IElementType) { + operator fun minusAssign(token: IElementType) { + require(token in decreasingTokens) { + "The token is $token while any of $decreasingTokens expected" + } + if (activeTokens.peek() == WHITE_SPACE) { + /*- + * In practice, there's always only a single `WHITE_SPACE` + * element type (representing the newline) pushed onto the stack + * after an opening brace (`LPAR` & friends), so it needs to be + * popped only once. + * + * Still, preserving the logic for compatibility. + */ while (activeTokens.peek() == WHITE_SPACE) { activeTokens.pop() } - regularIndent -= config.indentationSize + + /*- + * If an opening brace (`LPAR` etc.) was followed by a newline, + * this has led to the indentation being increased. + * + * Now, let's decrease it back to the original value. + */ + regularIndent -= SINGLE } + + /* + * In practice, the predicate is always `true` (provided braces are + * balanced) and can be replaced with a `check()` call. + */ if (activeTokens.isNotEmpty() && activeTokens.peek() == token.braceMatchOrNull()) { + /* + * Pop the matching opening brace. + */ activeTokens.pop() } } - /** - * @return full current indent - */ - fun indent() = regularIndent + exceptionalIndents.sumOf { it.indent } - /** * @param initiator a node that caused exceptional indentation - * @param indent an additional indent + * @param indentation an additional indentation. * @param includeLastChild whether the last child node should be included in the range affected by exceptional indentation * @return true if add exception in exceptionalIndents */ fun addException( initiator: ASTNode, - indent: Int, + indentation: Int, includeLastChild: Boolean - ) = exceptionalIndents.add(ExceptionalIndent(initiator, indent, includeLastChild)) + ) = exceptionalIndents.add(ExceptionalIndent(initiator, indentation, includeLastChild)) /** * @param astNode the node which is used to determine whether exceptional indents are still active @@ -527,16 +572,16 @@ class IndentationRule(configRules: List) : DiktatRule( /** * @property initiator a node that caused exceptional indentation - * @property indent an additional indent + * @property indentation an additional indentation. * @property includeLastChild whether the last child node should be included in the range affected by exceptional indentation */ private data class ExceptionalIndent( val initiator: ASTNode, - val indent: Int, + override val indentation: Int, val includeLastChild: Boolean = true - ) { + ) : IndentationAware { /** - * Checks whether this exceptional indent is still active. This is a hypotheses that exceptional indentation will end + * Checks whether this exceptional indentation is still active. This is a hypotheses that exceptional indentation will end * outside of node where it appeared, e.g. when an expression after assignment operator is over. * * @param currentNode the current node during AST traversal @@ -574,47 +619,57 @@ class IndentationRule(configRules: List) : DiktatRule( SPACE.toString().repeat(n = this) /** - * @return `true` if the indent should be incremented once this node is - * encountered, `false` otherwise. - * @see isIndentDecrementing + * @return the amount by which the indentation should be incremented + * once this node is encountered (may be [none][NONE]). + * @see ASTNode.getIndentationDecrement */ - private fun ASTNode.isIndentIncrementing(): Boolean = + private fun ASTNode.getIndentationIncrement(): IndentationAmount = when (elementType) { /* - * This is a special case of an opening parenthesis which *may* or - * *may not* be indent-incrementing. + * A special case of an opening parenthesis which *may* or *may not* + * increment the indentation. */ - LPAR -> isParenthesisAffectingIndent() + LPAR -> when { + isParenthesisAffectingIndent() -> SINGLE + else -> NONE + } - else -> elementType in increasingTokens + in increasingTokens -> SINGLE + + else -> NONE } /** - * @return `true` if the indent should be decremented once this node is - * encountered, `false` otherwise. - * @see isIndentIncrementing + * @return the amount by which the indentation should be decremented + * once this node is encountered (may be [none][NONE]). + * @see ASTNode.getIndentationIncrement */ - private fun ASTNode.isIndentDecrementing(): Boolean = + private fun ASTNode.getIndentationDecrement(): IndentationAmount = when (elementType) { /* - * This is a special case of a closing parenthesis which *may* or - * *may not* be indent-decrementing. + * A special case of a closing parenthesis which *may* or *may not* + * increment the indentation. */ - RPAR -> isParenthesisAffectingIndent() + RPAR -> when { + isParenthesisAffectingIndent() -> SINGLE + else -> NONE + } + + in decreasingTokens -> SINGLE - else -> elementType in decreasingTokens + else -> NONE } /** - * Parentheses always affect indent when they're a part of a + * Parentheses always affect indentation when they're a part of a * [VALUE_PARAMETER_LIST] (formal arguments) or a [VALUE_ARGUMENT_LIST] * (effective function call arguments). * * When they're children of a [PARENTHESIZED] (often inside a - * [BINARY_EXPRESSION]), contribute to the indent depending on whether - * there's a newline after the opening parenthesis. + * [BINARY_EXPRESSION]), contribute to the indentation depending on + * whether there's a newline after the opening parenthesis. * - * @return whether this [LPAR] or [RPAR] node affects indent. + * @return whether this [LPAR] or [RPAR] node affects indentation. * @see BINARY_EXPRESSION * @see PARENTHESIZED * @see VALUE_ARGUMENT_LIST @@ -629,13 +684,13 @@ class IndentationRule(configRules: List) : DiktatRule( PARENTHESIZED -> when (elementType) { /* * `LPAR` inside a binary expression only contributes to the - * indent if it's immediately followed by a newline. + * indentation if it's immediately followed by a newline. */ LPAR -> treeNext.isWhiteSpaceWithNewline() /* - * `RPAR` inside a binary expression affects the indent only - * if its matching `LPAR` node does so. + * `RPAR` inside a binary expression affects the indentation + * only if its matching `LPAR` node does so. */ else -> { val openingParenthesis = elementType.braceMatchOrNull()?.let { braceMatch -> @@ -654,6 +709,13 @@ class IndentationRule(configRules: List) : DiktatRule( } } + /** + * @return `true` if this is a [whitespace][WHITE_SPACE] node containing + * a [newline][NEWLINE], `false` otherwise. + */ + private fun ASTNode.isMultilineWhitespace(): Boolean = + elementType == WHITE_SPACE && textContains(NEWLINE) + /** * @return `true` if this is a [String.trimIndent] or [String.trimMargin] * call, `false` otherwise. diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/indentation/Checkers.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/indentation/Checkers.kt index 56b48ef4df..99ebd2458d 100644 --- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/indentation/Checkers.kt +++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/indentation/Checkers.kt @@ -4,6 +4,8 @@ package org.cqfn.diktat.ruleset.utils.indentation +import org.cqfn.diktat.ruleset.rules.chapter3.files.IndentationAmount +import org.cqfn.diktat.ruleset.rules.chapter3.files.IndentationAmount.SINGLE import org.cqfn.diktat.ruleset.rules.chapter3.files.IndentationError import org.cqfn.diktat.ruleset.utils.hasParent import org.cqfn.diktat.ruleset.utils.lastIndent @@ -64,7 +66,7 @@ internal class AssignmentOperatorChecker(configuration: IndentationConfig) : Cus val prevNode = whiteSpace.prevSibling?.node if (prevNode?.elementType == EQ && prevNode.treeNext.let { it.elementType == WHITE_SPACE && it.textContains('\n') }) { return CheckResult.from(indentError.actual, (whiteSpace.parentIndent() - ?: indentError.expected) + (if (configuration.extendedIndentForExpressionBodies) 2 else 1) * configuration.indentationSize, true) + ?: indentError.expected) + IndentationAmount.valueOf(configuration.extendedIndentForExpressionBodies), true) } return null } @@ -119,7 +121,7 @@ internal class ValueParameterListChecker(configuration: IndentationConfig) : Cus } .let { (_, line) -> line.substringBefore(parameterAfterLpar.text).length } } else if (configuration.extendedIndentOfParameters) { - indentError.expected + configuration.indentationSize + indentError.expected + SINGLE } else { indentError.expected } @@ -134,14 +136,16 @@ internal class ValueParameterListChecker(configuration: IndentationConfig) : Cus * Performs the following check: When breaking line after operators like +/-/`*` etc. new line can be indented with 8 space */ internal class ExpressionIndentationChecker(configuration: IndentationConfig) : CustomIndentationChecker(configuration) { - override fun checkNode(whiteSpace: PsiWhiteSpace, indentError: IndentationError): CheckResult? { - if (whiteSpace.parent.node.elementType == BINARY_EXPRESSION && whiteSpace.prevSibling.node.elementType == OPERATION_REFERENCE) { - val expectedIndent = (whiteSpace.parentIndent() ?: indentError.expected) + - (if (configuration.extendedIndentAfterOperators) 2 else 1) * configuration.indentationSize - return CheckResult.from(indentError.actual, expectedIndent, true) + override fun checkNode(whiteSpace: PsiWhiteSpace, indentError: IndentationError): CheckResult? = + when { + whiteSpace.parent.node.elementType == BINARY_EXPRESSION && whiteSpace.prevSibling.node.elementType == OPERATION_REFERENCE -> { + val parentIndent = whiteSpace.parentIndent() ?: indentError.expected + val expectedIndent = parentIndent + IndentationAmount.valueOf(configuration.extendedIndentAfterOperators) + CheckResult.from(indentError.actual, expectedIndent, true) + } + + else -> null } - return null - } } /** @@ -170,10 +174,10 @@ internal class SuperTypeListChecker(config: IndentationConfig) : CustomIndentati .treePrev .takeIf { it.elementType == WHITE_SPACE } ?.textContains('\n') ?: false - val expectedIndent = indentError.expected + (if (hasNewlineBeforeColon) 2 else 1) * configuration.indentationSize + val expectedIndent = indentError.expected + IndentationAmount.valueOf(extendedIndent = hasNewlineBeforeColon) return CheckResult.from(indentError.actual, expectedIndent) } else if (whiteSpace.parent.node.elementType == SUPER_TYPE_LIST) { - val expectedIndent = whiteSpace.parentIndent() ?: (indentError.expected + configuration.indentationSize) + val expectedIndent = whiteSpace.parentIndent() ?: (indentError.expected + SINGLE) return CheckResult.from(indentError.actual, expectedIndent) } return null @@ -213,7 +217,7 @@ internal class DotCallChecker(config: IndentationConfig) : CustomIndentationChec } || nextNode.isCommentBeforeDot()) && whiteSpace.parents.none { it.node.elementType == LONG_STRING_TEMPLATE_ENTRY } } ?.let { node -> - val indentIncrement = (if (configuration.extendedIndentBeforeDot) 2 else 1) * configuration.indentationSize + val indentIncrement = IndentationAmount.valueOf(configuration.extendedIndentBeforeDot) if (node.isFromStringTemplate()) { return CheckResult.from(indentError.actual, indentError.expected + indentIncrement, true) @@ -260,7 +264,7 @@ internal class ConditionalsAndLoopsWithoutBracesChecker(config: IndentationConfi } .takeIf { it } ?.let { - CheckResult.from(indentError.actual, indentError.expected + configuration.indentationSize, true) + CheckResult.from(indentError.actual, indentError.expected + SINGLE, true) } } } @@ -273,7 +277,7 @@ internal class CustomGettersAndSettersChecker(config: IndentationConfig) : Custo val parent = whiteSpace.parent if (parent is KtProperty && whiteSpace.nextSibling is KtPropertyAccessor) { return CheckResult.from(indentError.actual, (parent.parentIndent() - ?: indentError.expected) + configuration.indentationSize, true) + ?: indentError.expected) + SINGLE, true) } return null } @@ -287,7 +291,7 @@ internal class ArrowInWhenChecker(configuration: IndentationConfig) : CustomInde val prevNode = whiteSpace.prevSibling?.node if (prevNode?.elementType == ARROW && whiteSpace.parent is KtWhenEntry) { return CheckResult.from(indentError.actual, (whiteSpace.parentIndent() - ?: indentError.expected) + configuration.indentationSize, true) + ?: indentError.expected) + SINGLE, true) } return null } diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/indentation/CustomIndentationChecker.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/indentation/CustomIndentationChecker.kt index 8b9eec1051..c1cae755ae 100644 --- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/indentation/CustomIndentationChecker.kt +++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/indentation/CustomIndentationChecker.kt @@ -4,18 +4,22 @@ package org.cqfn.diktat.ruleset.utils.indentation +import org.cqfn.diktat.ruleset.rules.chapter3.files.IndentationConfigAware import org.cqfn.diktat.ruleset.rules.chapter3.files.IndentationError +import org.cqfn.diktat.ruleset.utils.NEWLINE + import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace /** * @property configuration configuration of indentation rule */ -internal abstract class CustomIndentationChecker(protected val configuration: IndentationConfig) { +internal abstract class CustomIndentationChecker(override val configuration: IndentationConfig) : IndentationConfigAware { /** * This method checks if this white space is an exception from general rule * If true, checks if it is properly indented and fixes * - * @param whiteSpace PSI element of type [PsiWhiteSpace] + * @param whiteSpace PSI element of type [PsiWhiteSpace]. The whitespace is + * guaranteed to contain a [newline][NEWLINE]. * @param indentError and [IndentationError] on this node * @return null true if node is not an exception, CheckResult otherwise */ diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationConfigAwareTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationConfigAwareTest.kt new file mode 100644 index 0000000000..60a3048c9c --- /dev/null +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationConfigAwareTest.kt @@ -0,0 +1,126 @@ +package org.cqfn.diktat.ruleset.chapter3.spaces + +import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestMixin.IndentationConfig +import org.cqfn.diktat.ruleset.rules.chapter3.files.IndentationAmount.EXTENDED +import org.cqfn.diktat.ruleset.rules.chapter3.files.IndentationAmount.NONE +import org.cqfn.diktat.ruleset.rules.chapter3.files.IndentationAmount.SINGLE +import org.cqfn.diktat.ruleset.rules.chapter3.files.IndentationConfigAware.Factory.withIndentationConfig + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.MethodOrderer.DisplayName +import org.junit.jupiter.api.TestMethodOrder +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +@TestMethodOrder(DisplayName::class) +class IndentationConfigAwareTest { + @ParameterizedTest(name = "indentationSize = {0}") + @ValueSource(ints = [2, 4, 8]) + fun `Int + IndentationAmount`(indentationSize: Int) { + val config = IndentationConfig("indentationSize" to indentationSize) + + withIndentationConfig(config) { + assertThat(42 + NONE).isEqualTo(42) + assertThat(42 + SINGLE).isEqualTo(42 + indentationSize) + assertThat(42 + EXTENDED).isEqualTo(42 + 2 * indentationSize) + } + } + + @ParameterizedTest(name = "indentationSize = {0}") + @ValueSource(ints = [2, 4, 8]) + fun `Int - IndentationAmount`(indentationSize: Int) { + val config = IndentationConfig("indentationSize" to indentationSize) + + withIndentationConfig(config) { + assertThat(42 - NONE).isEqualTo(42) + assertThat(42 - SINGLE).isEqualTo(42 - indentationSize) + assertThat(42 - EXTENDED).isEqualTo(42 - 2 * indentationSize) + } + } + + @ParameterizedTest(name = "indentationSize = {0}") + @ValueSource(ints = [2, 4, 8]) + fun `IndentationAmount + Int`(indentationSize: Int) { + val config = IndentationConfig("indentationSize" to indentationSize) + + withIndentationConfig(config) { + assertThat(NONE + 42).isEqualTo(42 + NONE) + assertThat(SINGLE + 42).isEqualTo(42 + SINGLE) + assertThat(EXTENDED + 42).isEqualTo(42 + EXTENDED) + + assertThat(42 + (SINGLE + 2)).isEqualTo((42 + SINGLE) + 2) + } + } + + @ParameterizedTest(name = "indentationSize = {0}") + @ValueSource(ints = [2, 4, 8]) + fun `IndentationAmount - Int`(indentationSize: Int) { + val config = IndentationConfig("indentationSize" to indentationSize) + + withIndentationConfig(config) { + assertThat(NONE - 42).isEqualTo(-(42 - NONE)) + assertThat(SINGLE - 42).isEqualTo(-(42 - SINGLE)) + assertThat(EXTENDED - 42).isEqualTo(-(42 - EXTENDED)) + + assertThat(42 - (SINGLE - 2)).isEqualTo(42 - SINGLE + 2) + } + } + + @ParameterizedTest(name = "indentationSize = {0}") + @ValueSource(ints = [2, 4, 8]) + fun `IndentationAmount + IndentationAmount`(indentationSize: Int) { + val config = IndentationConfig("indentationSize" to indentationSize) + + withIndentationConfig(config) { + assertThat(NONE + SINGLE).isEqualTo(0 + SINGLE) + assertThat(SINGLE + SINGLE).isEqualTo(0 + EXTENDED) + + assertThat(42 + SINGLE + SINGLE).isEqualTo(42 + EXTENDED) + assertThat(42 + (SINGLE + SINGLE)).isEqualTo(42 + EXTENDED) + } + } + + @ParameterizedTest(name = "indentationSize = {0}") + @ValueSource(ints = [2, 4, 8]) + fun `IndentationAmount - IndentationAmount`(indentationSize: Int) { + val config = IndentationConfig("indentationSize" to indentationSize) + + withIndentationConfig(config) { + assertThat(NONE - SINGLE).isEqualTo(0 - SINGLE) + assertThat(SINGLE - SINGLE).isEqualTo(0) + assertThat(EXTENDED - SINGLE).isEqualTo(0 + SINGLE) + assertThat(NONE - EXTENDED).isEqualTo(0 - EXTENDED) + + assertThat(42 + (SINGLE - SINGLE)).isEqualTo(42 + SINGLE - SINGLE) + } + } + + @ParameterizedTest(name = "indentationSize = {0}") + @ValueSource(ints = [2, 4, 8]) + fun unaryPlus(indentationSize: Int) { + val config = IndentationConfig("indentationSize" to indentationSize) + + withIndentationConfig(config) { + assertThat(+NONE).isEqualTo(0) + assertThat(+SINGLE).isEqualTo(indentationSize) + assertThat(+EXTENDED).isEqualTo(2 * indentationSize) + + assertThat(EXTENDED - SINGLE).isEqualTo(+SINGLE) + } + } + + @ParameterizedTest(name = "indentationSize = {0}") + @ValueSource(ints = [2, 4, 8]) + fun unaryMinus(indentationSize: Int) { + val config = IndentationConfig("indentationSize" to indentationSize) + + withIndentationConfig(config) { + assertThat(-NONE).isEqualTo(0) + assertThat(-SINGLE).isEqualTo(-indentationSize) + assertThat(-EXTENDED).isEqualTo(-2 * indentationSize) + + assertThat(NONE - SINGLE).isEqualTo(-SINGLE) + assertThat(NONE - EXTENDED).isEqualTo(-EXTENDED) + } + } +} diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleTestMixin.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleTestMixin.kt index 0b5759aab5..e7cbb8336d 100644 --- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleTestMixin.kt +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleTestMixin.kt @@ -21,7 +21,9 @@ internal object IndentationRuleTestMixin { */ @Suppress("TestFunctionName", "FUNCTION_NAME_INCORRECT_CASE") fun IndentationConfig(vararg configEntries: Pair): IndentationConfig = - IndentationConfig(mapOf(*configEntries).mapValues(Any::toString)) + IndentationConfig(mapOf(*configEntries).mapValues { (_, value) -> + value.toString() + }) /** * @param configEntries the optional values which override the state of this diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/DiktatRuleSetProvider4Test.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/DiktatRuleSetProvider4Test.kt index 4cc3705dfd..a8aba0c506 100644 --- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/DiktatRuleSetProvider4Test.kt +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/DiktatRuleSetProvider4Test.kt @@ -40,6 +40,18 @@ class DiktatRuleSetProviderTest { val filesName = File(path) .walk() .filter { it.isFile } + .filter { file -> + /* + * Include only those files which contain `Rule` or `DiktatRule` + * descendants (any of the 1st 150 lines contains a superclass + * constructor call). + */ + val constructorCall = Regex(""":\s*(?:Diktat)?Rule\s*\(""") + file.bufferedReader().lineSequence().take(150) + .any { line -> + line.contains(constructorCall) + } + } .map { it.nameWithoutExtension } .filterNot { it in ignoreFile } val rulesName = DiktatRuleSetProvider().get() @@ -61,9 +73,8 @@ class DiktatRuleSetProviderTest { companion object { private val ignoreFile = listOf( - "DiktatRuleSetProvider", "DiktatRule", - "IndentationError", - "OrderedRuleSet") + "OrderedRuleSet", + ) } }