Fix false positive "Unused symbol" for type alias & private nested class/object
#KT-21526 Fixed #KT-30527 Fixed
This commit is contained in:
@@ -63,8 +63,10 @@ import org.jetbrains.kotlin.idea.search.usagesSearch.getAccessorNames
|
||||
import org.jetbrains.kotlin.idea.search.usagesSearch.getClassNameForCompanionObject
|
||||
import org.jetbrains.kotlin.idea.stubindex.KotlinSourceFilterScope
|
||||
import org.jetbrains.kotlin.idea.util.ProjectRootsUtil
|
||||
import org.jetbrains.kotlin.idea.util.application.runWriteAction
|
||||
import org.jetbrains.kotlin.idea.util.compat.psiSearchHelperInstance
|
||||
import org.jetbrains.kotlin.idea.util.hasActualsFor
|
||||
import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.*
|
||||
@@ -302,47 +304,61 @@ class UnusedSymbolInspection : AbstractKotlinInspection() {
|
||||
hasPlatformImplementations(declaration, descriptor)
|
||||
}
|
||||
|
||||
private fun checkDeclaration(declaration: KtNamedDeclaration, importedDeclaration: KtNamedDeclaration): Boolean =
|
||||
declaration !in importedDeclaration.parentsWithSelf && !hasNonTrivialUsages(importedDeclaration)
|
||||
|
||||
private val KtNamedDeclaration.isObjectOrEnum: Boolean get() = this is KtObjectDeclaration || this is KtClass && isEnum()
|
||||
|
||||
private fun checkReference(ref: PsiReference, declaration: KtNamedDeclaration, descriptor: DeclarationDescriptor?): Boolean {
|
||||
if (declaration.isAncestor(ref.element)) return true // usages inside element's declaration are not counted
|
||||
|
||||
if (ref.element.parent is KtValueArgumentName) return true // usage of parameter in form of named argument is not counted
|
||||
|
||||
val import = ref.element.getParentOfType<KtImportDirective>(false)
|
||||
if (import != null) {
|
||||
if (import.aliasName != null && import.aliasName != declaration.name) {
|
||||
return false
|
||||
}
|
||||
// check if we import member(s) from object / nested object / enum and search for their usages
|
||||
val originalDeclaration = (descriptor as? TypeAliasDescriptor)?.classDescriptor?.findPsi() as? KtNamedDeclaration
|
||||
if (declaration is KtClassOrObject || originalDeclaration is KtClassOrObject) {
|
||||
if (import.isAllUnder) {
|
||||
val importedFrom = import.importedReference?.getQualifiedElementSelector()?.mainReference?.resolve()
|
||||
as? KtClassOrObject ?: return true
|
||||
return importedFrom.declarations.none { it is KtNamedDeclaration && hasNonTrivialUsages(it) }
|
||||
} else {
|
||||
if (import.importedFqName != declaration.fqName) {
|
||||
val importedDeclaration =
|
||||
import.importedReference?.getQualifiedElementSelector()?.mainReference?.resolve() as? KtNamedDeclaration
|
||||
?: return true
|
||||
|
||||
if (declaration.isObjectOrEnum || importedDeclaration.containingClassOrObject is KtObjectDeclaration) return checkDeclaration(
|
||||
declaration,
|
||||
importedDeclaration
|
||||
)
|
||||
|
||||
if (originalDeclaration?.isObjectOrEnum == true) return checkDeclaration(
|
||||
originalDeclaration,
|
||||
importedDeclaration
|
||||
)
|
||||
|
||||
// check type alias
|
||||
if (importedDeclaration.fqName == declaration.fqName) return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun hasReferences(
|
||||
declaration: KtNamedDeclaration,
|
||||
descriptor: DeclarationDescriptor?,
|
||||
useScope: SearchScope
|
||||
): Boolean {
|
||||
|
||||
fun checkReference(ref: PsiReference): Boolean {
|
||||
if (declaration.isAncestor(ref.element)) return true // usages inside element's declaration are not counted
|
||||
|
||||
if (ref.element.parent is KtValueArgumentName) return true // usage of parameter in form of named argument is not counted
|
||||
|
||||
val import = ref.element.getParentOfType<KtImportDirective>(false)
|
||||
if (import != null) {
|
||||
if (import.aliasName != null && import.aliasName != declaration.name) {
|
||||
return false
|
||||
}
|
||||
// check if we import member(s) from object / nested object / enum and search for their usages
|
||||
if (declaration is KtClassOrObject) {
|
||||
if (import.isAllUnder) {
|
||||
val importedFrom = import.importedReference?.getQualifiedElementSelector()?.mainReference?.resolve()
|
||||
as? KtClassOrObject ?: return true
|
||||
return importedFrom.declarations.none { it is KtNamedDeclaration && hasNonTrivialUsages(it) }
|
||||
} else {
|
||||
if (import.importedFqName != declaration.fqName) {
|
||||
val importedDeclaration =
|
||||
import.importedReference?.getQualifiedElementSelector()?.mainReference?.resolve() as? KtNamedDeclaration
|
||||
?: return true
|
||||
if (declaration is KtObjectDeclaration ||
|
||||
(declaration is KtClass && declaration.isEnum()) ||
|
||||
importedDeclaration.containingClassOrObject is KtObjectDeclaration
|
||||
) {
|
||||
return declaration !in importedDeclaration.parentsWithSelf && !hasNonTrivialUsages(importedDeclaration)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
fun checkReference(ref: PsiReference): Boolean = checkReference(ref, declaration, descriptor)
|
||||
|
||||
val searchParameters = KotlinReferencesSearchParameters(
|
||||
declaration,
|
||||
@@ -366,7 +382,20 @@ class UnusedSymbolInspection : AbstractKotlinInspection() {
|
||||
}
|
||||
}
|
||||
|
||||
return referenceUsed
|
||||
return referenceUsed || checkPrivateDeclaration(declaration, descriptor)
|
||||
}
|
||||
|
||||
private fun checkPrivateDeclaration(declaration: KtNamedDeclaration, descriptor: DeclarationDescriptor?): Boolean {
|
||||
if (descriptor == null || !declaration.isPrivateNestedClassOrObject) return false
|
||||
|
||||
val set = hashSetOf<KtSimpleNameExpression>()
|
||||
declaration.containingKtFile.importList?.acceptChildren(simpleNameExpressionRecursiveVisitor {
|
||||
set += it
|
||||
})
|
||||
|
||||
return set.mapNotNull { it.referenceExpression() }
|
||||
.filter { descriptor in it.resolveMainReferenceToDescriptors() }
|
||||
.any { !checkReference(it.mainReference, declaration, descriptor) }
|
||||
}
|
||||
|
||||
private fun KtCallableDeclaration.canBeHandledByLightMethods(descriptor: DeclarationDescriptor?): Boolean {
|
||||
@@ -482,9 +511,22 @@ class SafeDeleteFix(declaration: KtDeclaration) : LocalQuickFix {
|
||||
RemoveUnusedFunctionParameterFix(declaration).invoke(project, declaration.findExistingEditor(), declaration.containingKtFile)
|
||||
} else {
|
||||
ApplicationManager.getApplication().invokeLater(
|
||||
{ SafeDeleteHandler.invoke(project, arrayOf(declaration), false) },
|
||||
{ safeDelete(project, declaration) },
|
||||
ModalityState.NON_MODAL
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val KtNamedDeclaration.isPrivateNestedClassOrObject: Boolean get() = this is KtClassOrObject && isPrivate() && !isTopLevel()
|
||||
|
||||
private fun safeDelete(project: Project, declaration: PsiElement) {
|
||||
if (declaration is KtNamedDeclaration && declaration.isPrivateNestedClassOrObject) {
|
||||
runWriteAction {
|
||||
declaration.containingKtFile.importDirectives.forEach {
|
||||
if (it.importedFqName == declaration.fqName) it.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
SafeDeleteHandler.invoke(project, arrayOf(declaration), false)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// PROBLEM: none
|
||||
|
||||
import TestClass.NamedObject.CONST
|
||||
|
||||
class TestClass{
|
||||
fun method(){
|
||||
CONST
|
||||
}
|
||||
|
||||
private object NamedObject<caret> {
|
||||
const val CONST = "abc"
|
||||
}
|
||||
}
|
||||
|
||||
fun main(){
|
||||
TestClass().method()
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// PROBLEM: none
|
||||
|
||||
import MVE.Used.Companion.doStuff1
|
||||
|
||||
fun main() {
|
||||
MVE("").test()
|
||||
}
|
||||
|
||||
data class MVE(private val parameter: CharSequence) {
|
||||
fun test() = parameter.doStuff1()
|
||||
|
||||
private data class Used<caret>(val parameter: CharSequence) {
|
||||
companion object {
|
||||
fun CharSequence.doStuff1() = Used(this).doStuff2()
|
||||
}
|
||||
|
||||
private fun doStuff2() = ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// PROBLEM: none
|
||||
|
||||
import Other.HELLO
|
||||
|
||||
val ONE = HELLO
|
||||
|
||||
enum class MyEnum {
|
||||
HELLO,
|
||||
WORLD
|
||||
}
|
||||
|
||||
typealias Other<caret> = MyEnum
|
||||
@@ -0,0 +1,8 @@
|
||||
// "Safe delete 'NamedObject'" "true"
|
||||
import TestClass.NamedObject
|
||||
|
||||
class TestClass{
|
||||
private object NamedObject<caret> {
|
||||
const val CONST = "abc"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// "Safe delete 'NamedObject'" "true"
|
||||
|
||||
class TestClass{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// "Safe delete 'Other'" "true"
|
||||
import Other
|
||||
|
||||
enum class MyEnum {
|
||||
HELLO,
|
||||
WORLD
|
||||
}
|
||||
|
||||
typealias Other<caret> = MyEnum
|
||||
@@ -0,0 +1,7 @@
|
||||
// "Safe delete 'Other'" "true"
|
||||
|
||||
enum class MyEnum {
|
||||
HELLO,
|
||||
WORLD
|
||||
}
|
||||
|
||||
+15
@@ -8658,6 +8658,11 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest {
|
||||
runTest("idea/testData/inspectionsLocal/unusedSymbol/companionViaImport.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("companionViaImport2.kt")
|
||||
public void testCompanionViaImport2() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/unusedSymbol/companionViaImport2.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("dataInlineClassDeclaration.kt")
|
||||
public void testDataInlineClassDeclaration() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/unusedSymbol/dataInlineClassDeclaration.kt");
|
||||
@@ -8698,6 +8703,11 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest {
|
||||
runTest("idea/testData/inspectionsLocal/unusedSymbol/inlineClassMemberFunction.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("innerClass.kt")
|
||||
public void testInnerClass() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/unusedSymbol/innerClass.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("internal.kt")
|
||||
public void testInternal() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/unusedSymbol/internal.kt");
|
||||
@@ -8708,6 +8718,11 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest {
|
||||
runTest("idea/testData/inspectionsLocal/unusedSymbol/propertyOfInlineClassType.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("typeAlias.kt")
|
||||
public void testTypeAlias() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/unusedSymbol/typeAlias.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("withJvmNameUsedFromKotlin.kt")
|
||||
public void testWithJvmNameUsedFromKotlin() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/unusedSymbol/withJvmNameUsedFromKotlin.kt");
|
||||
|
||||
@@ -10096,6 +10096,11 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
|
||||
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/removeUnused"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
|
||||
}
|
||||
|
||||
@TestMetadata("companionViaImport3.kt")
|
||||
public void testCompanionViaImport3() throws Exception {
|
||||
runTest("idea/testData/quickfix/removeUnused/companionViaImport3.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("deledage.kt")
|
||||
public void testDeledage() throws Exception {
|
||||
runTest("idea/testData/quickfix/removeUnused/deledage.kt");
|
||||
@@ -10126,6 +10131,11 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
|
||||
runTest("idea/testData/quickfix/removeUnused/triangle2.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("typeAlias2.kt")
|
||||
public void testTypeAlias2() throws Exception {
|
||||
runTest("idea/testData/quickfix/removeUnused/typeAlias2.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("unusedClass.kt")
|
||||
public void testUnusedClass() throws Exception {
|
||||
runTest("idea/testData/quickfix/removeUnused/unusedClass.kt");
|
||||
|
||||
Reference in New Issue
Block a user