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

Rule 6.4.1: Avoid using utility classes/objects #508

Merged
merged 18 commits into from
Dec 6, 2020
Merged
3 changes: 3 additions & 0 deletions diktat-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -305,4 +305,7 @@
# there should be explicit backing property with the name of real property
# Example: val table get() {if (_table == null) ...} -> table should have _table
- name: NO_CORRESPONDING_PROPERTY
enabled: true
# Checks if there is class/object that can be replace with extension function
- name: AVOID_USING_UTILITY_CLASS
enabled: true
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 @@ -223,4 +223,6 @@ public object WarningNames {
public const val EMPTY_PRIMARY_CONSTRUCTOR: String = "EMPTY_PRIMARY_CONSTRUCTOR"

public const val NO_CORRESPONDING_PROPERTY: String = "NO_CORRESPONDING_PROPERTY"

public const val AVOID_USING_UTILITY_CLASS: String = "AVOID_USING_UTILITY_CLASS"
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ enum class Warnings(val canBeAutoCorrected: Boolean, val ruleId: String, private
TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED(true, "6.1.10", "trivial property accessors are not recommended"),
EXTENSION_FUNCTION_SAME_SIGNATURE(false, "6.2.2", "extension functions should not have same signature if their receiver classes are related"),
EMPTY_PRIMARY_CONSTRUCTOR(true,"6.1.3", "avoid empty primary constructor"),
NO_CORRESPONDING_PROPERTY(false, "6.1.7", "backing property should have the same name, but there is no corresponding property")
NO_CORRESPONDING_PROPERTY(false, "6.1.7", "backing property should have the same name, but there is no corresponding property"),
AVOID_USING_UTILITY_CLASS(false, "6.4.1", "avoid using utility classes/objects, use extensions functions"),
;

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.cqfn.diktat.ruleset.rules

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.BLOCK_COMMENT
import com.pinterest.ktlint.core.ast.ElementType.CLASS
import com.pinterest.ktlint.core.ast.ElementType.CLASS_BODY
import com.pinterest.ktlint.core.ast.ElementType.EOL_COMMENT
import com.pinterest.ktlint.core.ast.ElementType.FUN
import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER
import com.pinterest.ktlint.core.ast.ElementType.KDOC
import com.pinterest.ktlint.core.ast.ElementType.LBRACE
import com.pinterest.ktlint.core.ast.ElementType.OBJECT_DECLARATION
import com.pinterest.ktlint.core.ast.ElementType.PRIMARY_CONSTRUCTOR
import com.pinterest.ktlint.core.ast.ElementType.RBRACE
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
import org.cqfn.diktat.common.config.rules.RulesConfig
import org.cqfn.diktat.ruleset.constants.EmitType
import org.cqfn.diktat.ruleset.constants.Warnings.AVOID_USING_UTILITY_CLASS
import org.cqfn.diktat.ruleset.utils.hasChildOfType
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.psi.psiUtil.children

/**
* Rule 6.4.1 checks that class/object, with a word "util" in its name, has only functions.
*/
class AvoidUtilityClass(private val configRules: List<RulesConfig>) : Rule("avoid-utility-class") {

companion object {
private val UTILITY_CLASS_CHILDREN = listOf(LBRACE, WHITE_SPACE, FUN, RBRACE, KDOC,
EOL_COMMENT, BLOCK_COMMENT, OBJECT_DECLARATION)
}

private var isFixMode: Boolean = false
private lateinit var emitWarn: EmitType

override fun visit(node: ASTNode, autoCorrect: Boolean, emit: EmitType) {
emitWarn = emit
isFixMode = autoCorrect
if (node.elementType == OBJECT_DECLARATION || node.elementType == CLASS ) {
checkClass(node)
}
}

@Suppress("UnsafeCallOnNullableType")
private fun checkClass(node: ASTNode) {
//checks that class/object doesn't contain primary constructor and its identifier doesn't has "utli"
if (!node.hasChildOfType(IDENTIFIER) || node.hasChildOfType(PRIMARY_CONSTRUCTOR)
|| !node.findChildByType(IDENTIFIER)!!.text.toLowerCase().contains("util"))
return
node.findChildByType(CLASS_BODY)
?.children()
?.toList()
?.takeIf { childList -> childList.all { it.elementType in UTILITY_CLASS_CHILDREN } }
?.filter { it.elementType == FUN }
?.ifEmpty { return }
?: return
AVOID_USING_UTILITY_CLASS.warn(configRules, emitWarn, isFixMode, node.findChildByType(IDENTIFIER)?.text ?: node.text, node.startOffset, node)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class DiktatRuleSetProvider(private val diktatConfigFile: String = "diktat-analy
::DataClassesRule,
::LocalVariablesRule,
::SmartCastRule,
::AvoidUtilityClass,
::PropertyAccessorFields,
::AbstractClassesRule,
::SingleInitRule,
Expand Down
3 changes: 3 additions & 0 deletions diktat-rules/src/main/resources/diktat-analysis-huawei.yml
Original file line number Diff line number Diff line change
Expand Up @@ -303,4 +303,7 @@
# there should be explicit backing property with the name of real property
# Example: val table get() {if (_table == null) ...} -> table should have _table
- name: NO_CORRESPONDING_PROPERTY
enabled: true
# Checks if there is class/object that can be replace with extension function
- name: AVOID_USING_UTILITY_CLASS
enabled: true
3 changes: 3 additions & 0 deletions diktat-rules/src/main/resources/diktat-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -305,4 +305,7 @@
# there should be explicit backing property with the name of real property
# Example: val table get() {if (_table == null) ...} -> table should have _table
- name: NO_CORRESPONDING_PROPERTY
enabled: true
# Checks if there is class/object that can be replace with extension function
- name: AVOID_USING_UTILITY_CLASS
enabled: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package org.cqfn.diktat.ruleset.chapter6

import com.pinterest.ktlint.core.LintError
import generated.WarningNames
import org.cqfn.diktat.ruleset.constants.Warnings.AVOID_USING_UTILITY_CLASS
import org.cqfn.diktat.ruleset.rules.AvoidUtilityClass
import org.cqfn.diktat.ruleset.rules.DIKTAT_RULE_SET_ID
import org.cqfn.diktat.util.LintTestBase
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test

class AvoidUtilityClassWarnTest: LintTestBase(::AvoidUtilityClass) {
private val ruleId = "$DIKTAT_RULE_SET_ID:avoid-utility-class"

@Test
@Tag(WarningNames.AVOID_USING_UTILITY_CLASS)
fun `simple test`() {
lintMethod(
"""
|object StringUtil {
| fun stringInfo(myString: String): Int {
| return myString.count{ "something".contains(it) }
| }
|}
|
|class A() {
| fun foo() { }
|}
|
|class StringUtils {
| fun goo(tex: String): Int {
| return myString.count{ "something".contains(it) }
| }
|}
|
|class StringUtil {
| val z = "hello"
| fun goo(tex: String): Int {
| return myString.count{ "something".contains(it) }
| }
|}
""".trimMargin(),
LintError(1,1, ruleId, "${AVOID_USING_UTILITY_CLASS.warnText()} StringUtil"),
LintError(11,1, ruleId, "${AVOID_USING_UTILITY_CLASS.warnText()} StringUtils")
)
}

@Test
@Tag(WarningNames.AVOID_USING_UTILITY_CLASS)
fun `test with comment anf companion`() {
lintMethod(
"""
|
|class StringUtils {
| companion object {
| private val name = "Hello"
| }
| /**
| * @param tex
| */
| fun goo(tex: String): Int {
| //hehe
| return myString.count{ "something".contains(it) }
| }
|}
|
|class StringUtil {
| /*
|
| */
| companion object {
| }
| val z = "hello"
| fun goo(tex: String): Int {
| return myString.count{ "something".contains(it) }
| }
|}
""".trimMargin(),
LintError(2,1, ruleId, "${AVOID_USING_UTILITY_CLASS.warnText()} StringUtils")
)
}

@Test
@Tag(WarningNames.AVOID_USING_UTILITY_CLASS)
fun `test with class without identifier`() {
lintMethod(
"""
fun foo() {
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { /*...*/ }

override fun mouseEntered(e: MouseEvent) { /*...*/ }
})
}
""".trimMargin()
)
}
}
3 changes: 2 additions & 1 deletion info/available-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,5 @@
| 6 | 6.1.10 | TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED | Check: if there are any trivial getters or setters <br> Fix: Delete trivial getter or setter | yes | - | - |
| 6 | 6.1.8 | CUSTOM_GETTERS_SETTERS | Check: Inspection that checks that no custom getters and setters are used for properties | no | - | - |
| 6 | 6.1.11 | COMPACT_OBJECT_INITIALIZATION | Checks if class instantiation can be wrapped in `apply` for better readability | | | |
| 6 | 6.2.2 | EXTENSION_FUNCTION_SAME_SIGNATURE | Checks if extension function has the same signature as another extension function and their classes are related | no | - | + |
| 6 | 6.2.2 | EXTENSION_FUNCTION_SAME_SIGNATURE | Checks if extension function has the same signature as another extension function and their classes are related | no | - | + |
| 6 | 6.4.1 | AVOID_USING_UTILITY_CLASS | Checks if there is class/object that can be replace with extension function | no | - | - |