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");