Fix false positive "Unused symbol" for type alias & private nested class/object

#KT-21526 Fixed
 #KT-30527 Fixed
This commit is contained in:
Dmitry Gridin
2019-03-25 15:46:03 +07:00
parent 4441b1579f
commit 74cd53963d
10 changed files with 181 additions and 38 deletions
@@ -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{
}
+9
View File
@@ -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
}
@@ -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");