PY-4330 PY-77891 Extract PyUnusedImportInspection from PyUnresolvedReferencesInspection

Make the inspection for unused imports runForWholeFile=true to stop its blinking.

GitOrigin-RevId: 967e95edc229d021420b176f56a5d4b0fb44c3d4
This commit is contained in:
Mikhail Golubev
2025-05-08 17:01:37 +00:00
committed by intellij-monorepo-bot
parent e93d6305de
commit 2faeaa0da8
18 changed files with 512 additions and 374 deletions
@@ -215,6 +215,11 @@
bundle="messages.PyPsiBundle" key="INSP.NAME.unresolved.refs" groupKey="INSP.GROUP.python" enabledByDefault="true"
level="WARNING"
implementationClass="com.jetbrains.python.inspections.unresolvedReference.PyUnresolvedReferencesInspection"/>
<localInspection language="Python" shortName="PyUnusedImportsInspection"
suppressId="PyUnusedImports"
bundle="messages.PyPsiBundle" key="INSP.NAME.unused.imports" groupKey="INSP.GROUP.python" enabledByDefault="true"
level="WARNING" runForWholeFile="true"
implementationClass="com.jetbrains.python.inspections.PyUnusedImportsInspection"/>
<localInspection language="Python" shortName="PyTestParametrizedInspection" suppressId="PyTestParametrized"
bundle="messages.PyPsiBundle" key="INSP.NAME.pytest-parametrized" groupKey="INSP.GROUP.python"
enabledByDefault="true" level="WARNING"
@@ -0,0 +1,30 @@
<html>
<body>
Reports unused import statements in Python code.
<p>
This inspection detects import statements that are not used in the code and can be safely removed.
Removing unused imports helps to keep the code clean and reduces the risk of name conflicts.
</p>
<!-- tooltip end -->
<p><b>Example:</b></p>
<pre><code>
import os # Unused import
import sys # Used import
from math import pi # Unused import
print(sys.version)
</code></pre>
<p>
The inspection provides a quick fix to optimize imports, which removes all unused import statements.
</p>
<p>
Note that some imports might be used indirectly (e.g., for side effects) and should not be removed.
You can suppress this inspection for specific imports if they are needed for side effects.
</p>
</body>
</html>
@@ -927,6 +927,9 @@ INSP.unresolved.refs.ignore.references.label=Ignored references:
unresolved.docstring.param.reference=Function ''{0}'' does not have a parameter ''{1}''
unresolved.import.reference=No module named ''{0}''
# PyUnusedImportInspection
INSP.NAME.unused.imports=Unused imports
# PyRedundantParenthesesInspection
INSP.NAME.redundant.parentheses=Redundant parentheses
INSP.redundant.parens.ignore.empty.lists.of.base.classes=Ignore empty lists of base classes
@@ -23,8 +23,7 @@ import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import com.jetbrains.python.codeInsight.imports.AddImportHelper.ImportPriority;
import com.jetbrains.python.formatter.PyCodeStyleSettings;
import com.jetbrains.python.inspections.unresolvedReference.PyUnresolvedReferencesVisitor;
import com.jetbrains.python.inspections.unresolvedReference.SimplePyUnresolvedReferencesInspection;
import com.jetbrains.python.inspections.PyUnusedImportsInspection;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import com.jetbrains.python.psi.impl.PythonLanguageLevelPusher;
@@ -61,7 +60,7 @@ public final class PyImportOptimizer implements ImportOptimizer {
return EmptyRunnable.INSTANCE;
}
PyUnresolvedReferencesVisitor visitor = prepare(file);
PyUnusedImportsInspection.Visitor visitor = prepare(file);
return () -> {
LOG.debug(String.format("----------------- OPTIMIZE IMPORTS STARTED (%s) -----------------", file.getVirtualFile()));
visitor.optimizeImports();
@@ -72,14 +71,14 @@ public final class PyImportOptimizer implements ImportOptimizer {
};
}
private PyUnresolvedReferencesVisitor prepare(@NotNull PsiFile file) {
private PyUnusedImportsInspection.Visitor prepare(@NotNull PsiFile file) {
final PsiFile contextFile = FileContextUtil.getContextFile(file);
final PsiFile rfile = ObjectUtils.chooseNotNull(contextFile, file);
TypeEvalContext context = TypeEvalContext.codeAnalysis(file.getProject(), rfile);
SimplePyUnresolvedReferencesInspection inspection = new SimplePyUnresolvedReferencesInspection();
final PyUnresolvedReferencesVisitor visitor = new SimplePyUnresolvedReferencesInspection.Visitor(
PyUnusedImportsInspection inspection = new PyUnusedImportsInspection();
PyUnusedImportsInspection.Visitor visitor = new PyUnusedImportsInspection.Visitor(
null, inspection, context, PythonLanguageLevelPusher.getLanguageLevelForFile(file)
);
file.accept(new PyRecursiveElementVisitor() {
@@ -0,0 +1,437 @@
package com.jetbrains.python.inspections
import com.google.common.collect.Sets
import com.intellij.codeInsight.controlflow.ControlFlowUtil
import com.intellij.codeInsight.controlflow.Instruction
import com.intellij.codeInspection.LocalInspectionToolSession
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.lang.injection.InjectedLanguageManager
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.util.Condition
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.Ref
import com.intellij.openapi.util.Version
import com.intellij.psi.*
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.QualifiedName
import com.intellij.util.Processor
import com.intellij.util.ThrowableRunnable
import com.intellij.util.containers.ContainerUtil
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.PythonRuntimeService
import com.jetbrains.python.codeInsight.PyCodeInsightSettings
import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache
import com.jetbrains.python.codeInsight.controlflow.ReadWriteInstruction
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil
import com.jetbrains.python.codeInsight.imports.OptimizeImportsQuickFix
import com.jetbrains.python.inspections.PyInspectionVisitor.getContext
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.impl.PyBuiltinCache
import com.jetbrains.python.psi.impl.PyPsiUtils
import com.jetbrains.python.psi.impl.PythonLanguageLevelPusher
import com.jetbrains.python.psi.impl.stubs.evaluateVersionsForElement
import com.jetbrains.python.psi.resolve.ImportedResolveResult
import com.jetbrains.python.psi.resolve.PyResolveContext
import com.jetbrains.python.psi.resolve.QualifiedNameFinder
import com.jetbrains.python.psi.types.TypeEvalContext
import java.util.*
class PyUnusedImportsInspection : PyInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
val visitor = Visitor(holder = holder,
myInspection = this,
typeEvalContext = getContext(session),
languageLevel = PythonLanguageLevelPusher.getLanguageLevelForFile(session.file))
session.putUserData(KEY, visitor)
return visitor
}
override fun inspectionFinished(session: LocalInspectionToolSession, holder: ProblemsHolder) {
val visitor = session.getUserData(KEY)
checkNotNull(visitor)
ReadAction.run<RuntimeException?>(
ThrowableRunnable {
if (PyCodeInsightSettings.getInstance().HIGHLIGHT_UNUSED_IMPORTS) {
visitor.highlightUnusedImports()
}
visitor.highlightImportsInsideGuards()
}
)
session.putUserData(KEY, null)
}
class Visitor(
holder: ProblemsHolder?,
private val myInspection: PyInspection,
typeEvalContext: TypeEvalContext,
languageLevel: LanguageLevel,
) :
PyInspectionVisitor(holder, typeEvalContext) {
private val myAllImports = mutableSetOf<PyImportedNameDefiner>()
private val myImportsInsideGuard = mutableSetOf<PyImportedNameDefiner>()
private val myUsedImports = mutableSetOf<PyImportedNameDefiner>()
private val myUnresolvedImports = mutableSetOf<PyImportedNameDefiner>()
private val myVersion: Version = Version(languageLevel.majorVersion, languageLevel.minorVersion, 0)
override fun visitPyImportElement(node: PyImportElement) {
super.visitPyImportElement(node)
val fromImport = PsiTreeUtil.getParentOfType<PyFromImportStatement?>(node, PyFromImportStatement::class.java)
if (fromImport == null || !fromImport.isFromFuture()) {
myAllImports.add(node)
}
}
override fun visitPyStarImportElement(node: PyStarImportElement) {
super.visitPyStarImportElement(node)
myAllImports.add(node)
}
override fun visitComment(comment: PsiComment) {
super.visitComment(comment)
if (comment is PsiLanguageInjectionHost) {
processInjection(comment as PsiLanguageInjectionHost)
}
}
override fun visitPyElement(node: PyElement) {
super.visitPyElement(node)
if (node is PyReferenceOwner) {
val resolveContext = PyResolveContext.defaultContext(myTypeEvalContext)
processReference(node, node.getReference(resolveContext))
}
else {
for (reference in node.getReferences()) {
processReference(node, reference)
}
}
}
private fun processReference(node: PyElement, reference: PsiReference?) {
if (reference == null || reference.isSoft()) {
return
}
val guard = getImportErrorGuard(node)
if (guard != null) {
processReferenceInImportGuard(node, guard)
return
}
var target: PsiElement? = null
val unresolved: Boolean
if (reference is PsiPolyVariantReference) {
val resolveResults = reference.multiResolve(false)
unresolved = (resolveResults.size == 0)
for (resolveResult in resolveResults) {
if (target == null && resolveResult.isValidResult()) {
target = resolveResult.getElement()
}
if (resolveResult is ImportedResolveResult) {
val definer = resolveResult.getDefiner()
if (definer != null) {
myUsedImports.add(definer)
}
}
}
}
else {
target = reference.resolve()
unresolved = (target == null)
}
if (unresolved) {
val ignoreUnresolved = ignoreUnresolved(node, reference) || !evaluateVersionsForElement(node).contains(myVersion)
if (!ignoreUnresolved) {
val severity = if (reference is PsiReferenceEx)
reference.getUnresolvedHighlightSeverity(myTypeEvalContext)
else
HighlightSeverity.ERROR
if (severity == null) return
}
// don't highlight unresolved imports as unused
val importElement = node.getParent() as? PyImportElement
if (importElement != null) {
myUnresolvedImports.add(importElement)
}
}
}
private fun ignoreUnresolved(node: PyElement, reference: PsiReference): Boolean {
var ignoreUnresolved = false
for (extension in PyInspectionExtension.EP_NAME.extensionList) {
if (extension.ignoreUnresolvedReference(node, reference, myTypeEvalContext)) {
ignoreUnresolved = true
break
}
}
return ignoreUnresolved
}
private fun processReferenceInImportGuard(node: PyElement, guard: PyExceptPart) {
val importElement = PsiTreeUtil.getParentOfType<PyImportElement?>(node, PyImportElement::class.java)
if (importElement != null) {
val visibleName = importElement.getVisibleName()
val owner = ScopeUtil.getScopeOwner(importElement)
if (visibleName != null && owner != null) {
val allWrites: MutableCollection<PsiElement?> = ScopeUtil.getElementsOfAccessType(visibleName, owner, ReadWriteInstruction.ACCESS.WRITE)
val hasWriteInsideGuard = ContainerUtil.exists<PsiElement?>(allWrites, Condition { e: PsiElement? -> PsiTreeUtil.isAncestor(guard, e!!, false) })
if (!hasWriteInsideGuard && !shouldSkipMissingWriteInsideGuard(guard, visibleName)) {
myImportsInsideGuard.add(importElement)
}
}
}
}
private fun shouldSkipMissingWriteInsideGuard(guard: PyExceptPart, name: String): Boolean {
return isDefinedInParentScope(name, guard) || PyBuiltinCache.getInstance(guard).getByName(name) != null ||
controlFlowAlwaysTerminatesInsideGuard(guard)
}
private fun isDefinedInParentScope(name: String, anchor: PsiElement): Boolean {
return ScopeUtil.getDeclarationScopeOwner(ScopeUtil.getScopeOwner(anchor), name) != null
}
private fun controlFlowAlwaysTerminatesInsideGuard(guard: PyExceptPart): Boolean {
val owner = ScopeUtil.getScopeOwner(guard)
if (owner == null) return false
val flow = ControlFlowCache.getControlFlow(owner)
val instructions = flow.getInstructions()
val start = ControlFlowUtil.findInstructionNumberByElement(instructions, guard.exceptClass)
if (start <= 0) return false
val canEscapeGuard = Ref.create<Boolean>(false)
// TODO can we replace that with return ControlFlowUtil.process?
ControlFlowUtil.process(instructions, start, Processor { instruction: Instruction ->
val e = instruction.getElement()
if (e != null && !PsiTreeUtil.isAncestor(guard, e, true)) {
canEscapeGuard.set(true)
return@Processor false
}
return@Processor true
})
return !canEscapeGuard.get()
}
fun getImportsInsideGuard(): MutableCollection<PyImportedNameDefiner> {
return Collections.unmodifiableCollection(myImportsInsideGuard)
}
private fun getImportErrorGuard(node: PyElement?): PyExceptPart? {
val importStatement = PsiTreeUtil.getParentOfType(node, PyImportStatementBase::class.java)
if (importStatement != null) {
val tryPart = PsiTreeUtil.getParentOfType(node, PyTryPart::class.java)
if (tryPart != null) {
val tryExceptStatement = PsiTreeUtil.getParentOfType(tryPart, PyTryExceptStatement::class.java)
if (tryExceptStatement != null) {
for (exceptPart in tryExceptStatement.getExceptParts()) {
val expr = exceptPart.getExceptClass()
if (expr != null && "ImportError" == expr.getName()) {
return exceptPart
}
}
}
}
}
return null
}
private fun processInjection(node: PsiLanguageInjectionHost?) {
if (node == null) return
val files = InjectedLanguageManager.getInstance(node.getProject()).getInjectedPsiFiles(node)
if (files != null) {
for (pair in files) {
object : PyRecursiveElementVisitor() {
override fun visitPyElement(element: PyElement) {
super.visitPyElement(element)
if (element is PyReferenceOwner) {
val resolveContext = PyResolveContext.defaultContext(myTypeEvalContext)
val reference = element.getReference(resolveContext)
markTargetImportsAsUsed(reference)
}
}
}.visitElement(pair.getFirst()!!)
}
}
}
private fun markTargetImportsAsUsed(reference: PsiPolyVariantReference) {
val resolveResults = reference.multiResolve(false)
for (resolveResult in resolveResults) {
if (resolveResult is ImportedResolveResult) {
val definer = resolveResult.getDefiner()
if (definer != null) {
myUsedImports.add(definer)
}
}
}
}
fun highlightUnusedImports() {
val extensions: List<PyInspectionExtension?> = PyInspectionExtension.EP_NAME.extensionList
val unused: List<PsiElement> = collectUnusedImportElements()
for (element in unused) {
if (ContainerUtil.exists<PyInspectionExtension?>(extensions, Condition { extension: PyInspectionExtension? -> extension!!.ignoreUnused(element, myTypeEvalContext) })) {
continue
}
if (!evaluateVersionsForElement(element).contains(myVersion)) {
continue
}
if (element.getTextLength() > 0) {
val fix = OptimizeImportsQuickFix()
registerProblem(element, PyPsiBundle.message("INSP.unused.import.statement"), ProblemHighlightType.LIKE_UNUSED_SYMBOL, null, fix)
}
}
}
fun highlightImportsInsideGuards() {
val usedImportsInsideImportGuards: HashSet<PyImportedNameDefiner?> = Sets.newHashSet(getImportsInsideGuard())
usedImportsInsideImportGuards.retainAll(myUsedImports)
for (definer in usedImportsInsideImportGuards) {
val importElement = PyUtil.`as`<PyImportElement?>(definer, PyImportElement::class.java)
if (importElement == null) {
continue
}
val asElement = importElement.getAsNameElement()
val toHighlight: PyElement? = if (asElement != null) asElement else importElement.getImportReferenceExpression()
registerProblem(toHighlight,
PyPsiBundle.message("INSP.try.except.import.error",
importElement.getVisibleName()),
ProblemHighlightType.LIKE_UNKNOWN_SYMBOL)
}
}
fun collectUnusedImportElements(): List<PsiElement> {
if (myAllImports.isEmpty()) {
return emptyList()
}
// PY-1315 Unused imports inspection shouldn't work in python REPL console
val first: PyImportedNameDefiner = myAllImports.first()
if (first.getContainingFile() is PyExpressionCodeFragment || PythonRuntimeService.getInstance().isInPydevConsole(first)) {
return emptyList()
}
val result: MutableList<PsiElement> = ArrayList<PsiElement>()
val unusedImports: MutableSet<PyImportedNameDefiner> = HashSet(myAllImports)
unusedImports.removeAll(myUsedImports)
// TODO revise that
unusedImports.removeAll(myUnresolvedImports)
// Remove those unsed, that are reported to be skipped by extension points
val unusedImportToSkip: MutableSet<PyImportedNameDefiner?> = HashSet<PyImportedNameDefiner?>()
for (unusedImport in unusedImports) {
if (ContainerUtil.exists<PyInspectionExtension>(PyInspectionExtension.EP_NAME.extensionList, Condition { o: PyInspectionExtension? -> o!!.ignoreUnusedImports(unusedImport) })) {
unusedImportToSkip.add(unusedImport)
}
}
unusedImports.removeAll(unusedImportToSkip)
val usedImportNames: MutableSet<String?> = HashSet<String?>()
for (usedImport in myUsedImports) {
for (e in usedImport.iterateNames()) {
usedImportNames.add(e.getName())
}
}
val unusedStatements: MutableSet<PyImportStatementBase?> = HashSet<PyImportStatementBase?>()
var packageQName: QualifiedName? = null
var dunderAll: MutableList<String?>? = null
// TODO: Use strategies instead of pack of "continue"
iterUnused@ for (unusedImport in unusedImports) {
if (packageQName == null) {
val file = unusedImport.getContainingFile()
if (file is PyFile) {
dunderAll = file.getDunderAll()
}
if (file != null && PyUtil.isPackage(file)) {
packageQName = QualifiedNameFinder.findShortestImportableQName(file)
}
}
val importStatement = PsiTreeUtil.getParentOfType(unusedImport, PyImportStatementBase::class.java)
if (importStatement != null && !unusedStatements.contains(importStatement) && !myUsedImports.contains(unusedImport)) {
val inspection: PyInspection = checkNotNull(myInspection)
if (inspection.isSuppressedFor(importStatement)) {
continue
}
// don't remove as unused imports in try/except statements
if (PsiTreeUtil.getParentOfType<PyTryExceptStatement?>(importStatement, PyTryExceptStatement::class.java) != null) {
continue
}
// Don't report conditional imports as unused
if (PsiTreeUtil.getParentOfType<PyIfStatement?>(unusedImport, PyIfStatement::class.java) != null) {
for (e in unusedImport.iterateNames()) {
if (usedImportNames.contains(e.getName())) {
continue@iterUnused
}
}
}
val importedElement: PsiFileSystemItem?
if (unusedImport is PyImportElement) {
val element = unusedImport.resolve()
if (element == null) {
if (unusedImport.getImportedQName() != null) {
//Mark import as unused even if it can't be resolved
if (areAllImportsUnused(importStatement, unusedImports)) {
result.add(importStatement)
}
else {
result.add(unusedImport)
}
}
continue
}
if (dunderAll != null && dunderAll.contains(unusedImport.getVisibleName())) {
continue
}
importedElement = element.getContainingFile()
}
else {
assert(importStatement is PyFromImportStatement)
importedElement = (importStatement as PyFromImportStatement).resolveImportSource()
if (importedElement == null) {
continue
}
}
if (packageQName != null && importedElement != null) {
val importedQName = QualifiedNameFinder.findShortestImportableQName(importedElement)
if (importedQName != null && importedQName.matchesPrefix(packageQName)) {
continue
}
}
if (unusedImport is PyStarImportElement || areAllImportsUnused(importStatement, unusedImports)) {
unusedStatements.add(importStatement)
result.add(importStatement)
}
else {
result.add(unusedImport)
}
}
}
return result
}
private fun areAllImportsUnused(importStatement: PyImportStatementBase, unusedImports: MutableSet<PyImportedNameDefiner>): Boolean {
val elements = importStatement.getImportElements()
for (element in elements) {
if (!unusedImports.contains(element)) {
return false
}
}
return true
}
fun optimizeImports() {
val elementsToDelete: List<PsiElement> = collectUnusedImportElements()
for (element in elementsToDelete) {
PyPsiUtils.assertValid(element)
element.delete()
}
}
}
companion object {
private val KEY: Key<Visitor> = Key.create<Visitor>("PyUnusedImportsInspection.Visitor")
}
}
@@ -9,19 +9,14 @@ import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiNamedElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.impl.source.resolve.FileContextUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.usageView.UsageInfo;
import com.intellij.util.ObjectUtils;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PyPsiBundle;
import com.jetbrains.python.codeInsight.PyPsiIndexUtil;
import com.jetbrains.python.codeInsight.imports.AddImportHelper;
import com.jetbrains.python.inspections.unresolvedReference.PyUnresolvedReferencesVisitor;
import com.jetbrains.python.inspections.unresolvedReference.SimplePyUnresolvedReferencesInspection;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PythonLanguageLevelPusher;
import com.jetbrains.python.psi.types.TypeEvalContext;
import com.jetbrains.python.refactoring.classes.PyClassRefactoringUtil;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -108,22 +103,7 @@ public class PyMakeFunctionFromMethodQuickFix implements LocalQuickFix {
private static void removeFormerImport(final @NotNull PsiFile usageFile, boolean addImport) {
if (usageFile instanceof PyFile && addImport) {
final PsiFile contextFile = FileContextUtil.getContextFile(usageFile);
final PsiFile file = ObjectUtils.chooseNotNull(contextFile, usageFile);
TypeEvalContext context = TypeEvalContext.codeAnalysis(file.getProject(), file);
final PyUnresolvedReferencesVisitor visitor = new SimplePyUnresolvedReferencesInspection.Visitor(
null, new SimplePyUnresolvedReferencesInspection(), context, PythonLanguageLevelPusher.getLanguageLevelForFile(usageFile)
);
usageFile.accept(new PyRecursiveElementVisitor() {
@Override
public void visitPyElement(@NotNull PyElement node) {
super.visitPyElement(node);
node.accept(visitor);
}
});
visitor.optimizeImports();
PyClassRefactoringUtil.optimizeImports(usageFile);
}
}
@@ -5,7 +5,6 @@ import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.util.Key;
import com.intellij.psi.PsiElementVisitor;
import com.jetbrains.python.codeInsight.PyCodeInsightSettings;
import com.jetbrains.python.inspections.PyInspection;
import org.intellij.lang.annotations.Pattern;
import org.jetbrains.annotations.NotNull;
@@ -36,15 +35,9 @@ public abstract class PyUnresolvedReferencesInspectionBase extends PyInspection
public final void inspectionFinished(@NotNull LocalInspectionToolSession session, @NotNull ProblemsHolder holder) {
final PyUnresolvedReferencesVisitor visitor = session.getUserData(KEY);
assert visitor != null;
ReadAction.run(
() -> {
visitor.addInstallAllImports();
if (PyCodeInsightSettings.getInstance().HIGHLIGHT_UNUSED_IMPORTS) {
visitor.highlightUnusedImports();
}
visitor.highlightImportsInsideGuards();
}
);
ReadAction.run(() -> {
visitor.addInstallAllImports();
});
session.putUserData(KEY, null);
}
@@ -3,17 +3,15 @@ package com.jetbrains.python.inspections.unresolvedReference;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.intellij.codeInsight.controlflow.ControlFlow;
import com.intellij.codeInsight.controlflow.ControlFlowUtil;
import com.intellij.codeInsight.controlflow.Instruction;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.lang.ASTNode;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.Version;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
@@ -24,29 +22,24 @@ import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PyCustomType;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PyPsiBundle;
import com.jetbrains.python.PythonRuntimeService;
import com.jetbrains.python.codeInsight.PyCustomMember;
import com.jetbrains.python.codeInsight.PySubstitutionChunkReference;
import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache;
import com.jetbrains.python.codeInsight.controlflow.PyDataFlow;
import com.jetbrains.python.codeInsight.controlflow.ReadWriteInstruction;
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
import com.jetbrains.python.codeInsight.imports.OptimizeImportsQuickFix;
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider;
import com.jetbrains.python.documentation.docstrings.DocStringParameterReference;
import com.jetbrains.python.documentation.docstrings.DocStringTypeReference;
import com.jetbrains.python.inspections.PyInspection;
import com.jetbrains.python.inspections.PyInspectionExtension;
import com.jetbrains.python.inspections.PyInspectionVisitor;
import com.jetbrains.python.inspections.quickfix.*;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.*;
import com.jetbrains.python.psi.impl.PyBuiltinCache;
import com.jetbrains.python.psi.impl.PyCallExpressionNavigator;
import com.jetbrains.python.psi.impl.PyImportStatementNavigator;
import com.jetbrains.python.psi.impl.PyImportedModule;
import com.jetbrains.python.psi.impl.references.PyFromImportNameReference;
import com.jetbrains.python.psi.impl.references.PyImportReference;
import com.jetbrains.python.psi.impl.references.PyOperatorReference;
import com.jetbrains.python.psi.impl.references.hasattr.PyHasAttrHelper;
import com.jetbrains.python.psi.resolve.ImportedResolveResult;
import com.jetbrains.python.psi.resolve.PyResolveContext;
import com.jetbrains.python.psi.resolve.QualifiedNameFinder;
import com.jetbrains.python.psi.types.*;
@@ -61,24 +54,17 @@ import static com.jetbrains.python.PyNames.END_WILDCARD;
import static com.jetbrains.python.psi.impl.stubs.PyVersionSpecificStubBaseKt.evaluateVersionsForElement;
public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor {
private final Set<PyImportedNameDefiner> myAllImports = Collections.synchronizedSet(new HashSet<>());
private final Set<PyImportedNameDefiner> myImportsInsideGuard = Collections.synchronizedSet(new HashSet<>());
private final Set<PyImportedNameDefiner> myUsedImports = Collections.synchronizedSet(new HashSet<>());
private final Set<PyImportedNameDefiner> myUnresolvedImports = Collections.synchronizedSet(new HashSet<>());
private final ImmutableSet<String> myIgnoredIdentifiers;
private final PyInspection myInspection;
private final Version myVersion;
private volatile Boolean myIsEnabled = null;
protected final List<PyPackageInstallAllProblemInfo> myUnresolvedRefs = Collections.synchronizedList(new ArrayList<>());
protected PyUnresolvedReferencesVisitor(@Nullable ProblemsHolder holder,
@NotNull List<String> ignoredIdentifiers,
@NotNull PyInspection inspection,
@NotNull TypeEvalContext context,
@NotNull LanguageLevel languageLevel) {
super(holder, context);
myIgnoredIdentifiers = ImmutableSet.copyOf(ignoredIdentifiers);
myInspection = inspection;
myVersion = new Version(languageLevel.getMajorVersion(), languageLevel.getMinorVersion(), 0);
}
@@ -100,93 +86,26 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor
}
}
@Override
public void visitPyImportElement(@NotNull PyImportElement node) {
super.visitPyImportElement(node);
final PyFromImportStatement fromImport = PsiTreeUtil.getParentOfType(node, PyFromImportStatement.class);
if (isEnabled(node) && (fromImport == null || !fromImport.isFromFuture())) {
myAllImports.add(node);
}
}
@Override
public void visitPyStarImportElement(@NotNull PyStarImportElement node) {
super.visitPyStarImportElement(node);
if (isEnabled(node)) {
myAllImports.add(node);
}
}
@Override
public void visitComment(@NotNull PsiComment comment) {
super.visitComment(comment);
if (comment instanceof PsiLanguageInjectionHost) {
processInjection((PsiLanguageInjectionHost)comment);
}
}
@Override
public void visitPyElement(final @NotNull PyElement node) {
super.visitPyElement(node);
final PsiFile file = node.getContainingFile();
final InjectedLanguageManager injectedLanguageManager = InjectedLanguageManager.getInstance(node.getProject());
if (injectedLanguageManager.isInjectedFragment(file)) {
final PsiLanguageInjectionHost host = injectedLanguageManager.getInjectionHost(node);
processInjection(host);
}
if (node instanceof PyReferenceOwner) {
if (node instanceof PyReferenceOwner referenceOwner) {
final PyResolveContext resolveContext = PyResolveContext.defaultContext(myTypeEvalContext);
processReference(node, ((PyReferenceOwner)node).getReference(resolveContext));
processReference(node, referenceOwner.getReference(resolveContext));
}
else {
if (node instanceof PsiLanguageInjectionHost) {
processInjection((PsiLanguageInjectionHost)node);
}
for (final PsiReference reference : node.getReferences()) {
processReference(node, reference);
}
}
}
private void processInjection(@Nullable PsiLanguageInjectionHost node) {
if (node == null) return;
final List<Pair<PsiElement, TextRange>> files = InjectedLanguageManager.getInstance(node.getProject()).getInjectedPsiFiles(node);
if (files != null) {
for (Pair<PsiElement, TextRange> pair : files) {
new PyRecursiveElementVisitor() {
@Override
public void visitPyElement(@NotNull PyElement element) {
super.visitPyElement(element);
if (element instanceof PyReferenceOwner) {
final PyResolveContext resolveContext = PyResolveContext.defaultContext(myTypeEvalContext);
final PsiPolyVariantReference reference = ((PyReferenceOwner)element).getReference(resolveContext);
markTargetImportsAsUsed(reference);
}
}
}.visitElement(pair.getFirst());
}
}
}
private void markTargetImportsAsUsed(@NotNull PsiPolyVariantReference reference) {
final ResolveResult[] resolveResults = reference.multiResolve(false);
for (ResolveResult resolveResult : resolveResults) {
if (resolveResult instanceof ImportedResolveResult) {
final PyImportedNameDefiner definer = ((ImportedResolveResult)resolveResult).getDefiner();
if (definer != null) {
myUsedImports.add(definer);
}
}
}
}
private void processReference(@NotNull PyElement node, @Nullable PsiReference reference) {
if (!isEnabled(node) || reference == null || reference.isSoft()) {
return;
}
final PyExceptPart guard = getImportErrorGuard(node);
if (guard != null) {
processReferenceInImportGuard(node, guard);
return;
}
if (node instanceof PyQualifiedExpression qExpr) {
@@ -205,12 +124,6 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor
if (target == null && resolveResult.isValidResult()) {
target = resolveResult.getElement();
}
if (resolveResult instanceof ImportedResolveResult) {
final PyImportedNameDefiner definer = ((ImportedResolveResult)resolveResult).getDefiner();
if (definer != null) {
myUsedImports.add(definer);
}
}
}
}
else {
@@ -226,10 +139,6 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor
if (severity == null) return;
registerUnresolvedReferenceProblem(node, reference, severity);
}
// don't highlight unresolved imports as unused
if (node.getParent() instanceof PyImportElement importElement) {
myUnresolvedImports.add(importElement);
}
}
else if (reference instanceof PyImportReference &&
target == reference.getElement().getContainingFile() &&
@@ -238,21 +147,6 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor
}
}
private void processReferenceInImportGuard(@NotNull PyElement node, @NotNull PyExceptPart guard) {
final PyImportElement importElement = PsiTreeUtil.getParentOfType(node, PyImportElement.class);
if (importElement != null) {
final String visibleName = importElement.getVisibleName();
final ScopeOwner owner = ScopeUtil.getScopeOwner(importElement);
if (visibleName != null && owner != null) {
final Collection<PsiElement> allWrites = ScopeUtil.getElementsOfAccessType(visibleName, owner, ReadWriteInstruction.ACCESS.WRITE);
final boolean hasWriteInsideGuard = ContainerUtil.exists(allWrites, e -> PsiTreeUtil.isAncestor(guard, e, false));
if (!hasWriteInsideGuard && !shouldSkipMissingWriteInsideGuard(guard, visibleName)) {
myImportsInsideGuard.add(importElement);
}
}
}
}
private void registerUnresolvedReferenceProblem(@NotNull PyElement node, final @NotNull PsiReference reference,
@NotNull HighlightSeverity severity) {
if (reference instanceof DocStringTypeReference) {
@@ -550,172 +444,6 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor
}
}
public void highlightUnusedImports() {
final List<PyInspectionExtension> extensions = PyInspectionExtension.EP_NAME.getExtensionList();
final List<PsiElement> unused = collectUnusedImportElements();
for (PsiElement element : unused) {
if (ContainerUtil.exists(extensions, extension -> extension.ignoreUnused(element, myTypeEvalContext))) {
continue;
}
if (!evaluateVersionsForElement(element).contains(myVersion)) {
continue;
}
if (element.getTextLength() > 0) {
OptimizeImportsQuickFix fix = new OptimizeImportsQuickFix();
registerProblem(element, PyPsiBundle.message("INSP.unused.import.statement"), ProblemHighlightType.LIKE_UNUSED_SYMBOL, null, fix);
}
}
}
public void highlightImportsInsideGuards() {
HashSet<PyImportedNameDefiner> usedImportsInsideImportGuards = Sets.newHashSet(getImportsInsideGuard());
usedImportsInsideImportGuards.retainAll(getUsedImports());
for (PyImportedNameDefiner definer : usedImportsInsideImportGuards) {
PyImportElement importElement = PyUtil.as(definer, PyImportElement.class);
if (importElement == null) {
continue;
}
final PyTargetExpression asElement = importElement.getAsNameElement();
final PyElement toHighlight = asElement != null ? asElement : importElement.getImportReferenceExpression();
registerProblem(toHighlight,
PyPsiBundle.message("INSP.try.except.import.error",
importElement.getVisibleName()),
ProblemHighlightType.LIKE_UNKNOWN_SYMBOL);
}
}
public void optimizeImports() {
final List<PsiElement> elementsToDelete = collectUnusedImportElements();
for (PsiElement element : elementsToDelete) {
PyPsiUtils.assertValid(element);
element.delete();
}
}
protected List<PsiElement> collectUnusedImportElements() {
if (getAllImports().isEmpty()) {
return Collections.emptyList();
}
// PY-1315 Unused imports inspection shouldn't work in python REPL console
final PyImportedNameDefiner first = getAllImports().iterator().next();
if (first.getContainingFile() instanceof PyExpressionCodeFragment || PythonRuntimeService.getInstance().isInPydevConsole(first)) {
return Collections.emptyList();
}
List<PsiElement> result = new ArrayList<>();
Set<PyImportedNameDefiner> unusedImports = new HashSet<>(getAllImports());
unusedImports.removeAll(getUsedImports());
unusedImports.removeAll(myUnresolvedImports);
// Remove those unsed, that are reported to be skipped by extension points
final Set<PyImportedNameDefiner> unusedImportToSkip = new HashSet<>();
for (final PyImportedNameDefiner unusedImport : unusedImports) {
if (ContainerUtil.exists(PyInspectionExtension.EP_NAME.getExtensionList(), o -> o.ignoreUnusedImports(unusedImport))) {
unusedImportToSkip.add(unusedImport);
}
}
unusedImports.removeAll(unusedImportToSkip);
Set<String> usedImportNames = new HashSet<>();
for (PyImportedNameDefiner usedImport : getUsedImports()) {
for (PyElement e : usedImport.iterateNames()) {
usedImportNames.add(e.getName());
}
}
Set<PyImportStatementBase> unusedStatements = new HashSet<>();
QualifiedName packageQName = null;
List<String> dunderAll = null;
// TODO: Use strategies instead of pack of "continue"
iterUnused:
for (PyImportedNameDefiner unusedImport : unusedImports) {
if (packageQName == null) {
final PsiFile file = unusedImport.getContainingFile();
if (file instanceof PyFile) {
dunderAll = ((PyFile)file).getDunderAll();
}
if (file != null && PyUtil.isPackage(file)) {
packageQName = QualifiedNameFinder.findShortestImportableQName(file);
}
}
PyImportStatementBase importStatement = PsiTreeUtil.getParentOfType(unusedImport, PyImportStatementBase.class);
if (importStatement != null && !unusedStatements.contains(importStatement) && !getUsedImports().contains(unusedImport)) {
PyInspection inspection = myInspection;
assert inspection != null;
if (inspection.isSuppressedFor(importStatement)) {
continue;
}
// don't remove as unused imports in try/except statements
if (PsiTreeUtil.getParentOfType(importStatement, PyTryExceptStatement.class) != null) {
continue;
}
// Don't report conditional imports as unused
if (PsiTreeUtil.getParentOfType(unusedImport, PyIfStatement.class) != null) {
for (PyElement e : unusedImport.iterateNames()) {
if (usedImportNames.contains(e.getName())) {
continue iterUnused;
}
}
}
PsiFileSystemItem importedElement;
if (unusedImport instanceof PyImportElement importElement) {
final PsiElement element = importElement.resolve();
if (element == null) {
if (importElement.getImportedQName() != null) {
//Mark import as unused even if it can't be resolved
if (areAllImportsUnused(importStatement, unusedImports)) {
result.add(importStatement);
}
else {
result.add(importElement);
}
}
continue;
}
if (dunderAll != null && dunderAll.contains(importElement.getVisibleName())) {
continue;
}
importedElement = element.getContainingFile();
}
else {
assert importStatement instanceof PyFromImportStatement;
importedElement = ((PyFromImportStatement)importStatement).resolveImportSource();
if (importedElement == null) {
continue;
}
}
if (packageQName != null && importedElement != null) {
final QualifiedName importedQName = QualifiedNameFinder.findShortestImportableQName(importedElement);
if (importedQName != null && importedQName.matchesPrefix(packageQName)) {
continue;
}
}
if (unusedImport instanceof PyStarImportElement || areAllImportsUnused(importStatement, unusedImports)) {
unusedStatements.add(importStatement);
result.add(importStatement);
}
else {
result.add(unusedImport);
}
}
}
return result;
}
private static boolean areAllImportsUnused(PyImportStatementBase importStatement, Set<PyImportedNameDefiner> unusedImports) {
final PyImportElement[] elements = importStatement.getImportElements();
for (PyImportElement element : elements) {
if (!unusedImports.contains(element)) {
return false;
}
}
return true;
}
private boolean ignoreUnresolved(@NotNull PyElement node, @NotNull PsiReference reference) {
boolean ignoreUnresolved = false;
for (PyInspectionExtension extension : PyInspectionExtension.EP_NAME.getExtensionList()) {
@@ -727,18 +455,6 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor
return ignoreUnresolved;
}
Collection<PyImportedNameDefiner> getImportsInsideGuard() {
return Collections.unmodifiableCollection(myImportsInsideGuard);
}
Collection<PyImportedNameDefiner> getAllImports() {
return Collections.unmodifiableCollection(myAllImports);
}
Collection<PyImportedNameDefiner> getUsedImports() {
return Collections.unmodifiableCollection(myUsedImports);
}
private static @Nullable PyExceptPart getImportErrorGuard(PyElement node) {
final PyImportStatementBase importStatement = PsiTreeUtil.getParentOfType(node, PyImportStatementBase.class);
if (importStatement != null) {
@@ -762,35 +478,6 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor
return PyImportStatementNavigator.getImportStatementByElement(node) == null && target.getName().equals(PyNames.INIT_DOT_PY);
}
private static boolean shouldSkipMissingWriteInsideGuard(@NotNull PyExceptPart guard, @NotNull String name) {
return isDefinedInParentScope(name, guard) ||
PyBuiltinCache.getInstance(guard).getByName(name) != null ||
controlFlowAlwaysTerminatesInsideGuard(guard);
}
private static boolean isDefinedInParentScope(@NotNull String name, @NotNull PsiElement anchor) {
return ScopeUtil.getDeclarationScopeOwner(ScopeUtil.getScopeOwner(anchor), name) != null;
}
private static boolean controlFlowAlwaysTerminatesInsideGuard(@NotNull PyExceptPart guard) {
final ScopeOwner owner = ScopeUtil.getScopeOwner(guard);
if (owner == null) return false;
final ControlFlow flow = ControlFlowCache.getControlFlow(owner);
final Instruction[] instructions = flow.getInstructions();
final int start = ControlFlowUtil.findInstructionNumberByElement(instructions, guard.getExceptClass());
if (start <= 0) return false;
final Ref<Boolean> canEscapeGuard = Ref.create(false);
ControlFlowUtil.process(instructions, start, instruction -> {
final PsiElement e = instruction.getElement();
if (e != null && !PsiTreeUtil.isAncestor(guard, e, true)) {
canEscapeGuard.set(true);
return false;
}
return true;
});
return !canEscapeGuard.get();
}
private static @Nullable PyExpression getReferenceQualifier(@NotNull PsiReference reference) {
final PsiElement element = reference.getElement();
@@ -30,7 +30,7 @@ public class SimplePyUnresolvedReferencesInspection extends PyUnresolvedReferenc
@NotNull PyInspection inspection,
@NotNull TypeEvalContext context,
@NotNull LanguageLevel languageLevel) {
super(holder, Collections.emptyList(), inspection, context, languageLevel);
super(holder, Collections.emptyList(), context, languageLevel);
}
}
}
@@ -26,7 +26,6 @@ import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil
import com.jetbrains.python.codeInsight.imports.AutoImportHintAction
import com.jetbrains.python.codeInsight.imports.AutoImportQuickFix
import com.jetbrains.python.codeInsight.imports.PythonImportUtils
import com.jetbrains.python.inspections.PyInspection
import com.jetbrains.python.inspections.PyInspectionVisitor
import com.jetbrains.python.inspections.PyUnresolvedReferenceQuickFixProvider
import com.jetbrains.python.inspections.quickfix.*
@@ -50,7 +49,6 @@ class PyUnresolvedReferencesInspection : PyUnresolvedReferencesInspectionBase()
override fun createVisitor(holder: ProblemsHolder, session: LocalInspectionToolSession): PyUnresolvedReferencesVisitor =
Visitor(holder,
ignoredIdentifiers,
this,
PyInspectionVisitor.getContext(session),
PythonLanguageLevelPusher.getLanguageLevelForFile(session.file))
@@ -61,10 +59,9 @@ class PyUnresolvedReferencesInspection : PyUnresolvedReferencesInspectionBase()
private class Visitor(
holder: ProblemsHolder,
ignoredIdentifiers: List<String>,
inspection: PyInspection,
context: TypeEvalContext,
languageLevel: LanguageLevel,
) : PyUnresolvedReferencesVisitor(holder, ignoredIdentifiers, inspection, context, languageLevel) {
) : PyUnresolvedReferencesVisitor(holder, ignoredIdentifiers, context, languageLevel) {
override fun getInstallPackageQuickFixes(
node: PyElement,
@@ -1,8 +1,8 @@
import mod_a
import mod_Z
# noinspection PyUnresolvedReferences
# noinspection PyUnusedImports
import pkg.pkg_a
# noinspection PyUnresolvedReferences
# noinspection PyUnusedImports
import pkg.pkg_Z
from mod import var_a
from mod import var_Z
@@ -1,8 +1,8 @@
import mod_a
import mod_Z
# noinspection PyUnresolvedReferences
# noinspection PyUnusedImports
import pkg.pkg_a
# noinspection PyUnresolvedReferences
# noinspection PyUnusedImports
import pkg.pkg_Z
from mod import var_a
from mod import var_Z
@@ -1,2 +1,2 @@
#noinspection PyUnresolvedReferences
#noinspection PyUnusedImports
import sys
@@ -1,2 +1,2 @@
#noinspection PyUnresolvedReferences
#noinspection PyUnusedImports
import sys
@@ -932,4 +932,10 @@ public class PyUnresolvedReferencesInspectionTest extends PyInspectionTestCase {
protected Class<? extends PyInspection> getInspectionClass() {
return PyUnresolvedReferencesInspection.class;
}
@Override
protected void configureInspection() {
myFixture.enableInspections(PyUnresolvedReferencesInspection.class, PyUnusedImportsInspection.class);
myFixture.checkHighlighting(isWarning(), isInfo(), isWeakWarning());
}
}
@@ -48,7 +48,7 @@ public class PyUnusedImportTest extends PyTestCase {
private void doTest(@NotNull String filename) {
myFixture.copyDirectoryToProject("inspections/unusedImport/" + getTestName(true), "");
myFixture.enableInspections(PyUnresolvedReferencesInspection.class);
myFixture.enableInspections(PyUnusedImportsInspection.class, PyUnresolvedReferencesInspection.class);
myFixture.testHighlighting(true, false, false, filename);
}
}
@@ -134,7 +134,7 @@ public class PyiInspectionsTest extends PyTestCase {
}
public void testPyiUnusedImports() {
doPyiTest(PyUnresolvedReferencesInspection.class);
doPyiTest(PyUnusedImportsInspection.class);
}
public void testPyiRelativeImports() {
@@ -30,6 +30,7 @@ import com.jetbrains.python.codeInsight.imports.AutoImportQuickFix;
import com.jetbrains.python.codeInsight.imports.ImportCandidateHolder;
import com.jetbrains.python.codeInsight.imports.PythonImportUtils;
import com.jetbrains.python.formatter.PyCodeStyleSettings;
import com.jetbrains.python.inspections.PyUnusedImportsInspection;
import com.jetbrains.python.inspections.unresolvedReference.PyUnresolvedReferencesInspection;
import com.jetbrains.python.psi.LanguageLevel;
import com.jetbrains.python.psi.PyReferenceExpression;
@@ -460,7 +461,7 @@ public class PyAddImportQuickFixTest extends PyQuickFixTestCase {
private void configureMultiFileProject() {
myFixture.copyDirectoryToProject(getTestName(true), "");
myFixture.enableInspections(PyUnresolvedReferencesInspection.class);
myFixture.enableInspections(PyUnresolvedReferencesInspection.class, PyUnusedImportsInspection.class);
myFixture.configureByFile("main.py");
myFixture.checkHighlighting(true, false, false);
}