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.2.2(#447) #501

Merged
merged 12 commits into from
Nov 16, 2020
4 changes: 4 additions & 0 deletions diktat-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -352,5 +352,9 @@
configuration: {}
# Checks if there are any trivial getters or setters
- name: TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED
enabled: true
configuration: {}
# Checks if extension function with the same signature don't have related classes
- name: EXTENSION_FUNCTION_SAME_SIGNATURE
enabled: true
configuration: {}
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 @@ -209,4 +209,6 @@ public object WarningNames {

public const val TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED: String =
"TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED"

public const val EXTENSION_FUNCTION_SAME_SIGNATURE: String = "EXTENSION_FUNCTION_SAME_SIGNATURE"
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ enum class Warnings(private val canBeAutoCorrected: Boolean, private val warn: S
WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR(false, "Use `field` keyword instead of property name inside property accessors"),
MULTIPLE_INIT_BLOCKS(true, "Avoid using multiple `init` blocks, this logic can be moved to constructors or properties declarations"),
CLASS_SHOULD_NOT_BE_ABSTRACT(true, "class should not be abstract, because it has no abstract functions"),
TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED(true, "trivial property accessors are not recommended")
TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED(true, "trivial property accessors are not recommended"),
EXTENSION_FUNCTION_SAME_SIGNATURE(false, "extension functions should not have same signature if their classes are related"),
aktsay6 marked this conversation as resolved.
Show resolved Hide resolved
;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class DiktatRuleSetProvider(private val diktatConfigFile: String = "diktat-analy
::NullableTypeRule,
::ImmutableValNoVarRule,
::AvoidNestedFunctionsRule,
::ExtensionFunctionsSameNameRule,
// formatting: moving blocks, adding line breaks, indentations etc.
::ConsecutiveSpacesRule,
::WhiteSpaceRule, // this rule should be after other rules that can cause wrong spacing
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package org.cqfn.diktat.ruleset.rules

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.CLASS
import com.pinterest.ktlint.core.ast.ElementType.COLON
import com.pinterest.ktlint.core.ast.ElementType.DOT
import com.pinterest.ktlint.core.ast.ElementType.FILE
import com.pinterest.ktlint.core.ast.ElementType.FUN
import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER
import com.pinterest.ktlint.core.ast.ElementType.SUPER_TYPE_CALL_ENTRY
import com.pinterest.ktlint.core.ast.ElementType.SUPER_TYPE_LIST
import com.pinterest.ktlint.core.ast.ElementType.TYPE_REFERENCE
import com.pinterest.ktlint.core.ast.ElementType.VALUE_ARGUMENT_LIST
import com.pinterest.ktlint.core.ast.ElementType.VALUE_PARAMETER_LIST
import org.cqfn.diktat.common.config.rules.RulesConfig
import org.cqfn.diktat.ruleset.constants.Warnings.EXTENSION_FUNCTION_SAME_SIGNATURE
import org.cqfn.diktat.ruleset.utils.findAllNodesWithSpecificType
import org.cqfn.diktat.ruleset.utils.findChildAfter
import org.cqfn.diktat.ruleset.utils.findChildBefore
import org.cqfn.diktat.ruleset.utils.findLeafWithSpecificType
import org.cqfn.diktat.ruleset.utils.getAllChildrenWithType
import org.cqfn.diktat.ruleset.utils.getFirstChildWithType
import org.cqfn.diktat.ruleset.utils.hasChildOfType
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtNamedFunction

typealias RelatedClasses = List<Pair<String, String>>
typealias SimilarSignatures = List<Pair<ExtensionFunctionsSameNameRule.ExtensionFunction, ExtensionFunctionsSameNameRule.ExtensionFunction>>

/**
* This rule checks if extension functions with the same signature don't have related classes
*/
class ExtensionFunctionsSameNameRule(private val configRules: List<RulesConfig>) : Rule("extension-functions-same-name") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if there are more than two matching classes? It should be a rare case, but at least we shouldn't crash when we encounter it and output something reasonable.

private lateinit var emitWarn: ((offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit)
private var isFixMode: Boolean = false

override fun visit(node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) {
emitWarn = emit
isFixMode = autoCorrect

/**
* 1) Collect all classes that extend other classes (collect related classes)
* 2) Collect all extension functions with same signature
* 3) Check if classes of functions are related
*/
if (node.elementType == FILE) {
val relatedClasses = collectAllRelatedClasses(node)
val extFunctionsWithSameName = collectAllExtensionFunctions(node)
handleFunctions(relatedClasses, extFunctionsWithSameName)
}

}

// Fixme: should find all related classes in project, not only in file
@Suppress("UnsafeCallOnNullableType")
private fun collectAllRelatedClasses(node: ASTNode) : List<Pair<String, String>> {
val classListWithInheritance = node.findAllNodesWithSpecificType(CLASS)
.filterNot { (it.psi as KtClass).isInterface() }
.filter { it.hasChildOfType(SUPER_TYPE_LIST) }

val pairs = mutableListOf<Pair<String, String>>()
classListWithInheritance.forEach {
val callEntries = it.findChildByType(SUPER_TYPE_LIST)!!.getAllChildrenWithType(SUPER_TYPE_CALL_ENTRY)

callEntries.forEach { entry ->
aktsay6 marked this conversation as resolved.
Show resolved Hide resolved
if (entry.hasChildOfType(VALUE_ARGUMENT_LIST)) {
aktsay6 marked this conversation as resolved.
Show resolved Hide resolved
val className = (it.psi as KtClass).name!!
val entryName = entry.findLeafWithSpecificType(IDENTIFIER)!!
pairs.add(Pair(className, entryName.text))
}
}
}
return pairs
}

@Suppress("UnsafeCallOnNullableType")
private fun collectAllExtensionFunctions(node: ASTNode) : SimilarSignatures {
val extensionFunctionList = node.findAllNodesWithSpecificType(FUN).filter { it.hasChildOfType(TYPE_REFERENCE) && it.hasChildOfType(DOT) }
val distinctFunctionSignatures = mutableMapOf<FunctionSignature, ASTNode>() // maps function signatures on node it is used by
val extensionFunctionsPairs = mutableListOf<Pair<ExtensionFunction, ExtensionFunction>>() // pairs extension functions with same signature

extensionFunctionList.forEach {
val functionName = (it.psi as KtNamedFunction).name!!
val params = it.getFirstChildWithType(VALUE_PARAMETER_LIST)!!.text
aktsay6 marked this conversation as resolved.
Show resolved Hide resolved
val returnType = it.findChildAfter(COLON, TYPE_REFERENCE)?.text
val className = it.findChildBefore(DOT, TYPE_REFERENCE)!!.text
val signature = FunctionSignature(functionName, params, returnType)

if (distinctFunctionSignatures.contains(signature)) {
val secondFuncClassName = distinctFunctionSignatures[signature]!!.findChildBefore(DOT, TYPE_REFERENCE)!!.text
extensionFunctionsPairs.add(Pair(
ExtensionFunction(secondFuncClassName, signature, distinctFunctionSignatures[signature]!!),
ExtensionFunction(className, signature, it)))
} else {
distinctFunctionSignatures[signature] = it
}
}

return extensionFunctionsPairs
}

private fun handleFunctions(relatedClasses: RelatedClasses, functions: SimilarSignatures) {
functions.forEach {
val firstClassName = it.first.className
val secondClassName = it.second.className

if (relatedClasses.hasRelatedClasses(Pair(firstClassName, secondClassName))) {
raiseWarning(it.first.node, it.first, it.second)
raiseWarning(it.second.node, it.first, it.second)
}
}
}

private fun RelatedClasses.hasRelatedClasses(pair: Pair<String, String>): Boolean {
forEach {
if (it.first == pair.first && it.second == pair.second
|| it.first == pair.second && it.second == pair.first)
return true
}
return false
aktsay6 marked this conversation as resolved.
Show resolved Hide resolved
}

private fun raiseWarning(node: ASTNode, firstFunc: ExtensionFunction, secondFunc: ExtensionFunction) {
EXTENSION_FUNCTION_SAME_SIGNATURE.warn(configRules, emitWarn, isFixMode, "$firstFunc and $secondFunc", node.startOffset, node)
}

data class FunctionSignature(val name: String, val parameters: String, val returnType: String?) {
override fun toString(): String {
return "$name$parameters${if(returnType != null) ": $returnType" else ""}"
}
}
data class ExtensionFunction(val className: String, val signature: FunctionSignature, val node: ASTNode) {
override fun toString(): String {
return "fun $className.$signature"
}
}
}
4 changes: 4 additions & 0 deletions diktat-rules/src/main/resources/diktat-analysis-huawei.yml
Original file line number Diff line number Diff line change
Expand Up @@ -351,5 +351,9 @@
configuration: {}
# Checks if there are any trivial getters or setters
- name: TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED
enabled: true
configuration: {}
# Checks if extension function with the same signature don't have related classes
- name: EXTENSION_FUNCTION_SAME_SIGNATURE
enabled: true
configuration: {}
4 changes: 4 additions & 0 deletions diktat-rules/src/main/resources/diktat-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -353,5 +353,9 @@
configuration: {}
# Checks if there are any trivial getters or setters
- name: TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED
enabled: true
configuration: {}
# Checks if extension function with the same signature don't have related classes
- name: EXTENSION_FUNCTION_SAME_SIGNATURE
enabled: true
configuration: {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package org.cqfn.diktat.ruleset.chapter6

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

class ExtensionFunctionsSameNameWarnTest : LintTestBase(::ExtensionFunctionsSameNameRule) {
private val ruleId = "$DIKTAT_RULE_SET_ID:extension-functions-same-name"

@Test
@Tag(EXTENSION_FUNCTION_SAME_SIGNATURE)
fun `should trigger on functions with same signatures`() {
lintMethod(
"""
|open class A
|class B: A(), C
|
|fun A.foo() = "A"
|fun B.foo() = "B"
|
|fun printClassName(s: A) { print(s.foo()) }
|
|fun main() { printClassName(B()) }
""".trimMargin(),
LintError(4, 1, ruleId, "${Warnings.EXTENSION_FUNCTION_SAME_SIGNATURE.warnText()} fun A.foo() and fun B.foo()"),
LintError(5, 1, ruleId, "${Warnings.EXTENSION_FUNCTION_SAME_SIGNATURE.warnText()} fun A.foo() and fun B.foo()")
)
}

@Test
@Tag(EXTENSION_FUNCTION_SAME_SIGNATURE)
fun `should not trigger on functions with different signatures`() {
lintMethod(
"""
|open class A
|class B: A(), C
|
|fun A.foo(): Boolean = return true
|fun B.foo() = "B"
|
|fun printClassName(s: A) { print(s.foo()) }
|
|fun main() { printClassName(B()) }
""".trimMargin()
)
}

@Test
@Tag(EXTENSION_FUNCTION_SAME_SIGNATURE)
fun `should not trigger on functions with unrelated classes`() {
lintMethod(
"""
|interface A
|class B: A
|class C
|
|fun C.foo() = "C"
|fun B.foo() = "B"
|
|fun printClassName(s: A) { print(s.foo()) }
|
|fun main() { printClassName(B()) }
""".trimMargin()
)
}

@Test
@Tag(EXTENSION_FUNCTION_SAME_SIGNATURE)
fun `should not trigger on regular func`() {
lintMethod(
"""
|interface A
|class B: A
|class C
|
|fun foo() = "C"
|fun bar() = "B"
|
|fun printClassName(s: A) { print(s.foo()) }
|
|fun main() { printClassName(B()) }
""".trimMargin()
)
}

@Test
@Disabled
@Tag(EXTENSION_FUNCTION_SAME_SIGNATURE)
fun `should trigger on classes in other files`() {
lintMethod(
"""
|fun A.foo() = "A"
|fun B.foo() = "B"
|
|fun printClassName(s: A) { print(s.foo()) }
|
|fun main() { printClassName(B()) }
""".trimMargin(),
LintError(1, 1, ruleId, "${Warnings.EXTENSION_FUNCTION_SAME_SIGNATURE.warnText()} fun A.foo() and fun B.foo()"),
LintError(2, 1, ruleId, "${Warnings.EXTENSION_FUNCTION_SAME_SIGNATURE.warnText()} fun A.foo() and fun B.foo()")
)
}
}
3 changes: 2 additions & 1 deletion info/available-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,5 @@
| 6 | 6.1.4 | MULTIPLE_INIT_BLOCKS | Checks that classes have only one init block | yes | no | - |
| 6 | 6.1.6 | CLASS_SHOULD_NOT_BE_ABSTRACT | Checks: if abstract class has any abstract method. If not, warns that class should not be abstract<br>Fix: deletes abstract modifier | yes | - | - |
| 6 | 6.1.9 | WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR | Check: used the name of a variable in the custom getter or setter | no | - |
| 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.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.2.2 | EXTENSION_FUNCTION_SAME_SIGNATURE | Checks if extension function has the same signature as another extension function and their classes are related | no | - | + |