Repository: cqfn/diKTat (kotlin)

Issue:
Rule for replacing range with Until
```(it .. this.length - 1)``` -> ```(it until this.length)```

Pull request:
### What's done:
* [x] Implemented Rule, added Warnings
* [x] Added tests on checks
* [x] Added tests on fixers
* [x] Updated diktat-analysis.yml
* [x] Updated available-rules.md
* [x] Closes #979

Patch:
diff --git a/diktat-analysis.yml b/diktat-analysis.yml
index b0c040c1f6..6e74a2b95c 100644
--- a/diktat-analysis.yml
+++ b/diktat-analysis.yml
@@ -507,4 +507,9 @@
   enabled: true
 # Check if boolean expression can be simplified
 - name: COMPLEX_BOOLEAN_EXPRESSION
-  enabled: true
\ No newline at end of file
+  enabled: true
+# Check if range can replace with until or `rangeTo` function with range
+- name: CONVENTIONAL_RANGE
+  enabled: true
+  configuration:
+    isRangeToIgnore: false
\ No newline at end of file
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/constants/Warnings.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/constants/Warnings.kt
index 8ce0ef7f88..40cbe88091 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/constants/Warnings.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/constants/Warnings.kt
@@ -135,6 +135,7 @@ enum class Warnings(
     STRING_TEMPLATE_QUOTES(true, "3.15.2", "string template has redundant quotes"),
     FILE_NAME_MATCH_CLASS(true, "3.1.2", "file name is incorrect - it should match with the class described in it if there is the only one class declared"),
     COLLAPSE_IF_STATEMENTS(true, "3.16.1", "avoid using redundant nested if-statements, which could be collapsed into a single one"),
+    CONVENTIONAL_RANGE(true, "3.17.1", "use conventional rule for range case"),
 
     // ======== chapter 4 ========
     NULLABLE_PROPERTY_TYPE(true, "4.3.1", "try to avoid use of nullable types"),
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/DiktatRuleSetProvider.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/DiktatRuleSetProvider.kt
index fc8733acb3..a8c6ce53f6 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/DiktatRuleSetProvider.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/DiktatRuleSetProvider.kt
@@ -28,6 +28,7 @@ import org.cqfn.diktat.ruleset.rules.chapter3.LongNumericalValuesSeparatedRule
 import org.cqfn.diktat.ruleset.rules.chapter3.MagicNumberRule
 import org.cqfn.diktat.ruleset.rules.chapter3.MultipleModifiersSequence
 import org.cqfn.diktat.ruleset.rules.chapter3.NullableTypeRule
+import org.cqfn.diktat.ruleset.rules.chapter3.RangeConventionalRule
 import org.cqfn.diktat.ruleset.rules.chapter3.SingleLineStatementsRule
 import org.cqfn.diktat.ruleset.rules.chapter3.SortRule
 import org.cqfn.diktat.ruleset.rules.chapter3.StringConcatenationRule
@@ -179,6 +180,7 @@ class DiktatRuleSetProvider(private var diktatConfigFile: String = DIKTAT_ANALYS
             ::AbstractClassesRule,
             ::TrailingCommaRule,
             ::SingleInitRule,
+            ::RangeConventionalRule,
             ::CustomLabel,
             ::VariableGenericTypeDeclarationRule,
             ::LongNumericalValuesSeparatedRule,
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/RangeConventionalRule.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/RangeConventionalRule.kt
new file mode 100644
index 0000000000..d9f6d1ff05
--- /dev/null
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/RangeConventionalRule.kt
@@ -0,0 +1,116 @@
+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.CONVENTIONAL_RANGE
+import org.cqfn.diktat.ruleset.rules.DiktatRule
+import org.cqfn.diktat.ruleset.utils.KotlinParser
+import org.cqfn.diktat.ruleset.utils.getIdentifierName
+import org.cqfn.diktat.ruleset.utils.hasChildOfType
+import org.cqfn.diktat.ruleset.utils.takeByChainOfTypes
+
+import com.pinterest.ktlint.core.ast.ElementType.BINARY_EXPRESSION
+import com.pinterest.ktlint.core.ast.ElementType.DOT_QUALIFIED_EXPRESSION
+import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER
+import com.pinterest.ktlint.core.ast.ElementType.MINUS
+import com.pinterest.ktlint.core.ast.ElementType.RANGE
+import com.pinterest.ktlint.core.ast.ElementType.REFERENCE_EXPRESSION
+import com.pinterest.ktlint.core.ast.ElementType.VALUE_ARGUMENT
+import com.pinterest.ktlint.core.ast.ElementType.VALUE_ARGUMENT_LIST
+import com.pinterest.ktlint.core.ast.isWhiteSpace
+import com.pinterest.ktlint.core.ast.parent
+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.TokenSet
+import org.jetbrains.kotlin.psi.KtBinaryExpression
+import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
+
+/**
+ * This rule warn and fix cases when it possible to replace range with until or replace rangeTo function with range
+ */
+@Suppress("UnsafeCallOnNullableType")
+class RangeConventionalRule(configRules: List<RulesConfig>) : DiktatRule(
+    "range",
+    configRules,
+    listOf(CONVENTIONAL_RANGE)
+) {
+    private val configuration by lazy {
+        RangeConventionalConfiguration(
+            this.configRules.getRuleConfig(CONVENTIONAL_RANGE)?.configuration ?: emptyMap(),
+        )
+    }
+    override fun logic(node: ASTNode) {
+        if (node.elementType == DOT_QUALIFIED_EXPRESSION && !configuration.isRangeToIgnore) {
+            handleQualifiedExpression(node)
+        }
+        if (node.elementType == RANGE) {
+            handleRange(node)
+        }
+    }
+
+    @Suppress("TOO_MANY_LINES_IN_LAMBDA")
+    private fun handleQualifiedExpression(node: ASTNode) {
+        (node.psi as KtDotQualifiedExpression).selectorExpression?.node?.let {
+            if (it.findChildByType(REFERENCE_EXPRESSION)?.getIdentifierName()?.text == "rangeTo") {
+                val arguments = it.findChildByType(VALUE_ARGUMENT_LIST)?.getChildren(TokenSet.create(VALUE_ARGUMENT))
+                if (arguments?.size == 1) {
+                    CONVENTIONAL_RANGE.warnAndFix(configRules, emitWarn, isFixMode, "replace `rangeTo` with `..`: ${node.text}", node.startOffset, node) {
+                        val receiverExpression = (node.psi as KtDotQualifiedExpression).receiverExpression.text
+                        val correctNode = KotlinParser().createNode("$receiverExpression..${arguments[0].text}")
+                        node.treeParent.addChild(correctNode, node)
+                        node.treeParent.removeChild(node)
+                    }
+                }
+            }
+        }
+    }
+
+    @Suppress("TOO_MANY_LINES_IN_LAMBDA")
+    private fun handleRange(node: ASTNode) {
+        val binaryInExpression = (node.parent({ it.elementType == BINARY_EXPRESSION })?.psi as KtBinaryExpression?)
+        (binaryInExpression
+            ?.right
+            ?.node
+            ?.takeByChainOfTypes(BINARY_EXPRESSION)
+            ?.psi as KtBinaryExpression?)
+            ?.let {
+                if (it.operationReference.node.hasChildOfType(MINUS)) {
+                    val errorNode = binaryInExpression!!.node
+                    CONVENTIONAL_RANGE.warnAndFix(configRules, emitWarn, isFixMode, "replace `..` with `until`: ${errorNode.text}", errorNode.startOffset, errorNode) {
+                        replaceUntil(node)
+                        // fix right side of binary expression to correct form (remove ` - 1 `) : (b-1) -> (b)
+                        val astNode = it.node
+                        val parent = astNode.treeParent
+                        parent.addChild(astNode.firstChildNode, astNode)
+                        parent.removeChild(astNode)
+                    }
+                }
+            }
+    }
+
+    private fun replaceUntil(node: ASTNode) {
+        val untilNode = LeafPsiElement(IDENTIFIER, "until")
+        val parent = node.treeParent
+        if (parent.treePrev?.isWhiteSpace() != true) {
+            parent.treeParent.addChild(PsiWhiteSpaceImpl(" "), parent)
+        }
+        if (parent.treeNext?.isWhiteSpace() != true) {
+            parent.treeParent.addChild(PsiWhiteSpaceImpl(" "), parent.treeNext)
+        }
+        parent.addChild(untilNode, node)
+        parent.removeChild(node)
+    }
+
+    /**
+     *
+     * [RuleConfiguration] for rangeTo function
+     */
+    class RangeConventionalConfiguration(config: Map<String, String>) : RuleConfiguration(config) {
+        /**
+         * Does ignore rangeTo function
+         */
+        val isRangeToIgnore = config["isRangeToIgnore"]?.toBoolean() ?: false
+    }
+}
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtils.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtils.kt
index 93def29f70..100fe34016 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtils.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtils.kt
@@ -32,6 +32,7 @@ import com.pinterest.ktlint.core.ast.ElementType.LBRACE
 import com.pinterest.ktlint.core.ast.ElementType.MODIFIER_LIST
 import com.pinterest.ktlint.core.ast.ElementType.OPERATION_REFERENCE
 import com.pinterest.ktlint.core.ast.ElementType.OVERRIDE_KEYWORD
+import com.pinterest.ktlint.core.ast.ElementType.PARENTHESIZED
 import com.pinterest.ktlint.core.ast.ElementType.PRIVATE_KEYWORD
 import com.pinterest.ktlint.core.ast.ElementType.PROTECTED_KEYWORD
 import com.pinterest.ktlint.core.ast.ElementType.PUBLIC_KEYWORD
@@ -802,6 +803,24 @@ fun ASTNode.hasEqBinaryExpression(): Boolean =
 fun ASTNode.getLineNumber(): Int =
         calculateLineNumber()
 
+/**
+ * Get node by taking children by types and ignore `PARENTHESIZED`
+ *
+ * @return child of type
+ */
+fun ASTNode.takeByChainOfTypes(vararg types: IElementType): ASTNode? {
+    var node: ASTNode? = this
+    types.forEach {
+        node = node?.findChildByType(it) ?: run {
+            while (node?.hasChildOfType(PARENTHESIZED) == true) {
+                node = node?.findChildByType(PARENTHESIZED)
+            }
+            node?.findChildByType(it)
+        }
+    }
+    return node
+}
+
 private fun ASTNode.findOffsetByLine(line: Int, positionByOffset: (Int) -> Pair<Int, Int>): Int {
     val currentLine = this.getLineNumber()
     val currentOffset = this.startOffset
diff --git a/diktat-rules/src/main/resources/diktat-analysis-huawei.yml b/diktat-rules/src/main/resources/diktat-analysis-huawei.yml
index f9b02b4d66..0ec2835e11 100644
--- a/diktat-rules/src/main/resources/diktat-analysis-huawei.yml
+++ b/diktat-rules/src/main/resources/diktat-analysis-huawei.yml
@@ -507,4 +507,9 @@
   enabled: true
 # Check if boolean expression can be simplified
 - name: COMPLEX_BOOLEAN_EXPRESSION
-  enabled: true
\ No newline at end of file
+  enabled: true
+# Check if range can replace with until or `rangeTo` function with range
+- name: CONVENTIONAL_RANGE
+  enabled: true
+  configuration:
+    isRangeToIgnore: false
\ No newline at end of file
diff --git a/diktat-rules/src/main/resources/diktat-analysis.yml b/diktat-rules/src/main/resources/diktat-analysis.yml
index 8577ad31b3..9118fea459 100644
--- a/diktat-rules/src/main/resources/diktat-analysis.yml
+++ b/diktat-rules/src/main/resources/diktat-analysis.yml
@@ -503,4 +503,9 @@
   enabled: true
 # Check if boolean expression can be simplified
 - name: COMPLEX_BOOLEAN_EXPRESSION
-  enabled: true
\ No newline at end of file
+  enabled: true
+# Check if range can replace with until or `rangeTo` function with range
+- name: CONVENTIONAL_RANGE
+  enabled: true
+  configuration:
+    isRangeToIgnore: false
\ No newline at end of file
diff --git a/info/available-rules.md b/info/available-rules.md
index 73e5139203..a772f2c29e 100644
--- a/info/available-rules.md
+++ b/info/available-rules.md
@@ -89,6 +89,7 @@
 |    3  |    3.15.2  |  STRING_TEMPLATE_QUOTES                       |  Check: warns if there are redundant quotes in string template<br> Fix: deletes quotes and $ symbol                    |  yes  |  no  |  + |
 |    3  |    3.16.1  |  COLLAPSE_IF_STATEMENTS                   | Check: warns if there are redundant nested if-statements, which could be collapsed into a single one by concatenating their conditions                                       |  yes  |  no    |   -  |
 |    3  |    3.16.2  |  COMPLEX_BOOLEAN_EXPRESSION                   | Check: warns if boolean expression is complex and can be simplified.<br>Fix: replaces boolean expression with the simpler one                                    |  yes  |  no    |   +  |
+|    3  |      3.17  |  CONVENTIONAL_RANGE                           | Check: warns if possible to replace range with until or replace `rangeTo` function with range.<br>Fix: replace range with until or replace `rangeTo` function with range                                  |  yes  |  no    |   -  |
 |    4  |     4.2.1  |  SMART_CAST_NEEDED                      |  Check: warns if casting can be omitted<br>Fix: Deletes casting                                                                                                                |  yes  |  no  |  -  | |
 |    4  |     4.3.1  |  NULLABLE_PROPERTY_TYPE                   | Check: warns if immutable property is initialized with null or if immutable property can have non-nullable type instead of nullable<br>Fix: suggests initial value instead of null or changes immutable property type |  yes  |  no  |  - |
 |    4  |     4.2.2  |  TYPE_ALIAS                               |  Check: if type reference of property is longer than expected                                                                                                                  |  yes  |  typeReferenceLength        |                                                                                      -|


Test patch:
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/RangeConventionalRuleFixTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/RangeConventionalRuleFixTest.kt
new file mode 100644
index 0000000000..52cc9a3189
--- /dev/null
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/RangeConventionalRuleFixTest.kt
@@ -0,0 +1,17 @@
+package org.cqfn.diktat.ruleset.chapter3
+
+import org.cqfn.diktat.ruleset.rules.chapter3.RangeConventionalRule
+import org.cqfn.diktat.util.FixTestBase
+import org.junit.jupiter.api.Test
+
+class RangeConventionalRuleFixTest : FixTestBase("test/paragraph3/range", ::RangeConventionalRule) {
+    @Test
+    fun `should fix with until`() {
+        fixAndCompare("RangeToUntilExpected.kt", "RangeToUntilTest.kt")
+    }
+
+    @Test
+    fun `should fix with rangeTo`() {
+        fixAndCompare("RangeToExpected.kt", "RangeToTest.kt")
+    }
+}
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/RangeConventionalRuleWarnTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/RangeConventionalRuleWarnTest.kt
new file mode 100644
index 0000000000..28aaa23452
--- /dev/null
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/RangeConventionalRuleWarnTest.kt
@@ -0,0 +1,70 @@
+package org.cqfn.diktat.ruleset.chapter3
+
+import org.cqfn.diktat.common.config.rules.RulesConfig
+import org.cqfn.diktat.ruleset.constants.Warnings
+import org.cqfn.diktat.ruleset.rules.DIKTAT_RULE_SET_ID
+import org.cqfn.diktat.ruleset.rules.chapter3.RangeConventionalRule
+import org.cqfn.diktat.util.LintTestBase
+
+import com.pinterest.ktlint.core.LintError
+import org.junit.jupiter.api.Test
+
+class RangeConventionalRuleWarnTest : LintTestBase(::RangeConventionalRule) {
+    private val ruleId = "$DIKTAT_RULE_SET_ID:range"
+    private val rulesConfigRangeRule: List<RulesConfig> = listOf(
+        RulesConfig(
+            Warnings.CONVENTIONAL_RANGE.name, true,
+            mapOf("isRangeToIgnore" to "true"))
+    )
+
+    @Test
+    fun `check simple examples with until`() {
+        lintMethod(
+            """
+                    |fun foo() {   
+                    |    for (i in 1..(4 - 1)) print(i)
+                    |    for (i in 1..(b - 1)) print(i)
+                    |    for (i in ((1 .. ((4 - 1))))) print(i)
+                    |    for (i in 4 downTo 1) print(i)
+                    |    for (i in 1..4) print(i)
+                    |    for (i in 1..4 step 2) print(i)
+                    |    for (i in 4 downTo 1 step 3) print(i)
+                    |    if (6 in (1..10) && true) {}
+                    |    for (i in 1..(4 - 1) step 3) print(i)
+                    |}
+                """.trimMargin(),
+            LintError(2, 15, ruleId, "${Warnings.CONVENTIONAL_RANGE.warnText()} replace `..` with `until`: 1..(4 - 1)", true),
+            LintError(3, 15, ruleId, "${Warnings.CONVENTIONAL_RANGE.warnText()} replace `..` with `until`: 1..(b - 1)", true),
+            LintError(4, 17, ruleId, "${Warnings.CONVENTIONAL_RANGE.warnText()} replace `..` with `until`: 1 .. ((4 - 1))", true),
+            LintError(10, 15, ruleId, "${Warnings.CONVENTIONAL_RANGE.warnText()} replace `..` with `until`: 1..(4 - 1)", true)
+        )
+    }
+
+    @Test
+    fun `check simple examples with rangeTo`() {
+        lintMethod(
+            """
+                    |fun foo() {   
+                    |    val num = 1
+                    |    val w = num.rangeTo(num, num)
+                    |    val q = 1..5
+                    |    val w = num.rangeTo(num)
+                    |}
+                """.trimMargin(),
+            LintError(5, 13, ruleId, "${Warnings.CONVENTIONAL_RANGE.warnText()} replace `rangeTo` with `..`: num.rangeTo(num)", true)
+        )
+    }
+
+    @Test
+    fun `check simple examples with rangeTo with config`() {
+        lintMethod(
+            """
+                    |fun foo() {   
+                    |    val w = num.rangeTo(num, num)
+                    |    val w = num.rangeTo(num)
+                    |}
+                """.trimMargin(),
+            rulesConfigList = rulesConfigRangeRule
+        )
+    }
+}
diff --git a/diktat-rules/src/test/resources/test/paragraph3/range/RangeToExpected.kt b/diktat-rules/src/test/resources/test/paragraph3/range/RangeToExpected.kt
new file mode 100644
index 0000000000..7f80c5cf9b
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/paragraph3/range/RangeToExpected.kt
@@ -0,0 +1,9 @@
+package test.paragraph3.range
+
+fun foo() {
+    val num = 1
+    val w = num.rangeTo(num, num)
+    val q = 1..5
+    val e = num..num
+    if (1 in num..10) { }
+}
diff --git a/diktat-rules/src/test/resources/test/paragraph3/range/RangeToTest.kt b/diktat-rules/src/test/resources/test/paragraph3/range/RangeToTest.kt
new file mode 100644
index 0000000000..c618114c1c
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/paragraph3/range/RangeToTest.kt
@@ -0,0 +1,9 @@
+package test.paragraph3.range
+
+fun foo() {
+    val num = 1
+    val w = num.rangeTo(num, num)
+    val q = 1..5
+    val e = num.rangeTo(num)
+    if (1 in num.rangeTo(10)) { }
+}
diff --git a/diktat-rules/src/test/resources/test/paragraph3/range/RangeToUntilExpected.kt b/diktat-rules/src/test/resources/test/paragraph3/range/RangeToUntilExpected.kt
new file mode 100644
index 0000000000..4aac33e657
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/paragraph3/range/RangeToUntilExpected.kt
@@ -0,0 +1,15 @@
+package test.paragraph3.range
+
+class A {
+    fun foo() {
+        for (i in 1..4) print(i)
+        for (i in 4 downTo 1) print(i)
+        for (i in 1 until 4) print(i)
+        for (i in 1..4 step 2) print(i)
+        for (i in 4 downTo 1 step 3) print(i)
+        if (6 in (1..10) && true) {}
+        for (i in 1 until (4)) print(i)
+        for (i in 1 until (b)) print(i)
+        for (i in ((1 until ((4))))) print(i)
+    }
+}
\ No newline at end of file
diff --git a/diktat-rules/src/test/resources/test/paragraph3/range/RangeToUntilTest.kt b/diktat-rules/src/test/resources/test/paragraph3/range/RangeToUntilTest.kt
new file mode 100644
index 0000000000..34681ea975
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/paragraph3/range/RangeToUntilTest.kt
@@ -0,0 +1,15 @@
+package test.paragraph3.range
+
+class A {
+    fun foo() {
+        for (i in 1..4) print(i)
+        for (i in 4 downTo 1) print(i)
+        for (i in 1 until 4) print(i)
+        for (i in 1..4 step 2) print(i)
+        for (i in 4 downTo 1 step 3) print(i)
+        if (6 in (1..10) && true) {}
+        for (i in 1..(4 - 1)) print(i)
+        for (i in 1..(b - 1)) print(i)
+        for (i in ((1 .. ((4 - 1))))) print(i)
+    }
+}