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