Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,136 @@ class UnusedDependenciesTest : RunnerTest() {
.clean() shouldBe """ModuleCheck found 0 issues"""
}

@Test
fun `module contributing a named companion object, consumed in the same package should not be unused`() {
// https://github.com/RBusarow/ModuleCheck/issues/705

settings.deleteUnused = false

val lib1 = kotlinProject(":lib1") {
addKotlinSource(
"""
package com.modulecheck.common

class Lib1Class {
companion object Factory {
fun create() = Lib1Class()
}
}
""".trimIndent()
)
}

val lib2 = kotlinProject(":lib2") {
addDependency(ConfigurationName.implementation, lib1)

buildFile {
"""
plugins {
kotlin("jvm")
}

dependencies {
implementation(project(path = ":lib1"))
}
"""
}

addKotlinSource(
"""
package com.modulecheck.common

fun foo() {
bar(Lib1Class.create())
}

fun bar(any: Any) = Unit
""".trimIndent(),
SourceSetName.MAIN
)
}

run().isSuccess shouldBe true

lib2.buildFile shouldHaveText """
plugins {
kotlin("jvm")
}

dependencies {
implementation(project(path = ":lib1"))
}
"""

logger.parsedReport() shouldBe listOf()
}

@Test
fun `module contributing a named companion object, consumed by the companion name should not be unused`() {
// https://github.com/RBusarow/ModuleCheck/issues/705

settings.deleteUnused = false

val lib1 = kotlinProject(":lib1") {
addKotlinSource(
"""
package com.modulecheck.common

class Lib1Class {
companion object Factory {
fun create() = Lib1Class()
}
}
""".trimIndent()
)
}

val lib2 = kotlinProject(":lib2") {
addDependency(ConfigurationName.implementation, lib1)

buildFile {
"""
plugins {
kotlin("jvm")
}

dependencies {
implementation(project(path = ":lib1"))
}
"""
}

addKotlinSource(
"""
package com.modulecheck.common

import com.modulecheck.common.Lib1Class.Factory

fun foo() {
bar(Factory.create())
}

fun bar(any: Any) = Unit
""".trimIndent(),
SourceSetName.MAIN
)
}

run().isSuccess shouldBe true

lib2.buildFile shouldHaveText """
plugins {
kotlin("jvm")
}

dependencies {
implementation(project(path = ":lib1"))
}
"""

logger.parsedReport() shouldBe listOf()
}

@Test
fun `testImplementation used in test should not be unused`() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import modulecheck.parsing.psi.internal.callSiteName
import modulecheck.parsing.psi.internal.getByNameOrIndex
import modulecheck.parsing.psi.internal.getChildrenOfTypeRecursive
import modulecheck.parsing.psi.internal.identifier
import modulecheck.parsing.psi.internal.isCompanionObject
import modulecheck.parsing.psi.internal.isInCompanionObject
import modulecheck.parsing.psi.internal.isJvmStatic
import modulecheck.parsing.psi.internal.isPartOf
import modulecheck.parsing.psi.internal.isPrivateOrInternal
Expand Down Expand Up @@ -146,18 +148,29 @@ class RealKotlinFile(
}
}

val psi = this@declaredNames

fun parseCompanionObjectDeclarations(companionName: String) {
both(nameAsString)

if (isStatic()) {
both(nameAsString.remove(".$companionName"))
} else if (psi is KtCallableDeclaration) {
kotlin(nameAsString.remove(".$companionName"))
}
}

when {
nameAsString.contains(".Companion") -> {
both(nameAsString)
psi.isCompanionObject() -> {
parseCompanionObjectDeclarations(psi.name ?: "Companion")
}

if (isStatic()) {
both(nameAsString.remove(".Companion"))
} else if (this@declaredNames is KtCallableDeclaration) {
kotlin(nameAsString.remove(".Companion"))
}
psi.isInCompanionObject() -> {
val companion = containingClassOrObject as KtObjectDeclaration
parseCompanionObjectDeclarations(companion.name ?: "Companion")
}

isTopLevelKtOrJavaMember() && this@declaredNames !is KtClassOrObject && !isStatic() -> {
isTopLevelKtOrJavaMember() && psi !is KtClassOrObject && !isStatic() -> {
kotlin(nameAsString)

jvmSimpleNames().forEach {
Expand Down Expand Up @@ -190,7 +203,7 @@ class RealKotlinFile(

val jvmNames = jvmSimpleNames()

if (this@declaredNames is KtFunction && jvmNameOrNull() == null) {
if (psi is KtFunction && psi.jvmNameOrNull() == null) {
both(nameAsString)
} else {
kotlin(nameAsString)
Expand All @@ -204,8 +217,7 @@ class RealKotlinFile(
}
}

this@declaredNames is KtParameter ||
(this@declaredNames is KtProperty && !isTopLevelKtOrJavaMember()) -> {
psi is KtParameter || (psi is KtProperty && !psi.isTopLevelKtOrJavaMember()) -> {

kotlin(nameAsString)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.KtBlockExpression
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtClassLiteralExpression
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
Expand All @@ -50,6 +49,7 @@ import org.jetbrains.kotlin.psi.psiUtil.isObjectLiteral
import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
import java.io.File
import kotlin.contracts.contract

inline fun <reified T : PsiElement> PsiElement.isPartOf() = getNonStrictParentOfType<T>() != null

Expand Down Expand Up @@ -126,6 +126,7 @@ suspend fun PsiElement.fqNameOrNull(
?.let { return it }
?: text
}

is KtNameReferenceExpression -> getReferencedName()
is KtUserType -> {
val isGenericType = children.any { it is KtTypeArgumentList }
Expand Down Expand Up @@ -163,6 +164,7 @@ suspend fun PsiElement.fqNameOrNull(
text
}
}

is KtTypeReference -> {
val children = children
if (children.size == 1) {
Expand All @@ -173,13 +175,15 @@ suspend fun PsiElement.fqNameOrNull(
text
}
}

is KtNullableType -> return innerType?.fqNameOrNull(project, sourceSetName)
is KtAnnotationEntry -> return typeReference?.fqNameOrNull(project, sourceSetName)
is KtClassLiteralExpression -> {
// Returns "Abc" for "Abc::class".
return children.singleOrNull()
?.fqNameOrNull(project, sourceSetName)
}

is KtSuperTypeListEntry -> return typeReference?.fqNameOrNull(project, sourceSetName)
else -> return null
}
Expand All @@ -197,6 +201,7 @@ suspend fun PsiElement.fqNameOrNull(
when {
matchingImportPaths.size == 1 ->
return matchingImportPaths[0].fqName

matchingImportPaths.size > 1 ->
return matchingImportPaths.firstOrNull { importPath ->
project.canResolveFqName(importPath.fqName, sourceSetName)
Expand All @@ -210,6 +215,7 @@ suspend fun PsiElement.fqNameOrNull(
when {
matchingImportPaths.size == 1 ->
return FqName("${matchingImportPaths[0].fqName.parent()}.$classReference")

matchingImportPaths.size > 1 ->
return matchingImportPaths.firstOrNull { importPath ->
project.canResolveFqName(
Expand Down Expand Up @@ -257,10 +263,24 @@ suspend fun PsiElement.fqNameOrNull(

fun KtDeclaration.isInObject() = containingClassOrObject?.isObjectLiteral() ?: false

fun KtDeclaration.isInCompanionObject() = containingClassOrObject?.isCompanionObject() ?: false
/**
* @return true if the receiver declaration is inside a companion object
*/
fun KtDeclaration.isInCompanionObject(): Boolean {
return containingClassOrObject?.isCompanionObject() ?: false
}

fun KtDeclaration.isInObjectOrCompanionObject() = isInObject() || isInCompanionObject()

fun KtClassOrObject.isCompanionObject(): Boolean = this is KtObjectDeclaration && isCompanion()
/**
* @return true if the receiver declaration is a companion object
*/
fun KtDeclaration.isCompanionObject(): Boolean {
contract {
returns(true) implies (this@isCompanionObject is KtObjectDeclaration)
}
return this is KtObjectDeclaration && isCompanion()
}

fun PsiElement.isQualifiedPropertyOrCallExpression(): Boolean {
// properties which are qualified have a direct parent of `KtQualifiedExpression`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,39 @@ internal class KotlinFileTest : ProjectTest(), NamedSymbolTest {
}
}

@Test
fun `named companion object and function should also have declarations using original class name`() =
test {

val project = kotlinProject(":subject")

val file = project.createFile(
"""
package com.subject

class SubjectClass {

companion object Factory {
fun create() = SubjectClass()
}
}
"""
)

file shouldBe {
references {
interpretedKotlin("SubjectClass")
interpretedKotlin("com.subject.SubjectClass")
}
declarations {
agnostic("com.subject.SubjectClass")
agnostic("com.subject.SubjectClass.Factory")
agnostic("com.subject.SubjectClass.Factory.create")
kotlin("com.subject.SubjectClass.create")
}
}
}

@Test
fun `explicit fully qualified type of public property in public class should be api reference`() =
test {
Expand Down