diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/UnusedSymbolInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/UnusedSymbolInspection.kt index 140367ff47e..8a65b903a6f 100644 --- a/idea/src/org/jetbrains/kotlin/idea/inspections/UnusedSymbolInspection.kt +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/UnusedSymbolInspection.kt @@ -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(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(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() + 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) +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/unusedSymbol/companionViaImport2.kt b/idea/testData/inspectionsLocal/unusedSymbol/companionViaImport2.kt new file mode 100644 index 00000000000..6cd4da13812 --- /dev/null +++ b/idea/testData/inspectionsLocal/unusedSymbol/companionViaImport2.kt @@ -0,0 +1,17 @@ +// PROBLEM: none + +import TestClass.NamedObject.CONST + +class TestClass{ + fun method(){ + CONST + } + + private object NamedObject { + const val CONST = "abc" + } +} + +fun main(){ + TestClass().method() +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/unusedSymbol/innerClass.kt b/idea/testData/inspectionsLocal/unusedSymbol/innerClass.kt new file mode 100644 index 00000000000..ee15254be6b --- /dev/null +++ b/idea/testData/inspectionsLocal/unusedSymbol/innerClass.kt @@ -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(val parameter: CharSequence) { + companion object { + fun CharSequence.doStuff1() = Used(this).doStuff2() + } + + private fun doStuff2() = "" + } +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/unusedSymbol/typeAlias.kt b/idea/testData/inspectionsLocal/unusedSymbol/typeAlias.kt new file mode 100644 index 00000000000..d5158b1037a --- /dev/null +++ b/idea/testData/inspectionsLocal/unusedSymbol/typeAlias.kt @@ -0,0 +1,12 @@ +// PROBLEM: none + +import Other.HELLO + +val ONE = HELLO + +enum class MyEnum { + HELLO, + WORLD +} + +typealias Other = MyEnum \ No newline at end of file diff --git a/idea/testData/quickfix/removeUnused/companionViaImport3.kt b/idea/testData/quickfix/removeUnused/companionViaImport3.kt new file mode 100644 index 00000000000..80b9557ab03 --- /dev/null +++ b/idea/testData/quickfix/removeUnused/companionViaImport3.kt @@ -0,0 +1,8 @@ +// "Safe delete 'NamedObject'" "true" +import TestClass.NamedObject + +class TestClass{ + private object NamedObject { + const val CONST = "abc" + } +} \ No newline at end of file diff --git a/idea/testData/quickfix/removeUnused/companionViaImport3.kt.after b/idea/testData/quickfix/removeUnused/companionViaImport3.kt.after new file mode 100644 index 00000000000..44b6f8fa276 --- /dev/null +++ b/idea/testData/quickfix/removeUnused/companionViaImport3.kt.after @@ -0,0 +1,4 @@ +// "Safe delete 'NamedObject'" "true" + +class TestClass{ +} \ No newline at end of file diff --git a/idea/testData/quickfix/removeUnused/typeAlias2.kt b/idea/testData/quickfix/removeUnused/typeAlias2.kt new file mode 100644 index 00000000000..dc6d05a28a8 --- /dev/null +++ b/idea/testData/quickfix/removeUnused/typeAlias2.kt @@ -0,0 +1,9 @@ +// "Safe delete 'Other'" "true" +import Other + +enum class MyEnum { + HELLO, + WORLD +} + +typealias Other = MyEnum diff --git a/idea/testData/quickfix/removeUnused/typeAlias2.kt.after b/idea/testData/quickfix/removeUnused/typeAlias2.kt.after new file mode 100644 index 00000000000..0cf6bcb138c --- /dev/null +++ b/idea/testData/quickfix/removeUnused/typeAlias2.kt.after @@ -0,0 +1,7 @@ +// "Safe delete 'Other'" "true" + +enum class MyEnum { + HELLO, + WORLD +} + diff --git a/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java index 4b833afd7a6..cd47addef0f 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java @@ -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"); diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java index 23cf26b68ae..1eeec852f4a 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixTestGenerated.java @@ -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");