Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Top level order #739

Merged
merged 16 commits into from
Feb 5, 2021
3 changes: 3 additions & 0 deletions diktat-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,6 @@ open class RulesConfigReader(override val classLoader: ClassLoader) : JsonResour
}
}

/**
* @return common configuration from list of all rules configuration
*/
fun List<RulesConfig>.getCommonConfiguration() = lazy {
CommonConfiguration(getCommonConfig()?.configuration)
}

/**
* class returns the list of common configurations that we have read from a configuration map
*
Expand Down Expand Up @@ -142,6 +135,13 @@ data class CommonConfiguration(private val configuration: Map<String, String>?)

// ================== utils for List<RulesConfig> from yml config

/**
* @return common configuration from list of all rules configuration
*/
fun List<RulesConfig>.getCommonConfiguration() = lazy {
CommonConfiguration(getCommonConfig()?.configuration)
}

/**
* Get [RulesConfig] for particular [Rule] object.
*
Expand All @@ -150,11 +150,6 @@ data class CommonConfiguration(private val configuration: Map<String, String>?)
*/
fun List<RulesConfig>.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<RulesConfig>.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)
Expand Down Expand Up @@ -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<RulesConfig>.getCommonConfig() = find { it.name == DIKTAT_COMMON }
Original file line number Diff line number Diff line change
Expand Up @@ -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 <T> Any.closureOf(action: T.() -> Unit): Closure<Any?> =
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<in T : Any?, V : Any>(
Expand All @@ -24,3 +18,9 @@ class KotlinClosure1<in T : Any?, V : Any>(
@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 <T> Any.closureOf(action: T.() -> Unit): Closure<Any?> =
KotlinClosure1(action, this, this)
2 changes: 2 additions & 0 deletions diktat-rules/src/main/kotlin/generated/WarningNames.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,16 @@ fun Warnings.isRuleFromActiveChapter(configRules: List<RulesConfig>): 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
*
* @return chapter to which warning refers
*/
@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>?"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -150,6 +151,7 @@ class DiktatRuleSetProvider(private var diktatConfigFile: String = DIKTAT_ANALYS
::EmptyBlock,
::AvoidEmptyPrimaryConstructor,
::EnumsSeparated,
::TopLevelOrderRule,
::SingleLineStatementsRule,
::MultipleModifiersSequence,
::TrivialPropertyAccessors,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<RulesConfig>) : 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)
kentr0w marked this conversation as resolved.
Show resolved Hide resolved
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<ASTNode>
}

/**
* Class containing different groups of properties in file
*/
private data class Properties(private val properties: List<ASTNode>) : Elements {
override fun sortElements(): MutableList<ASTNode> {
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<ASTNode>,
private val functions: List<ASTNode>,
private val classes: List<ASTNode>) : Elements {
override fun sortElements(): MutableList<ASTNode> {
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 `{ }`
*/
Expand All @@ -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
Expand All @@ -42,10 +48,3 @@ enum class StandardPlatforms(val packages: List<String>) {
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 "%="
)
Loading