Skip to content

Commit 38f42df

Browse files
RBusarowkodiakhq[bot]
authored andcommitted
parse the declarations of named companion objects and their members
fixes #705
1 parent 3b341b6 commit 38f42df

File tree

4 files changed

+209
-14
lines changed

4 files changed

+209
-14
lines changed

modulecheck-core/src/test/kotlin/modulecheck/core/UnusedDependenciesTest.kt

+130
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,136 @@ class UnusedDependenciesTest : RunnerTest() {
834834
.clean() shouldBe """ModuleCheck found 0 issues"""
835835
}
836836

837+
@Test
838+
fun `module contributing a named companion object, consumed in the same package should not be unused`() {
839+
// https://github.com/RBusarow/ModuleCheck/issues/705
840+
841+
settings.deleteUnused = false
842+
843+
val lib1 = kotlinProject(":lib1") {
844+
addKotlinSource(
845+
"""
846+
package com.modulecheck.common
847+
848+
class Lib1Class {
849+
companion object Factory {
850+
fun create() = Lib1Class()
851+
}
852+
}
853+
""".trimIndent()
854+
)
855+
}
856+
857+
val lib2 = kotlinProject(":lib2") {
858+
addDependency(ConfigurationName.implementation, lib1)
859+
860+
buildFile {
861+
"""
862+
plugins {
863+
kotlin("jvm")
864+
}
865+
866+
dependencies {
867+
implementation(project(path = ":lib1"))
868+
}
869+
"""
870+
}
871+
872+
addKotlinSource(
873+
"""
874+
package com.modulecheck.common
875+
876+
fun foo() {
877+
bar(Lib1Class.create())
878+
}
879+
880+
fun bar(any: Any) = Unit
881+
""".trimIndent(),
882+
SourceSetName.MAIN
883+
)
884+
}
885+
886+
run().isSuccess shouldBe true
887+
888+
lib2.buildFile shouldHaveText """
889+
plugins {
890+
kotlin("jvm")
891+
}
892+
893+
dependencies {
894+
implementation(project(path = ":lib1"))
895+
}
896+
"""
897+
898+
logger.parsedReport() shouldBe listOf()
899+
}
900+
901+
@Test
902+
fun `module contributing a named companion object, consumed by the companion name should not be unused`() {
903+
// https://github.com/RBusarow/ModuleCheck/issues/705
904+
905+
settings.deleteUnused = false
906+
907+
val lib1 = kotlinProject(":lib1") {
908+
addKotlinSource(
909+
"""
910+
package com.modulecheck.common
911+
912+
class Lib1Class {
913+
companion object Factory {
914+
fun create() = Lib1Class()
915+
}
916+
}
917+
""".trimIndent()
918+
)
919+
}
920+
921+
val lib2 = kotlinProject(":lib2") {
922+
addDependency(ConfigurationName.implementation, lib1)
923+
924+
buildFile {
925+
"""
926+
plugins {
927+
kotlin("jvm")
928+
}
929+
930+
dependencies {
931+
implementation(project(path = ":lib1"))
932+
}
933+
"""
934+
}
935+
936+
addKotlinSource(
937+
"""
938+
package com.modulecheck.common
939+
940+
import com.modulecheck.common.Lib1Class.Factory
941+
942+
fun foo() {
943+
bar(Factory.create())
944+
}
945+
946+
fun bar(any: Any) = Unit
947+
""".trimIndent(),
948+
SourceSetName.MAIN
949+
)
950+
}
951+
952+
run().isSuccess shouldBe true
953+
954+
lib2.buildFile shouldHaveText """
955+
plugins {
956+
kotlin("jvm")
957+
}
958+
959+
dependencies {
960+
implementation(project(path = ":lib1"))
961+
}
962+
"""
963+
964+
logger.parsedReport() shouldBe listOf()
965+
}
966+
837967
@Test
838968
fun `testImplementation used in test should not be unused`() {
839969

modulecheck-parsing/psi/src/main/kotlin/modulecheck/parsing/psi/RealKotlinFile.kt

+23-11
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import modulecheck.parsing.psi.internal.callSiteName
2222
import modulecheck.parsing.psi.internal.getByNameOrIndex
2323
import modulecheck.parsing.psi.internal.getChildrenOfTypeRecursive
2424
import modulecheck.parsing.psi.internal.identifier
25+
import modulecheck.parsing.psi.internal.isCompanionObject
26+
import modulecheck.parsing.psi.internal.isInCompanionObject
2527
import modulecheck.parsing.psi.internal.isJvmStatic
2628
import modulecheck.parsing.psi.internal.isPartOf
2729
import modulecheck.parsing.psi.internal.isPrivateOrInternal
@@ -146,18 +148,29 @@ class RealKotlinFile(
146148
}
147149
}
148150

151+
val psi = this@declaredNames
152+
153+
fun parseCompanionObjectDeclarations(companionName: String) {
154+
both(nameAsString)
155+
156+
if (isStatic()) {
157+
both(nameAsString.remove(".$companionName"))
158+
} else if (psi is KtCallableDeclaration) {
159+
kotlin(nameAsString.remove(".$companionName"))
160+
}
161+
}
162+
149163
when {
150-
nameAsString.contains(".Companion") -> {
151-
both(nameAsString)
164+
psi.isCompanionObject() -> {
165+
parseCompanionObjectDeclarations(psi.name ?: "Companion")
166+
}
152167

153-
if (isStatic()) {
154-
both(nameAsString.remove(".Companion"))
155-
} else if (this@declaredNames is KtCallableDeclaration) {
156-
kotlin(nameAsString.remove(".Companion"))
157-
}
168+
psi.isInCompanionObject() -> {
169+
val companion = containingClassOrObject as KtObjectDeclaration
170+
parseCompanionObjectDeclarations(companion.name ?: "Companion")
158171
}
159172

160-
isTopLevelKtOrJavaMember() && this@declaredNames !is KtClassOrObject && !isStatic() -> {
173+
isTopLevelKtOrJavaMember() && psi !is KtClassOrObject && !isStatic() -> {
161174
kotlin(nameAsString)
162175

163176
jvmSimpleNames().forEach {
@@ -190,7 +203,7 @@ class RealKotlinFile(
190203

191204
val jvmNames = jvmSimpleNames()
192205

193-
if (this@declaredNames is KtFunction && jvmNameOrNull() == null) {
206+
if (psi is KtFunction && psi.jvmNameOrNull() == null) {
194207
both(nameAsString)
195208
} else {
196209
kotlin(nameAsString)
@@ -204,8 +217,7 @@ class RealKotlinFile(
204217
}
205218
}
206219

207-
this@declaredNames is KtParameter ||
208-
(this@declaredNames is KtProperty && !isTopLevelKtOrJavaMember()) -> {
220+
psi is KtParameter || (psi is KtProperty && !psi.isTopLevelKtOrJavaMember()) -> {
209221

210222
kotlin(nameAsString)
211223

modulecheck-parsing/psi/src/main/kotlin/modulecheck/parsing/psi/internal/psiElement.kt

+23-3
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import org.jetbrains.kotlin.psi.KtAnnotationEntry
2828
import org.jetbrains.kotlin.psi.KtBlockExpression
2929
import org.jetbrains.kotlin.psi.KtCallExpression
3030
import org.jetbrains.kotlin.psi.KtClassLiteralExpression
31-
import org.jetbrains.kotlin.psi.KtClassOrObject
3231
import org.jetbrains.kotlin.psi.KtDeclaration
3332
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
3433
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
@@ -50,6 +49,7 @@ import org.jetbrains.kotlin.psi.psiUtil.isObjectLiteral
5049
import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf
5150
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
5251
import java.io.File
52+
import kotlin.contracts.contract
5353

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

@@ -126,6 +126,7 @@ suspend fun PsiElement.fqNameOrNull(
126126
?.let { return it }
127127
?: text
128128
}
129+
129130
is KtNameReferenceExpression -> getReferencedName()
130131
is KtUserType -> {
131132
val isGenericType = children.any { it is KtTypeArgumentList }
@@ -163,6 +164,7 @@ suspend fun PsiElement.fqNameOrNull(
163164
text
164165
}
165166
}
167+
166168
is KtTypeReference -> {
167169
val children = children
168170
if (children.size == 1) {
@@ -173,13 +175,15 @@ suspend fun PsiElement.fqNameOrNull(
173175
text
174176
}
175177
}
178+
176179
is KtNullableType -> return innerType?.fqNameOrNull(project, sourceSetName)
177180
is KtAnnotationEntry -> return typeReference?.fqNameOrNull(project, sourceSetName)
178181
is KtClassLiteralExpression -> {
179182
// Returns "Abc" for "Abc::class".
180183
return children.singleOrNull()
181184
?.fqNameOrNull(project, sourceSetName)
182185
}
186+
183187
is KtSuperTypeListEntry -> return typeReference?.fqNameOrNull(project, sourceSetName)
184188
else -> return null
185189
}
@@ -197,6 +201,7 @@ suspend fun PsiElement.fqNameOrNull(
197201
when {
198202
matchingImportPaths.size == 1 ->
199203
return matchingImportPaths[0].fqName
204+
200205
matchingImportPaths.size > 1 ->
201206
return matchingImportPaths.firstOrNull { importPath ->
202207
project.canResolveFqName(importPath.fqName, sourceSetName)
@@ -210,6 +215,7 @@ suspend fun PsiElement.fqNameOrNull(
210215
when {
211216
matchingImportPaths.size == 1 ->
212217
return FqName("${matchingImportPaths[0].fqName.parent()}.$classReference")
218+
213219
matchingImportPaths.size > 1 ->
214220
return matchingImportPaths.firstOrNull { importPath ->
215221
project.canResolveFqName(
@@ -257,10 +263,24 @@ suspend fun PsiElement.fqNameOrNull(
257263

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

260-
fun KtDeclaration.isInCompanionObject() = containingClassOrObject?.isCompanionObject() ?: false
266+
/**
267+
* @return true if the receiver declaration is inside a companion object
268+
*/
269+
fun KtDeclaration.isInCompanionObject(): Boolean {
270+
return containingClassOrObject?.isCompanionObject() ?: false
271+
}
272+
261273
fun KtDeclaration.isInObjectOrCompanionObject() = isInObject() || isInCompanionObject()
262274

263-
fun KtClassOrObject.isCompanionObject(): Boolean = this is KtObjectDeclaration && isCompanion()
275+
/**
276+
* @return true if the receiver declaration is a companion object
277+
*/
278+
fun KtDeclaration.isCompanionObject(): Boolean {
279+
contract {
280+
returns(true) implies (this@isCompanionObject is KtObjectDeclaration)
281+
}
282+
return this is KtObjectDeclaration && isCompanion()
283+
}
264284

265285
fun PsiElement.isQualifiedPropertyOrCallExpression(): Boolean {
266286
// properties which are qualified have a direct parent of `KtQualifiedExpression`

modulecheck-parsing/psi/src/test/kotlin/modulecheck/parsing/psi/KotlinFileTest.kt

+33
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,39 @@ internal class KotlinFileTest : ProjectTest(), NamedSymbolTest {
455455
}
456456
}
457457

458+
@Test
459+
fun `named companion object and function should also have declarations using original class name`() =
460+
test {
461+
462+
val project = kotlinProject(":subject")
463+
464+
val file = project.createFile(
465+
"""
466+
package com.subject
467+
468+
class SubjectClass {
469+
470+
companion object Factory {
471+
fun create() = SubjectClass()
472+
}
473+
}
474+
"""
475+
)
476+
477+
file shouldBe {
478+
references {
479+
interpretedKotlin("SubjectClass")
480+
interpretedKotlin("com.subject.SubjectClass")
481+
}
482+
declarations {
483+
agnostic("com.subject.SubjectClass")
484+
agnostic("com.subject.SubjectClass.Factory")
485+
agnostic("com.subject.SubjectClass.Factory.create")
486+
kotlin("com.subject.SubjectClass.create")
487+
}
488+
}
489+
}
490+
458491
@Test
459492
fun `explicit fully qualified type of public property in public class should be api reference`() =
460493
test {

0 commit comments

Comments
 (0)