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
3 changes: 3 additions & 0 deletions diktat-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,7 @@
enabled: true
# Checks explicit supertype qualification
- name: USELESS_SUPERTYPE
enabled: true
# Checks if extension function with the same signature don't have related classes
- name: EXTENSION_FUNCTION_SAME_SIGNATURE
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 @@ -215,4 +215,6 @@ public object WarningNames {
public const val COMPACT_OBJECT_INITIALIZATION: String = "COMPACT_OBJECT_INITIALIZATION"

public const val USELESS_SUPERTYPE: String = "USELESS_SUPERTYPE"

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,10 +125,11 @@ 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"),
CUSTOM_GETTERS_SETTERS(false, "Custom getters and setters are not recommended, use class methods instead"),
COMPACT_OBJECT_INITIALIZATION(true, "class instance can be initialized in `apply` block"),
USELESS_SUPERTYPE(true,"unnecessary supertype specification"),
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 receiver classes are related"),
;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,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,137 @@
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
import org.jetbrains.kotlin.psi.KtParameter
import org.jetbrains.kotlin.psi.KtParameterList

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 { classNode ->
val callEntries = classNode.findChildByType(SUPER_TYPE_LIST)!!.getAllChildrenWithType(SUPER_TYPE_CALL_ENTRY)

callEntries.forEach { entry ->
aktsay6 marked this conversation as resolved.
Show resolved Hide resolved
val className = (classNode.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 { func ->
val functionName = (func.psi as KtNamedFunction).name!!
// List<String> is used to show param names in warning
val params = (func.getFirstChildWithType(VALUE_PARAMETER_LIST)!!.psi as KtParameterList).parameters.map { it.name!! }
val returnType = func.findChildAfter(COLON, TYPE_REFERENCE)?.text
val className = func.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, func)))
} else {
distinctFunctionSignatures[signature] = func
}
}

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 {
return any { it.first == pair.first && it.second == pair.second
|| it.first == pair.second && it.second == pair.first }
}

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: List<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"
}
}
}
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 @@ -285,4 +285,7 @@
enabled: true
# Checks explicit supertype qualification
- name: USELESS_SUPERTYPE
enabled: true
# Checks if extension function with the same signature don't have related classes
- name: EXTENSION_FUNCTION_SAME_SIGNATURE
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 @@ -287,4 +287,7 @@
enabled: true
# Checks explicit supertype qualification
- name: USELESS_SUPERTYPE
enabled: true
# Checks if extension function with the same signature don't have related classes
- name: EXTENSION_FUNCTION_SAME_SIGNATURE
enabled: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
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
|class D: A()
|
|fun A.foo() = "A"
|fun B.foo() = "B"
|fun D.foo() = "D"
|
|fun printClassName(s: A) { print(s.foo()) }
|
|fun main() { printClassName(B()) }
""".trimMargin(),
LintError(5, 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 D.foo[]"),
LintError(6, 1, ruleId, "${Warnings.EXTENSION_FUNCTION_SAME_SIGNATURE.warnText()} fun A.foo[] and fun B.foo[]"),
LintError(7, 1, ruleId, "${Warnings.EXTENSION_FUNCTION_SAME_SIGNATURE.warnText()} fun A.foo[] and fun D.foo[]"),
)
}

@Test
@Tag(EXTENSION_FUNCTION_SAME_SIGNATURE)
fun `should trigger on functions with same signatures 2`() {
lintMethod(
"""
|open class A
|class B: A(), C
|
|fun A.foo(some: Int) = "A"
|fun B.foo(some: Int) = "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[some] and fun B.foo[some]"),
LintError(5, 1, ruleId, "${Warnings.EXTENSION_FUNCTION_SAME_SIGNATURE.warnText()} fun A.foo[some] and fun B.foo[some]")
)
}

@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 different signatures 2`() {
lintMethod(
"""
|open class A
|class B: A(), C
|
|fun A.foo() = return true
|fun B.foo(some: Int) = "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 @@ -100,4 +100,5 @@
| 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.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.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 | - | + |