diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 5cfd263d0af..bf628291234 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -1046,7 +1046,7 @@ /> + + { - val usedDescriptors = HashSet() - file.accept(object : JetVisitorVoid() { - override fun visitElement(element: PsiElement) { - ProgressIndicatorProvider.checkCanceled() - element.acceptChildren(this) - } + val visitor = CollectUsedDescriptorsVisitor(file, recursive = true) + file.accept(visitor) + return visitor.descriptors + } + } - override fun visitImportList(importList: JetImportList) { - } + public class CollectUsedDescriptorsVisitor(val file: JetFile, val recursive: Boolean) : JetVisitorVoid() { + private val _descriptors = HashSet() - override fun visitPackageDirective(directive: JetPackageDirective) { - } + public val descriptors: Set + get() = _descriptors - override fun visitJetElement(element: JetElement) { - for (reference in element.getReferences()) { - if (reference is JetReference) { - val referencedName = (element as? JetNameReferenceExpression)?.getReferencedNameAsName() //TODO: other types of references + override fun visitElement(element: PsiElement) { + if (recursive) { + ProgressIndicatorProvider.checkCanceled() + element.acceptChildren(this) + } + } - val bindingContext = element.analyze() - //class qualifiers that refer to companion objects should be considered (containing) class references - val targets = bindingContext[BindingContext.SHORT_REFERENCE_TO_COMPANION_OBJECT, element as? JetReferenceExpression]?.let { listOf(it) } - ?: reference.resolveToDescriptors(bindingContext) - for (target in targets) { - if (!target.canBeReferencedViaImport()) continue - if (target is PackageViewDescriptor && target.fqName.parent() == FqName.ROOT) continue // no need to import top-level packages + override fun visitImportList(importList: JetImportList) { + } - if (!target.isExtension) { // for non-extension targets, count only non-qualified simple name usages - if (element !is JetNameReferenceExpression) continue - if (element.getIdentifier() == null) continue // skip 'this' etc - if (element.getReceiverExpression() != null) continue - } + override fun visitPackageDirective(directive: JetPackageDirective) { + } - val importableDescriptor = target.getImportableDescriptor() - if (referencedName != null && importableDescriptor.getName() != referencedName) continue // resolved via alias + override fun visitJetElement(element: JetElement) { + if (!recursive && element.parents.any { it is JetImportDirective || it is JetPackageDirective }) return - if (isAccessibleAsMember(importableDescriptor, element)) continue + for (reference in element.getReferences()) { + if (reference !is JetReference) continue - usedDescriptors.add(importableDescriptor) - } - } + val referencedName = (element as? JetNameReferenceExpression)?.getReferencedNameAsName() //TODO: other types of references + + val bindingContext = element.analyze() + //class qualifiers that refer to companion objects should be considered (containing) class references + val targets = bindingContext[BindingContext.SHORT_REFERENCE_TO_COMPANION_OBJECT, element as? JetReferenceExpression]?.let { listOf(it) } + ?: reference.resolveToDescriptors(bindingContext) + for (target in targets) { + if (!target.canBeReferencedViaImport()) continue + if (target is PackageViewDescriptor && target.fqName.parent() == FqName.ROOT) continue // no need to import top-level packages + + if (!target.isExtension) { // for non-extension targets, count only non-qualified simple name usages + if (element !is JetNameReferenceExpression) continue + if (element.getIdentifier() == null) continue // skip 'this' etc + if (element.getReceiverExpression() != null) continue } - super.visitJetElement(element) + val importableDescriptor = target.getImportableDescriptor() + if (referencedName != null && importableDescriptor.getName() != referencedName) continue // resolved via alias + + if (isAccessibleAsMember(importableDescriptor, element)) continue + + _descriptors.add(importableDescriptor) } - }) - return usedDescriptors + } + + super.visitJetElement(element) } private fun isAccessibleAsMember(target: DeclarationDescriptor, place: JetElement): Boolean { diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/UnusedImportInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/UnusedImportInspection.kt new file mode 100644 index 00000000000..42fc93a2791 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/UnusedImportInspection.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.inspections + +import com.intellij.codeInspection.LocalInspectionToolSession +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.util.Key +import com.intellij.psi.PsiElementVisitor +import org.jetbrains.kotlin.idea.JetBundle +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.caches.resolve.resolveImportReference +import org.jetbrains.kotlin.idea.imports.KotlinImportOptimizer +import org.jetbrains.kotlin.idea.imports.importableFqNameSafe +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.JetFile +import org.jetbrains.kotlin.psi.JetSimpleNameExpression +import org.jetbrains.kotlin.psi.psiUtil.getQualifiedElementSelector +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.bindingContextUtil.getReferenceTargets +import java.util.HashSet + +class UnusedImportInspection : AbstractKotlinInspection() { + private val visitorKey = Key("UnusedImportInspection.visitorKey") + + override fun runForWholeFile() = true + + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor { + val visitor = KotlinImportOptimizer.CollectUsedDescriptorsVisitor(session.getFile() as JetFile, recursive = false) + session.putUserData(visitorKey, visitor) + return visitor + } + + override fun inspectionFinished(session: LocalInspectionToolSession, problemsHolder: ProblemsHolder) { + val usedDescriptors = session.getUserData(visitorKey)!!.descriptors + + val fqNames = HashSet() + val parentFqNames = HashSet() + for (descriptor in usedDescriptors) { + val fqName = descriptor.importableFqNameSafe + fqNames.add(fqName) + val parentFqName = fqName.parent() + if (!parentFqName.isRoot()) { + parentFqNames.add(parentFqName) + } + } + + val file = session.getFile() as JetFile + val directives = file.getImportDirectives() + for (directive in directives) { + val importPath = directive.getImportPath() ?: continue + if (importPath.getAlias() != null) continue // highlighting of unused alias imports not supported yet + val isUsed = if (importPath.isAllUnder()) { + importPath.fqnPart() in parentFqNames + } + else { + importPath.fqnPart() in fqNames + } + + if (!isUsed) { + val nameExpression = directive.getImportedReference()?.getQualifiedElementSelector() as? JetSimpleNameExpression + if (nameExpression == null || nameExpression.getReferenceTargets(nameExpression.analyze()).isEmpty()) continue // do not highlight unresolved imports as unused + + problemsHolder.registerProblem( + directive, + "Unused import directive", + ProblemHighlightType.LIKE_UNUSED_SYMBOL) + } + } + } +} \ No newline at end of file diff --git a/idea/testData/inspections/unusedImport/file.kt b/idea/testData/inspections/unusedImport/file.kt new file mode 100644 index 00000000000..e04fe39f75e --- /dev/null +++ b/idea/testData/inspections/unusedImport/file.kt @@ -0,0 +1,15 @@ +import java.io.* // unused +import java.sql.* // used + +import java.util.HashMap // unused +import java.util.ArrayList // used + +import java.unresolved.* // unused but unresolved +import java.net.Unresolved // unused but unresolved + +import java.net.ConnectException as CE // highlighting of unused aliases not implemented yet + +fun foo(list: ArrayList) { + list.add("") + Date() +} diff --git a/idea/testData/inspections/unusedImport/inspectionData/expected.xml b/idea/testData/inspections/unusedImport/inspectionData/expected.xml new file mode 100644 index 00000000000..d2568c984bf --- /dev/null +++ b/idea/testData/inspections/unusedImport/inspectionData/expected.xml @@ -0,0 +1,20 @@ + + + file.kt + 1 + light_idea_test_case + + Unused import directive + Unused import directive + + + + file.kt + 4 + light_idea_test_case + + Unused import directive + Unused import directive + + + diff --git a/idea/testData/inspections/unusedImport/inspectionData/inspections.test b/idea/testData/inspections/unusedImport/inspectionData/inspections.test new file mode 100644 index 00000000000..3867df33cb9 --- /dev/null +++ b/idea/testData/inspections/unusedImport/inspectionData/inspections.test @@ -0,0 +1 @@ +// INSPECTION_CLASS: org.jetbrains.kotlin.idea.inspections.UnusedImportInspection diff --git a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/JetInspectionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/JetInspectionTestGenerated.java index 70dafa1ab80..f275df4c325 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/JetInspectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/JetInspectionTestGenerated.java @@ -94,6 +94,12 @@ public class JetInspectionTestGenerated extends AbstractJetInspectionTest { doTest(fileName); } + @TestMetadata("unusedImport/inspectionData/inspections.test") + public void testUnusedImport_inspectionData_Inspections_test() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/inspections/unusedImport/inspectionData/inspections.test"); + doTest(fileName); + } + @TestMetadata("unusedReceiverParameter/inspectionData/inspections.test") public void testUnusedReceiverParameter_inspectionData_Inspections_test() throws Exception { String fileName = JetTestUtils.navigationMetadata("idea/testData/inspections/unusedReceiverParameter/inspectionData/inspections.test");