diff --git a/README.md b/README.md
index 8ee180a696..fe82b07bda 100644
--- a/README.md
+++ b/README.md
@@ -26,9 +26,9 @@ Now diKTat was already added to the lists of [static analysis tools](https://git
## See first
-| | | | | |
-| --- | --- | --- | --- | --- |
-|[DiKTat codestyle](info/guide/diktat-coding-convention.md)|[Supported Rules](info/available-rules.md) | [Examples of Usage](https://github.com/akuleshov7/diktat-examples) | [Online Demo](https://ktlint-demo.herokuapp.com) | [White Paper](wp/wp.pdf) |
+| | | | | | |
+| --- | --- | --- | --- | --- | --- |
+|[Codestyle](info/guide/diktat-coding-convention.md)|[Inspections](info/available-rules.md) | [Examples](https://github.com/akuleshov7/diktat-examples) | [Demo](https://ktlint-demo.herokuapp.com) | [White Paper](wp/wp.pdf) | [Groups of Inspections](info/rules-mapping.md) |
## Why should I use diktat in my CI/CD?
@@ -64,7 +64,7 @@ Main features of diktat are the following:
3. Finally, run KTlint (with diKTat injected) to check your `*.kt` files in `dir/your/dir`:
```bash
- $ ./ktlint -R diktat.jar "dir/your/dir/**/*.kt"
+ $ ./ktlint -R diktat.jar --disabled_rules=standard "dir/your/dir/**/*.kt"
```
To **autofix** all code style violations use `-F` option.
diff --git a/diktat-analysis.yml b/diktat-analysis.yml
index 3e370cd232..0f0be5e505 100644
--- a/diktat-analysis.yml
+++ b/diktat-analysis.yml
@@ -7,7 +7,7 @@
# testDirs: test
disabledChapters: ""
testDirs: test
- kotlinVersion: "1.4.21"
+ kotlinVersion: 1.4.30
# Checks that the Class/Enum/Interface name does not match Pascal case
- name: CLASS_NAME_INCORRECT
enabled: true
@@ -183,6 +183,9 @@
# Checks that properties with comments are separated by a blank line
- name: BLANK_LINE_BETWEEN_PROPERTIES
enabled: true
+# Checks top level order
+- name: TOP_LEVEL_ORDER
+ enabled: true
# Checks that non-empty code blocks with braces follow the K&R style (1TBS or OTBS style)
- name: BRACES_BLOCK_STRUCTURE_ERROR
enabled: true
diff --git a/diktat-common/pom.xml b/diktat-common/pom.xml
index f17f979ede..9414be5f08 100644
--- a/diktat-common/pom.xml
+++ b/diktat-common/pom.xml
@@ -25,7 +25,7 @@
com.charleskorn.kaml
kaml
- 0.26.0
+ 0.27.0
commons-cli
diff --git a/diktat-common/src/main/kotlin/org/cqfn/diktat/common/config/rules/RulesConfigReader.kt b/diktat-common/src/main/kotlin/org/cqfn/diktat/common/config/rules/RulesConfigReader.kt
index 1c34c2a34d..94394510b2 100644
--- a/diktat-common/src/main/kotlin/org/cqfn/diktat/common/config/rules/RulesConfigReader.kt
+++ b/diktat-common/src/main/kotlin/org/cqfn/diktat/common/config/rules/RulesConfigReader.kt
@@ -90,13 +90,6 @@ open class RulesConfigReader(override val classLoader: ClassLoader) : JsonResour
}
}
-/**
- * @return common configuration from list of all rules configuration
- */
-fun List.getCommonConfiguration() = lazy {
- CommonConfiguration(getCommonConfig()?.configuration)
-}
-
/**
* class returns the list of common configurations that we have read from a configuration map
*
@@ -142,6 +135,13 @@ data class CommonConfiguration(private val configuration: Map?)
// ================== utils for List from yml config
+/**
+ * @return common configuration from list of all rules configuration
+ */
+fun List.getCommonConfiguration() = lazy {
+ CommonConfiguration(getCommonConfig()?.configuration)
+}
+
/**
* Get [RulesConfig] for particular [Rule] object.
*
@@ -150,11 +150,6 @@ data class CommonConfiguration(private val configuration: Map?)
*/
fun List.getRuleConfig(rule: Rule): RulesConfig? = this.find { it.name == rule.ruleName() }
-/**
- * Get [RulesConfig] representing common configuration part that can be used in any rule
- */
-private fun List.getCommonConfig() = find { it.name == DIKTAT_COMMON }
-
/**
* checking if in yml config particular rule is enabled or disabled
* (!) the default value is "true" (in case there is no config specified)
@@ -183,3 +178,8 @@ fun String.kotlinVersion(): KotlinVersion {
KotlinVersion(versions[0], versions[1], versions[2])
}
}
+
+/**
+ * Get [RulesConfig] representing common configuration part that can be used in any rule
+ */
+private fun List.getCommonConfig() = find { it.name == DIKTAT_COMMON }
diff --git a/diktat-common/src/test/kotlin/org/cqfn/diktat/test/ConfigReaderTest.kt b/diktat-common/src/test/kotlin/org/cqfn/diktat/test/ConfigReaderTest.kt
index b1b624a80e..4c0678fe65 100644
--- a/diktat-common/src/test/kotlin/org/cqfn/diktat/test/ConfigReaderTest.kt
+++ b/diktat-common/src/test/kotlin/org/cqfn/diktat/test/ConfigReaderTest.kt
@@ -23,12 +23,12 @@ class ConfigReaderTest {
fun `testing kotlin version`() {
val rulesConfigList: List? = RulesConfigReader(javaClass.classLoader)
.readResource("src/test/resources/test-rules-config.yml")
- val currentKotlinVersion = KotlinVersion.CURRENT
+ val kotlinVersionForTest = KotlinVersion(1, 4, 21)
requireNotNull(rulesConfigList)
- assert(rulesConfigList.getCommonConfiguration().value.kotlinVersion == currentKotlinVersion)
+ assert(rulesConfigList.getCommonConfiguration().value.kotlinVersion == kotlinVersionForTest)
assert(rulesConfigList.find { it.name == DIKTAT_COMMON }
?.configuration
?.get("kotlinVersion")
- ?.kotlinVersion() == currentKotlinVersion)
+ ?.kotlinVersion() == kotlinVersionForTest)
}
}
diff --git a/diktat-gradle-plugin/build.gradle.kts b/diktat-gradle-plugin/build.gradle.kts
index 467346ebb4..daa17dcf97 100644
--- a/diktat-gradle-plugin/build.gradle.kts
+++ b/diktat-gradle-plugin/build.gradle.kts
@@ -3,7 +3,7 @@ import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.getCurr
plugins {
`java-gradle-plugin`
- kotlin("jvm") version "1.4.21"
+ kotlin("jvm") version "1.4.30"
jacoco
id("pl.droidsonroids.jacoco.testkit") version "1.0.7"
}
diff --git a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/Utils.kt b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/Utils.kt
index f5f8415f5b..58ee144069 100644
--- a/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/Utils.kt
+++ b/diktat-gradle-plugin/src/main/kotlin/org/cqfn/diktat/plugin/gradle/Utils.kt
@@ -2,18 +2,12 @@
* Utilities for diktat gradle plugin
*/
-@file:Suppress("FILE_NAME_MATCH_CLASS")
+@file:Suppress("FILE_NAME_MATCH_CLASS", "MatchingDeclarationName")
package org.cqfn.diktat.plugin.gradle
import groovy.lang.Closure
-// These two are copy-pasted from `kotlin-dsl` plugin's groovy interop.
-// Because `kotlin-dsl` depends on kotlin 1.3.x.
-@Suppress("MISSING_KDOC_TOP_LEVEL", "MISSING_KDOC_ON_FUNCTION", "KDOC_WITHOUT_PARAM_TAG", "KDOC_WITHOUT_RETURN_TAG")
-fun Any.closureOf(action: T.() -> Unit): Closure =
- KotlinClosure1(action, this, this)
-
@Suppress("MISSING_KDOC_TOP_LEVEL", "MISSING_KDOC_CLASS_ELEMENTS", "KDOC_NO_CONSTRUCTOR_PROPERTY",
"MISSING_KDOC_ON_FUNCTION", "KDOC_WITHOUT_PARAM_TAG", "KDOC_WITHOUT_RETURN_TAG")
class KotlinClosure1(
@@ -24,3 +18,9 @@ class KotlinClosure1(
@Suppress("unused") // to be called dynamically by Groovy
fun doCall(it: T): V? = it.function()
}
+
+// These two are copy-pasted from `kotlin-dsl` plugin's groovy interop.
+// Because `kotlin-dsl` depends on kotlin 1.3.x.
+@Suppress("MISSING_KDOC_TOP_LEVEL", "MISSING_KDOC_ON_FUNCTION", "KDOC_WITHOUT_PARAM_TAG", "KDOC_WITHOUT_RETURN_TAG")
+fun Any.closureOf(action: T.() -> Unit): Closure =
+ KotlinClosure1(action, this, this)
diff --git a/diktat-rules/src/main/kotlin/generated/WarningNames.kt b/diktat-rules/src/main/kotlin/generated/WarningNames.kt
index e863a4ba90..c762fbf767 100644
--- a/diktat-rules/src/main/kotlin/generated/WarningNames.kt
+++ b/diktat-rules/src/main/kotlin/generated/WarningNames.kt
@@ -131,6 +131,8 @@ public object WarningNames {
public const val BLANK_LINE_BETWEEN_PROPERTIES: String = "BLANK_LINE_BETWEEN_PROPERTIES"
+ public const val TOP_LEVEL_ORDER: String = "TOP_LEVEL_ORDER"
+
public const val BRACES_BLOCK_STRUCTURE_ERROR: String = "BRACES_BLOCK_STRUCTURE_ERROR"
public const val WRONG_INDENTATION: String = "WRONG_INDENTATION"
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/constants/Chapters.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/constants/Chapters.kt
index 807bece99b..82886b3175 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/constants/Chapters.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/constants/Chapters.kt
@@ -46,12 +46,6 @@ fun Warnings.isRuleFromActiveChapter(configRules: List): Boolean {
return disabledChapters?.let { return chapterFromRule !in it } ?: true
}
-private fun validate(chapter: String) =
- require(chapter in Chapters.values().map { it.title }) {
- val closestMatch = Chapters.values().minByOrNull { Levenshtein.distance(it.title, chapter) }
- "Chapter name <$chapter> in configuration file is invalid, did you mean <$closestMatch>?"
- }
-
/**
* Function get chapter by warning
*
@@ -59,3 +53,9 @@ private fun validate(chapter: String) =
*/
@Suppress("UnsafeCallOnNullableType")
fun Warnings.getChapterByWarning() = Chapters.values().find { it.number == this.ruleId.first().toString() }!!
+
+private fun validate(chapter: String) =
+ require(chapter in Chapters.values().map { it.title }) {
+ val closestMatch = Chapters.values().minByOrNull { Levenshtein.distance(it.title, chapter) }
+ "Chapter name <$chapter> in configuration file is invalid, did you mean <$closestMatch>?"
+ }
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 37c444dd1f..92d00419a9 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
@@ -92,6 +92,7 @@ enum class Warnings(
NO_BRACES_IN_CONDITIONALS_AND_LOOPS(true, "3.2.1", "in if, else, when, for, do, and while statements braces should be used. Exception: single line if statement."),
WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES(true, "3.1.4", "the declaration part of a class-like code structures (class/interface/etc.) should be in the proper order"),
BLANK_LINE_BETWEEN_PROPERTIES(true, "3.1.4", "there should be no blank lines between properties without comments; comment or KDoc on property should have blank line before"),
+ TOP_LEVEL_ORDER(true, "3.1.5", "the declaration part of a top level elements should be in the proper order"),
BRACES_BLOCK_STRUCTURE_ERROR(true, "3.2.2", "braces should follow 1TBS style"),
WRONG_INDENTATION(true, "3.3.1", "only spaces are allowed for indentation and each indentation should equal to 4 spaces (tabs are not allowed)"),
EMPTY_BLOCK_STRUCTURE_ERROR(true, "3.4.1", "incorrect format of empty block"),
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 5c247ad108..86331ed1bc 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
@@ -35,6 +35,7 @@ import org.cqfn.diktat.ruleset.rules.chapter3.files.FileSize
import org.cqfn.diktat.ruleset.rules.chapter3.files.FileStructureRule
import org.cqfn.diktat.ruleset.rules.chapter3.files.IndentationRule
import org.cqfn.diktat.ruleset.rules.chapter3.files.NewlinesRule
+import org.cqfn.diktat.ruleset.rules.chapter3.files.TopLevelOrderRule
import org.cqfn.diktat.ruleset.rules.chapter3.files.WhiteSpaceRule
import org.cqfn.diktat.ruleset.rules.chapter3.identifiers.LocalVariablesRule
import org.cqfn.diktat.ruleset.rules.chapter4.ImmutableValNoVarRule
@@ -150,6 +151,7 @@ class DiktatRuleSetProvider(private var diktatConfigFile: String = DIKTAT_ANALYS
::EmptyBlock,
::AvoidEmptyPrimaryConstructor,
::EnumsSeparated,
+ ::TopLevelOrderRule,
::SingleLineStatementsRule,
::MultipleModifiersSequence,
::TrivialPropertyAccessors,
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/TopLevelOrderRule.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/TopLevelOrderRule.kt
new file mode 100644
index 0000000000..ac967e1e46
--- /dev/null
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/TopLevelOrderRule.kt
@@ -0,0 +1,113 @@
+package org.cqfn.diktat.ruleset.rules.chapter3.files
+
+import org.cqfn.diktat.common.config.rules.RulesConfig
+import org.cqfn.diktat.ruleset.constants.Warnings.TOP_LEVEL_ORDER
+import org.cqfn.diktat.ruleset.rules.DiktatRule
+import org.cqfn.diktat.ruleset.utils.*
+
+import com.pinterest.ktlint.core.ast.ElementType.CLASS
+import com.pinterest.ktlint.core.ast.ElementType.FILE
+import com.pinterest.ktlint.core.ast.ElementType.FUN
+import com.pinterest.ktlint.core.ast.ElementType.IMPORT_LIST
+import com.pinterest.ktlint.core.ast.ElementType.INTERNAL_KEYWORD
+import com.pinterest.ktlint.core.ast.ElementType.OBJECT_DECLARATION
+import com.pinterest.ktlint.core.ast.ElementType.OVERRIDE_KEYWORD
+import com.pinterest.ktlint.core.ast.ElementType.PRIVATE_KEYWORD
+import com.pinterest.ktlint.core.ast.ElementType.PROPERTY
+import com.pinterest.ktlint.core.ast.ElementType.PROTECTED_KEYWORD
+import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
+import com.pinterest.ktlint.core.ast.isPartOfComment
+import org.jetbrains.kotlin.com.intellij.lang.ASTNode
+import org.jetbrains.kotlin.psi.KtFunction
+import org.jetbrains.kotlin.psi.psiUtil.isExtensionDeclaration
+import org.jetbrains.kotlin.psi.psiUtil.siblings
+
+/**
+ * Rule that checks order in top level
+ */
+class TopLevelOrderRule(configRules: List) : DiktatRule("top-level-order", configRules, listOf(TOP_LEVEL_ORDER)) {
+ override fun logic(node: ASTNode) {
+ if (node.elementType == FILE) {
+ checkNode(node)
+ }
+ }
+
+ @Suppress("UnsafeCallOnNullableType")
+ private fun checkNode(node: ASTNode) {
+ val children = node.getChildren(null)
+ val initialElementsOrder = children.filter { it.elementType in sortedType }
+ if (initialElementsOrder.isEmpty()) {
+ return
+ }
+ val properties = Properties(children.filter { it.elementType == PROPERTY }).sortElements()
+ val functions = children.filter { it.elementType == FUN }
+ val classes = children.filter { it.elementType == CLASS || it.elementType == OBJECT_DECLARATION }
+ val sortOrder = Blocks(properties, functions, classes).sortElements().map { astNode ->
+ Pair(astNode, astNode.siblings(false).takeWhile { it.elementType == WHITE_SPACE || it.isPartOfComment() }.toList())
+ }
+ val lastNonSortedChildren = initialElementsOrder.last().siblings(true).toList()
+ sortOrder.filterIndexed { index, pair -> initialElementsOrder[index] != pair.first }
+ .forEach { listOfChildren ->
+ val wrongNode = listOfChildren.first
+ TOP_LEVEL_ORDER.warnAndFix(configRules, emitWarn, isFixMode, wrongNode.text, wrongNode.startOffset, wrongNode) {
+ node.removeRange(node.findChildByType(IMPORT_LIST)!!.treeNext, node.lastChildNode)
+ node.removeChild(node.lastChildNode)
+ sortOrder.map { (sortedNode, sortedNodePrevSibling) ->
+ sortedNodePrevSibling.reversed().map { node.addChild(it, null) }
+ node.addChild(sortedNode, null)
+ }
+ lastNonSortedChildren.map { node.addChild(it, null) }
+ }
+ }
+ }
+
+ /**
+ * Interface for classes to collect child and sort them
+ */
+ interface Elements {
+ /**
+ * Method to sort children
+ *
+ * @return sorted mutable list
+ */
+ fun sortElements(): MutableList
+ }
+
+ /**
+ * Class containing different groups of properties in file
+ */
+ private data class Properties(private val properties: List) : Elements {
+ override fun sortElements(): MutableList {
+ val constValProperties = properties.filter { it.isConstant() }
+ val valProperties = properties.filter { it.isValProperty() && !it.isConstant() }
+ val lateinitProperties = properties.filter { it.isLateInit() }
+ val varProperties = properties.filter { it.isVarProperty() && !it.isLateInit() }
+ return listOf(constValProperties, valProperties, lateinitProperties, varProperties).flatten().toMutableList()
+ }
+ }
+
+ /**
+ * Class containing different children in file
+ */
+ private data class Blocks(
+ private val properties: List,
+ private val functions: List,
+ private val classes: List) : Elements {
+ override fun sortElements(): MutableList {
+ val (extensionFun, nonExtensionFun) = functions.partition { (it.psi as KtFunction).isExtensionDeclaration() }
+ return (properties + listOf(classes, extensionFun, nonExtensionFun).map { nodes ->
+ val (privatePart, notPrivatePart) = nodes.partition { it.hasModifier(PRIVATE_KEYWORD) }
+ val (protectedPart, notProtectedPart) = notPrivatePart.partition { it.hasModifier(PROTECTED_KEYWORD) || it.hasModifier(OVERRIDE_KEYWORD) }
+ val (internalPart, publicPart) = notProtectedPart.partition { it.hasModifier(INTERNAL_KEYWORD) }
+ listOf(publicPart, internalPart, protectedPart, privatePart).flatten()
+ }.flatten()).toMutableList()
+ }
+ }
+
+ companion object {
+ /**
+ * List of children that should be sort
+ */
+ val sortedType = listOf(PROPERTY, FUN, CLASS, OBJECT_DECLARATION)
+ }
+}
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/AstConstants.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/AstConstants.kt
index dd52e901d3..0bb8a86e62 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/AstConstants.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/AstConstants.kt
@@ -13,14 +13,15 @@ import com.pinterest.ktlint.core.ast.ElementType.SEMICOLON
import com.pinterest.ktlint.core.ast.ElementType.WHILE
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
+internal const val GET_PREFIX = "get"
+internal const val SET_PREFIX = "set"
+internal const val EMPTY_BLOCK_TEXT = "{}"
+
/**
* List of standard methods which do not need mandatory documentation
*/
internal val standardMethods = listOf("main", "equals", "hashCode", "toString", "clone", "finalize")
-internal const val GET_PREFIX = "get"
-internal const val SET_PREFIX = "set"
-
/**
* List of element types present in empty code block `{ }`
*/
@@ -30,7 +31,12 @@ val commentType = listOf(BLOCK_COMMENT, EOL_COMMENT, KDOC)
val loopType = listOf(FOR, WHILE, DO_WHILE)
val copyrightWords = setOf("copyright", "版权")
-internal const val EMPTY_BLOCK_TEXT = "{}"
+internal val operatorMap = mapOf(
+ "unaryPlus" to "+", "unaryMinus" to "-", "not" to "!",
+ "plus" to "+", "minus" to "-", "times" to "*", "div" to "/", "rem" to "%", "mod" to "%", "rangeTo" to "..",
+ "inc" to "++", "dec" to "--", "contains" to "in",
+ "plusAssign" to "+=", "minusAssign" to "-=", "timesAssign" to "*=", "divAssign" to "/=", "modAssign" to "%="
+)
/**
* Enum that represents some standard platforms that can appear in kotlin code
@@ -42,10 +48,3 @@ enum class StandardPlatforms(val packages: List) {
KOTLIN(listOf("kotlin", "kotlinx")),
;
}
-
-internal val operatorMap = mapOf(
- "unaryPlus" to "+", "unaryMinus" to "-", "not" to "!",
- "plus" to "+", "minus" to "-", "times" to "*", "div" to "/", "rem" to "%", "mod" to "%", "rangeTo" to "..",
- "inc" to "++", "dec" to "--", "contains" to "in",
- "plusAssign" to "+=", "minusAssign" to "-=", "timesAssign" to "*=", "divAssign" to "/=", "modAssign" to "%="
-)
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 c79d344683..8352db3b0f 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
@@ -20,6 +20,7 @@ import com.pinterest.ktlint.core.ast.ElementType.FILE_ANNOTATION_LIST
import com.pinterest.ktlint.core.ast.ElementType.IMPORT_LIST
import com.pinterest.ktlint.core.ast.ElementType.INTERNAL_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.KDOC
+import com.pinterest.ktlint.core.ast.ElementType.LATEINIT_KEYWORD
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
@@ -56,6 +57,18 @@ import org.slf4j.LoggerFactory
*/
val log: Logger = LoggerFactory.getLogger(ASTNode::class.java)
+/**
+ * A class that represents result of nodes swapping. [oldNodes] should always have same size as [newNodes]
+ *
+ * @property oldNodes nodes that were to be moved
+ * @property newNodes nodes that have been moved
+ */
+data class ReplacementResult(val oldNodes: List, val newNodes: List) {
+ init {
+ require(oldNodes.size == newNodes.size)
+ }
+}
+
/**
* @return the highest parent node of the tree
*/
@@ -330,6 +343,16 @@ fun ASTNode.isValProperty() =
*/
fun ASTNode.isConst() = this.findLeafWithSpecificType(CONST_KEYWORD) != null
+/**
+ * Checks whether this node of type PROPERTY has `lateinit` modifier
+ */
+fun ASTNode.isLateInit() = this.findLeafWithSpecificType(LATEINIT_KEYWORD) != null
+
+/**
+ * @param modifier modifier to find in node
+ */
+fun ASTNode.hasModifier(modifier: IElementType) = this.findChildByType(MODIFIER_LIST)?.hasChildOfType(modifier) ?: false
+
/**
* Checks whether [this] node of type PROPERTY is `var`
*/
@@ -693,20 +716,6 @@ fun ASTNode.hasTestAnnotation() = findChildByType(MODIFIER_LIST)
?.any { it.findLeafWithSpecificType(ElementType.IDENTIFIER)?.text == "Test" }
?: false
-/**
- * Checks node is located in file src/test/**/*Test.kt
- *
- * @param testAnchors names of test directories, e.g. "test", "jvmTest"
- */
-fun isLocatedInTest(filePathParts: List, testAnchors: List) = filePathParts
- .takeIf { it.contains(PackageNaming.PACKAGE_PATH_ANCHOR) }
- ?.run { subList(lastIndexOf(PackageNaming.PACKAGE_PATH_ANCHOR), size) }
- ?.run {
- // e.g. src/test/ClassTest.kt, other files like src/test/Utils.kt are still checked
- testAnchors.any { contains(it) } && last().substringBeforeLast('.').endsWith("Test")
- }
- ?: false
-
/**
* Returns the first line of this node's text if it is single, or the first line followed by [suffix] if there are more than one.
*
@@ -734,18 +743,6 @@ fun ASTNode.getFilePath(): String = getUserData(KtLint.FILE_PATH_USER_DATA_KEY).
it
}
-/**
- * A class that represents result of nodes swapping. [oldNodes] should always have same size as [newNodes]
- *
- * @property oldNodes nodes that were to be moved
- * @property newNodes nodes that have been moved
- */
-data class ReplacementResult(val oldNodes: List, val newNodes: List) {
- init {
- require(oldNodes.size == newNodes.size)
- }
-}
-
/**
* checks that this one node is placed after the other node in code (by comparing lines of code where nodes start)
*/
@@ -790,6 +787,20 @@ private fun ASTNode.calculateLineNumber() = getRootNode()
it + 1
}
+/**
+ * Checks node is located in file src/test/**/*Test.kt
+ *
+ * @param testAnchors names of test directories, e.g. "test", "jvmTest"
+ */
+fun isLocatedInTest(filePathParts: List, testAnchors: List) = filePathParts
+ .takeIf { it.contains(PackageNaming.PACKAGE_PATH_ANCHOR) }
+ ?.run { subList(lastIndexOf(PackageNaming.PACKAGE_PATH_ANCHOR), size) }
+ ?.run {
+ // e.g. src/test/ClassTest.kt, other files like src/test/Utils.kt are still checked
+ testAnchors.any { contains(it) } && last().substringBeforeLast('.').endsWith("Test")
+ }
+ ?: false
+
/**
* Count number of lines in code block. Note: only *copy* of a node should be passed to this method, because the method changes the node.
*
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/PositionInTextLocator.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/PositionInTextLocator.kt
index 1b55c0f449..cb7906d23d 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/PositionInTextLocator.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/PositionInTextLocator.kt
@@ -7,37 +7,6 @@ package org.cqfn.diktat.ruleset.utils
internal typealias LineAndColumn = Pair
-/**
- * Calculate position in text - line and column based on offset from the text start.
- *
- * @param text a piece of text
- * @return mapping function from offset to line and column number
- */
-internal fun buildPositionInTextLocator(text: String): (offset: Int) -> LineAndColumn {
- val textLength = text.length
- val identifierArray: ArrayList = ArrayList()
- var endOfLineIndex = -1
-
- do {
- identifierArray.add(endOfLineIndex + 1)
- endOfLineIndex = text.indexOf('\n', endOfLineIndex + 1)
- } while (endOfLineIndex != -1)
-
- identifierArray.add(textLength + if (identifierArray.last() == textLength) 1 else 0)
-
- val segmentTree = SegmentTree(identifierArray.toIntArray())
-
- return { offset ->
- val line = segmentTree.indexOf(offset)
- if (line != -1) {
- val column = offset - segmentTree.get(line).left
- line + 1 to column + 1
- } else {
- 1 to 1
- }
- }
-}
-
@Suppress("MISSING_KDOC_ON_FUNCTION", "KDOC_WITHOUT_PARAM_TAG", "KDOC_WITHOUT_RETURN_TAG")
private class SegmentTree(sortedArray: IntArray) {
private val segments: List = sortedArray
@@ -80,3 +49,34 @@ private data class Segment(
val left: Int,
val right: Int
)
+
+/**
+ * Calculate position in text - line and column based on offset from the text start.
+ *
+ * @param text a piece of text
+ * @return mapping function from offset to line and column number
+ */
+internal fun buildPositionInTextLocator(text: String): (offset: Int) -> LineAndColumn {
+ val textLength = text.length
+ val identifierArray: ArrayList = ArrayList()
+ var endOfLineIndex = -1
+
+ do {
+ identifierArray.add(endOfLineIndex + 1)
+ endOfLineIndex = text.indexOf('\n', endOfLineIndex + 1)
+ } while (endOfLineIndex != -1)
+
+ identifierArray.add(textLength + if (identifierArray.last() == textLength) 1 else 0)
+
+ val segmentTree = SegmentTree(identifierArray.toIntArray())
+
+ return { offset ->
+ val line = segmentTree.indexOf(offset)
+ if (line != -1) {
+ val column = offset - segmentTree.get(line).left
+ line + 1 to column + 1
+ } else {
+ 1 to 1
+ }
+ }
+}
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/StringCaseUtils.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/StringCaseUtils.kt
index 775da020d2..549ee9dc0f 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/StringCaseUtils.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/StringCaseUtils.kt
@@ -1,9 +1,18 @@
-@file:Suppress("FILE_NAME_MATCH_CLASS")
+@file:Suppress("FILE_NAME_MATCH_CLASS", "MatchingDeclarationName")
package org.cqfn.diktat.ruleset.utils
import com.google.common.base.CaseFormat
+/**
+ * Available cases to name enum members
+ */
+enum class Style {
+ PASCAL_CASE,
+ SNAKE_CASE,
+ ;
+}
+
/**
* checking that string looks like: PascalCaseForClassName
*
@@ -141,6 +150,12 @@ fun String.toPascalCase(): String = when {
}
}
+/**
+ * @return index of first character which is a letter or a digit
+ */
+private fun String.getFirstLetterOrDigit() =
+ indexOfFirst { it.isLetterOrDigit() }
+
private fun convertUnknownCaseToCamel(str: String, isFirstLetterCapital: Boolean): String {
// [p]a[SC]a[_]l -> [P]a[Sc]a[L]
var isPreviousLetterCapital = isFirstLetterCapital
@@ -186,18 +201,3 @@ private fun convertUnknownCaseToUpperSnake(str: String): String {
}
}.joinToString("")
}
-
-/**
- * @return index of first character which is a letter or a digit
- */
-private fun String.getFirstLetterOrDigit() =
- indexOfFirst { it.isLetterOrDigit() }
-
-/**
- * Available cases to name enum members
- */
-enum class Style {
- PASCAL_CASE,
- SNAKE_CASE,
- ;
-}
diff --git a/diktat-rules/src/main/resources/diktat-analysis-huawei.yml b/diktat-rules/src/main/resources/diktat-analysis-huawei.yml
index 6328ec6a72..02aadccb5a 100644
--- a/diktat-rules/src/main/resources/diktat-analysis-huawei.yml
+++ b/diktat-rules/src/main/resources/diktat-analysis-huawei.yml
@@ -7,7 +7,7 @@
# testDirs: test
disabledChapters: ""
testDirs: test
- kotlinVersion: "1.4.21"
+ kotlinVersion: 1.4.30
# Checks that the Class/Enum/Interface name does not match Pascal case
- name: CLASS_NAME_INCORRECT
enabled: true
@@ -183,6 +183,9 @@
# Checks that properties with comments are separated by a blank line
- name: BLANK_LINE_BETWEEN_PROPERTIES
enabled: true
+# Checks top level order
+- name: TOP_LEVEL_ORDER
+ enabled: true
# Checks that non-empty code blocks with braces follow the K&R style (1TBS or OTBS style)
- name: BRACES_BLOCK_STRUCTURE_ERROR
enabled: true
diff --git a/diktat-rules/src/main/resources/diktat-analysis.yml b/diktat-rules/src/main/resources/diktat-analysis.yml
index bf8a221864..774694e8ab 100644
--- a/diktat-rules/src/main/resources/diktat-analysis.yml
+++ b/diktat-rules/src/main/resources/diktat-analysis.yml
@@ -5,7 +5,7 @@
domainName: your.name.here
testDirs: test
disabledChapters: ""
- kotlinVersion: "1.4.21"
+ kotlinVersion: 1.4
# Checks that the Class/Enum/Interface name does not match Pascal case
- name: CLASS_NAME_INCORRECT
enabled: true
@@ -181,6 +181,9 @@
# Checks that properties with comments are separated by a blank line
- name: BLANK_LINE_BETWEEN_PROPERTIES
enabled: true
+# Checks top level order
+- name: TOP_LEVEL_ORDER
+ enabled: true
# Checks that non-empty code blocks with braces follow the K&R style (1TBS or OTBS style)
- name: BRACES_BLOCK_STRUCTURE_ERROR
enabled: true
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/files/TopLevelOrderRuleFixTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/files/TopLevelOrderRuleFixTest.kt
new file mode 100644
index 0000000000..4b6d875b60
--- /dev/null
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/files/TopLevelOrderRuleFixTest.kt
@@ -0,0 +1,22 @@
+package org.cqfn.diktat.ruleset.chapter3.files
+
+import org.cqfn.diktat.ruleset.rules.chapter3.files.TopLevelOrderRule
+import org.cqfn.diktat.util.FixTestBase
+
+import generated.WarningNames
+import org.junit.jupiter.api.Tag
+import org.junit.jupiter.api.Test
+
+class TopLevelOrderRuleFixTest : FixTestBase("test/paragraph3/top_level", ::TopLevelOrderRule) {
+ @Test
+ @Tag(WarningNames.TOP_LEVEL_ORDER)
+ fun `should fix top level order`() {
+ fixAndCompare("TopLevelSortExpected.kt", "TopLevelSortTest.kt")
+ }
+
+ @Test
+ @Tag(WarningNames.TOP_LEVEL_ORDER)
+ fun `should fix top level order with comment`() {
+ fixAndCompare("TopLevelWithCommentExpected.kt", "TopLevelWithCommentTest.kt")
+ }
+}
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/files/TopLevelOrderRuleWarnTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/files/TopLevelOrderRuleWarnTest.kt
new file mode 100644
index 0000000000..11dee42588
--- /dev/null
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/files/TopLevelOrderRuleWarnTest.kt
@@ -0,0 +1,55 @@
+package org.cqfn.diktat.ruleset.chapter3.files
+
+import org.cqfn.diktat.ruleset.constants.Warnings.TOP_LEVEL_ORDER
+import org.cqfn.diktat.ruleset.rules.DIKTAT_RULE_SET_ID
+import org.cqfn.diktat.ruleset.rules.chapter3.files.TopLevelOrderRule
+import org.cqfn.diktat.util.LintTestBase
+
+import com.pinterest.ktlint.core.LintError
+import generated.WarningNames
+import org.junit.jupiter.api.Tag
+import org.junit.jupiter.api.Test
+
+class TopLevelOrderRuleWarnTest : LintTestBase(::TopLevelOrderRule) {
+ private val ruleId = "$DIKTAT_RULE_SET_ID:top-level-order"
+
+ @Test
+ @Tag(WarningNames.TOP_LEVEL_ORDER)
+ fun `correct order`() {
+ lintMethod(
+ """
+ |const val CONSTANT = 42
+ |val topLevelProperty = "String constant"
+ |lateinit var q: String
+ |fun String.foo() {}
+ |fun foo() {}
+ |private fun gio() {}
+ """.trimMargin()
+ )
+ }
+
+ @Test
+ @Tag(WarningNames.TOP_LEVEL_ORDER)
+ fun `wrong order`() {
+ lintMethod(
+ """
+ |class A {}
+ |lateinit var q: String
+ |interface B {}
+ |fun foo() {}
+ |fun String.foo() {}
+ |private val et = 0
+ |public const val g = 9.8
+ |object B {}
+ """.trimMargin(),
+ LintError(1, 1, ruleId, "${TOP_LEVEL_ORDER.warnText()} class A {}", true),
+ LintError(2, 1, ruleId, "${TOP_LEVEL_ORDER.warnText()} lateinit var q: String", true),
+ LintError(3, 1, ruleId, "${TOP_LEVEL_ORDER.warnText()} interface B {}", true),
+ LintError(4, 1, ruleId, "${TOP_LEVEL_ORDER.warnText()} fun foo() {}", true),
+ LintError(5, 1, ruleId, "${TOP_LEVEL_ORDER.warnText()} fun String.foo() {}", true),
+ LintError(6, 1, ruleId, "${TOP_LEVEL_ORDER.warnText()} private val et = 0", true),
+ LintError(7, 1, ruleId, "${TOP_LEVEL_ORDER.warnText()} public const val g = 9.8", true),
+ LintError(8, 1, ruleId, "${TOP_LEVEL_ORDER.warnText()} object B {}", true)
+ )
+ }
+}
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/TestUtils.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/TestUtils.kt
index 053e56d20d..cc0dfb7675 100644
--- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/TestUtils.kt
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/TestUtils.kt
@@ -24,6 +24,11 @@ internal const val TEST_FILE_NAME = "TestFileName.kt"
typealias LintErrorCallback = (LintError, Boolean) -> Unit
+@Suppress("TYPE_ALIAS")
+internal val defaultCallback: (lintError: LintError, corrected: Boolean) -> Unit = { lintError, _ ->
+ log.warn("Received linting error: $lintError")
+}
+
/**
* Compare [LintError]s from [this] with [expectedLintErrors]
*
@@ -87,11 +92,6 @@ internal fun format(ruleSetProviderRef: (rulesConfigList: List?) ->
)
)
-@Suppress("TYPE_ALIAS")
-internal val defaultCallback: (lintError: LintError, corrected: Boolean) -> Unit = { lintError, _ ->
- log.warn("Received linting error: $lintError")
-}
-
/**
* This utility function lets you run arbitrary code on every node of given [code].
* It also provides you with counter which can be incremented inside [applyToNode] and then will be compared to [expectedAsserts].
diff --git a/diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelSortExpected.kt b/diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelSortExpected.kt
new file mode 100644
index 0000000000..3a6cfe5722
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelSortExpected.kt
@@ -0,0 +1,15 @@
+package test.paragraph3.top_level
+
+const val q = "1"
+
+val heh = 10
+
+private var t = 1
+
+class Qwe() {}
+
+fun String.qq() {}
+
+internal fun kl() {}
+
+private fun foo() {}
diff --git a/diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelSortTest.kt b/diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelSortTest.kt
new file mode 100644
index 0000000000..3bec663066
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelSortTest.kt
@@ -0,0 +1,15 @@
+package test.paragraph3.top_level
+
+const val q = "1"
+
+private fun foo() {}
+
+class Qwe() {}
+
+fun String.qq() {}
+
+private var t = 1
+
+internal fun kl() {}
+
+val heh = 10
diff --git a/diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelWithCommentExpected.kt b/diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelWithCommentExpected.kt
new file mode 100644
index 0000000000..58968da89d
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelWithCommentExpected.kt
@@ -0,0 +1,24 @@
+/**
+ * Some text here
+ */
+
+package test.paragraph3.top_level
+
+import org.cqfn.diktat.bar
+
+class A {}
+
+/**
+ * Hehe
+ */
+fun String.ww() {}
+
+/**
+ * Text for function
+ */
+fun foo() {}
+
+
+/*
+text here
+ */
\ No newline at end of file
diff --git a/diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelWithCommentTest.kt b/diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelWithCommentTest.kt
new file mode 100644
index 0000000000..72267c55ee
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelWithCommentTest.kt
@@ -0,0 +1,24 @@
+/**
+ * Some text here
+ */
+
+package test.paragraph3.top_level
+
+import org.cqfn.diktat.bar
+
+class A {}
+
+/**
+ * Text for function
+ */
+fun foo() {}
+
+/**
+ * Hehe
+ */
+fun String.ww() {}
+
+
+/*
+text here
+ */
diff --git a/diktat-rules/src/test/resources/test/smoke/src/main/kotlin/Bug1Expected.kt b/diktat-rules/src/test/resources/test/smoke/src/main/kotlin/Bug1Expected.kt
index 21214634b2..b53073c8f8 100644
--- a/diktat-rules/src/test/resources/test/smoke/src/main/kotlin/Bug1Expected.kt
+++ b/diktat-rules/src/test/resources/test/smoke/src/main/kotlin/Bug1Expected.kt
@@ -1,12 +1,5 @@
package org.cqfn.diktat
-/**
- * @param foo
- */
-fun readFile(foo: Foo) {
- var bar: Bar
-}
-
class D {
val x = 0
@@ -19,3 +12,10 @@ class D {
}
}
+/**
+ * @param foo
+ */
+fun readFile(foo: Foo) {
+ var bar: Bar
+}
+
diff --git a/diktat-rules/src/test/resources/test/smoke/src/main/kotlin/Example3Expected.kt b/diktat-rules/src/test/resources/test/smoke/src/main/kotlin/Example3Expected.kt
index 515da9b064..943de52b8a 100644
--- a/diktat-rules/src/test/resources/test/smoke/src/main/kotlin/Example3Expected.kt
+++ b/diktat-rules/src/test/resources/test/smoke/src/main/kotlin/Example3Expected.kt
@@ -15,16 +15,6 @@ class HttpClient(var name: String) {
fun doRequest() {}
}
-fun mains() {
- val httpClient = HttpClient("myConnection")
- .apply {
- url = "http://example.com"
- port = "8080"
- timeout = 100
- }
- httpClient.doRequest()
-}
-
class Example {
fun foo() {
if (condition1) {
@@ -52,3 +42,13 @@ class Foo {
private fun foo() {}
}
+fun mains() {
+ val httpClient = HttpClient("myConnection")
+ .apply {
+ url = "http://example.com"
+ port = "8080"
+ timeout = 100
+ }
+ httpClient.doRequest()
+}
+
diff --git a/info/available-rules.md b/info/available-rules.md
index eab6a6c04b..0d633d17e2 100644
--- a/info/available-rules.md
+++ b/info/available-rules.md
@@ -61,6 +61,7 @@
| 3 | 3.2.1 | NO_BRACES_IN_CONDITIONALS_AND_LOOPS | Check: warns if braces are not used in if, else, when, for, do, and while statements. Exception: single line if statement.
Fix: adds missing braces. | yes | no | - |
| 3 | 3.1.4 | WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES | Check: warns if the declaration part of a class-like code structures (class/interface/etc.) is not in the proper order.
Fix: restores order according to code style guide. | yes | no | - |
| 3 | 3.1.4 | BLANK_LINE_BETWEEN_PROPERTIES | Check: warns if properties with comments are not separated by a blank line, properties without comments are
Fix: fixes number of blank lines | yes | no | - |
+| 3 | 3.1.5 | TOP_LEVEL_ORDER | Check: warns if top level order is incorrect | yes | no | - |
| 3 | 3.2.2 | BRACES_BLOCK_STRUCTURE_ERROR | Check: warns if non-empty code blocks with braces don't follow the K&R style (1TBS or OTBS style) | yes | openBraceNewline closeBraceNewline | - |
| 3 | 3.4.1 | EMPTY_BLOCK_STRUCTURE_ERROR | Check: warns if empty block exist or if it's style is incorrect | yes | allowEmptyBlocks styleEmptyBlockWithNewline | - |
| 3 | 3.6.1 | MORE_THAN_ONE_STATEMENT_PER_LINE | Check: warns if there is more than one statement per line | yes | no | - |
diff --git a/pom.xml b/pom.xml
index 1272e4eebe..e053dfbbd4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,11 +42,11 @@
1.8
1.8
UTF-8
- 1.4.21
+ 1.4.30
true
1.0.1
0.39.0
- 5.7.0
+ 5.7.1
30.0-jre
1.7.30
1.4