From b8f41c5b840d4935a81119cf6ecf5e98ce5f577f Mon Sep 17 00:00:00 2001 From: Semyon Proshev Date: Fri, 27 Apr 2018 17:07:44 +0300 Subject: [PATCH] PyReferenceExpression.multiFollowAssignmentsChain has an ability to not follow some references (PY-28243) Update PyTypeHintsInspection to use it. --- .../python/psi/PyReferenceExpression.java | 20 ++- .../typing/PyTypingTypeProvider.java | 2 +- .../inspections/PyTypeHintsInspection.kt | 118 +++++++++--------- .../psi/impl/PyReferenceExpressionImpl.java | 6 +- .../python/psi/resolve/PyResolveUtil.java | 11 +- 5 files changed, 85 insertions(+), 72 deletions(-) diff --git a/python/psi-api/src/com/jetbrains/python/psi/PyReferenceExpression.java b/python/psi-api/src/com/jetbrains/python/psi/PyReferenceExpression.java index 7144acc48f3a..7e605a62c827 100644 --- a/python/psi-api/src/com/jetbrains/python/psi/PyReferenceExpression.java +++ b/python/psi-api/src/com/jetbrains/python/psi/PyReferenceExpression.java @@ -15,6 +15,7 @@ */ package com.jetbrains.python.psi; +import com.google.common.base.Predicates; import com.intellij.psi.PsiPolyVariantReference; import com.jetbrains.python.psi.resolve.PyResolveContext; import com.jetbrains.python.psi.resolve.QualifiedRatedResolveResult; @@ -22,6 +23,7 @@ import com.jetbrains.python.psi.resolve.QualifiedResolveResult; import org.jetbrains.annotations.NotNull; import java.util.List; +import java.util.function.Predicate; public interface PyReferenceExpression extends PyQualifiedExpression, PyReferenceOwner { PyReferenceExpression[] EMPTY_ARRAY = new PyReferenceExpression[0]; @@ -48,8 +50,24 @@ public interface PyReferenceExpression extends PyQualifiedExpression, PyReferenc * Note: the returned list does not contain null values. */ @NotNull - List multiFollowAssignmentsChain(@NotNull PyResolveContext resolveContext); + default List multiFollowAssignmentsChain(@NotNull PyResolveContext resolveContext) { + return multiFollowAssignmentsChain(resolveContext, Predicates.alwaysTrue()); + } + /** + * Goes through a chain of assignment statements until a non-assignment expression is encountered. + * Starts at this, expecting it to resolve to a target of an assignment. + * + * @param resolveContext resolve context + * @param follow predicate to test if target should be followed + * @return the values that could be assigned to this element via a chain of assignments, or an empty list. + * Note: the returned list does not contain null values. + */ + @NotNull + List multiFollowAssignmentsChain(@NotNull PyResolveContext resolveContext, + @NotNull Predicate follow); + + @Override @NotNull PsiPolyVariantReference getReference(); } diff --git a/python/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java b/python/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java index 55beb7b33cc4..e24960023cfa 100644 --- a/python/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java +++ b/python/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java @@ -129,7 +129,7 @@ public class PyTypingTypeProvider extends PyTypeProviderBase { * since they are not type aliases already and in typing.pyi are assigned to * some synthetic values. */ - private static final ImmutableSet OPAQUE_NAMES = ImmutableSet.builder() + public static final ImmutableSet OPAQUE_NAMES = ImmutableSet.builder() .add(PyKnownDecoratorUtil.KnownDecorator.TYPING_OVERLOAD.name()) .add(ANY) .add(TYPE_VAR) diff --git a/python/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt b/python/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt index 7092b2cc5582..5ab143093506 100644 --- a/python/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt +++ b/python/src/com/jetbrains/python/inspections/PyTypeHintsInspection.kt @@ -17,7 +17,6 @@ import com.jetbrains.python.psi.* import com.jetbrains.python.psi.impl.PyEvaluator import com.jetbrains.python.psi.resolve.PyResolveContext import com.jetbrains.python.psi.resolve.PyResolveUtil -import com.jetbrains.python.psi.resolve.RatedResolveResult import com.jetbrains.python.psi.types.PyGenericType import com.jetbrains.python.psi.types.PyTypeChecker @@ -188,29 +187,30 @@ class PyTypeHintsInspection : PyInspection() { private fun checkInstanceAndClassChecksOnReference(base: PyExpression) { if (base is PyReferenceExpression) { - val resolvedBase = PyResolveUtil.fullMultiResolveLocally(base) + val resolvedBase = multiFollowAssignmentsChain(base) resolvedBase .asSequence() - .filterIsInstance() - .mapNotNull { PyResolveUtil.getImportedElementQName(it) } + .filterIsInstance() + .mapNotNull { it.qualifiedName } .forEach { - when (it.toString()) { + when (it) { PyTypingTypeProvider.ANY, PyTypingTypeProvider.UNION, PyTypingTypeProvider.GENERIC, PyTypingTypeProvider.OPTIONAL, PyTypingTypeProvider.CLASS_VAR, - PyTypingTypeProvider.NO_RETURN -> registerProblem(base, - "'${it.lastComponent}' cannot be used with instance and class checks", - ProblemHighlightType.GENERIC_ERROR) + PyTypingTypeProvider.NO_RETURN -> + registerProblem(base, + "'${it.substringAfterLast('.')}' cannot be used with instance and class checks", + ProblemHighlightType.GENERIC_ERROR) } } resolvedBase .asSequence() - .filterIsInstance() - .mapNotNull { it.findAssignedValue() as? PySubscriptionExpression } + .filterIsInstance() + .filter { myTypeEvalContext.maySwitchToAST(it) } .forEach { checkInstanceAndClassChecksOnSubscriptionOperand(base, it.operand) } } } @@ -223,63 +223,47 @@ class PyTypeHintsInspection : PyInspection() { private fun checkInstanceAndClassChecksOnSubscriptionOperand(base: PyExpression, operand: PyExpression) { if (operand is PyReferenceExpression) { - PyResolveUtil - .fullMultiResolveLocally(operand) + multiFollowAssignmentsChain(operand) .forEach { - if (it == null) return@forEach + if (it is PyQualifiedNameOwner) { + val qName = it.qualifiedName - if (it is PyImportElement) { - val qName = PyResolveUtil.getImportedElementQName(it) + when (qName) { + PyTypingTypeProvider.GENERIC -> { + registerProblem(base, "'Generic' cannot be used with instance and class checks", ProblemHighlightType.GENERIC_ERROR) + return@forEach + } - if (qName == genericQName) { - registerProblem(base, - "'Generic' cannot be used with instance and class checks", - ProblemHighlightType.GENERIC_ERROR) - - return@forEach - } - - if (qName != null) { - val qNameAsString = qName.toString() - - if (qNameAsString == PyTypingTypeProvider.UNION || - qNameAsString == PyTypingTypeProvider.OPTIONAL || - qNameAsString == PyTypingTypeProvider.CLASS_VAR) { + PyTypingTypeProvider.UNION, + PyTypingTypeProvider.OPTIONAL, + PyTypingTypeProvider.CLASS_VAR -> { registerProblem(base, - "'${qName.lastComponent}' cannot be used with instance and class checks", + "'${qName.substringAfterLast('.')}' cannot be used with instance and class checks", ProblemHighlightType.GENERIC_ERROR) + return@forEach + } + PyTypingTypeProvider.CALLABLE, + PyTypingTypeProvider.TYPE, + PyTypingTypeProvider.PROTOCOL, + PyTypingTypeProvider.PROTOCOL_EXT -> { + registerProblem(base, + "Parameterized generics cannot be used with instance and class checks", + ProblemHighlightType.GENERIC_ERROR) return@forEach } } } - val resolved = if (it is PyImportElement) it.multiResolve() else listOf(RatedResolveResult(RatedResolveResult.RATE_NORMAL, it)) + if (it is PyTypedElement) { + val type = myTypeEvalContext.getType(it) - resolved - .asSequence() - .filterNotNull() - .mapNotNull { it.element as? PyTypedElement } - .any { - if (it is PyQualifiedNameOwner) { - val qName = it.qualifiedName - - if (qName == PyTypingTypeProvider.CALLABLE || - qName == PyTypingTypeProvider.TYPE || - qName == PyTypingTypeProvider.PROTOCOL || - qName == PyTypingTypeProvider.PROTOCOL_EXT) return@any true - } - - val type = myTypeEvalContext.getType(it) - type is PyWithAncestors && PyTypingTypeProvider.isGeneric(type, myTypeEvalContext) - } - .let { - if (it) { - registerProblem(base, - "Parameterized generics cannot be used with instance and class checks", - ProblemHighlightType.GENERIC_ERROR) - } + if (type is PyWithAncestors && PyTypingTypeProvider.isGeneric(type, myTypeEvalContext)) { + registerProblem(base, + "Parameterized generics cannot be used with instance and class checks", + ProblemHighlightType.GENERIC_ERROR) } + } } } } @@ -296,12 +280,12 @@ class PyTypeHintsInspection : PyInspection() { superClassExpressions .asSequence() .filter { - val resolved = if (it is PyReferenceExpression) PyResolveUtil.fullMultiResolveLocally(it) else listOf(it) + val resolved = if (it is PyReferenceExpression) multiFollowAssignmentsChain(it) else listOf(it) resolved .asSequence() - .map { if (it is PyTargetExpression) it.findAssignedValue() else it } .filterIsInstance() + .filter { myTypeEvalContext.maySwitchToAST(it) } .mapNotNull { it.operand as? PyReferenceExpression } .any { genericQName in PyResolveUtil.resolveImportedElementQNameLocally(it) } } @@ -346,7 +330,7 @@ class PyTypeHintsInspection : PyInspection() { private fun collectGenerics(superClassExpression: PyExpression): Pair?, Set> { val resolvedSuperClass = - if (superClassExpression is PyReferenceExpression) PyResolveUtil.fullMultiResolveLocally(superClassExpression) + if (superClassExpression is PyReferenceExpression) multiFollowAssignmentsChain(superClassExpression) else listOf(superClassExpression) var seenGeneric = false @@ -355,8 +339,8 @@ class PyTypeHintsInspection : PyInspection() { resolvedSuperClass .asSequence() - .map { if (it is PyTargetExpression) it.findAssignedValue() else it } .filterIsInstance() + .filter { myTypeEvalContext.maySwitchToAST(it) } .forEach { val operand = it.operand val generic = @@ -368,7 +352,7 @@ class PyTypeHintsInspection : PyInspection() { val superClassTypeVars = parameters .asSequence() .filterIsInstance() - .map { PyResolveUtil.fullMultiResolveLocally(it).toSet() } + .map { multiFollowAssignmentsChain(it, this::followNotTypeVar).toSet() } .fold(emptySet(), { acc, typeVars -> acc.union(typeVars) }) if (generic) genericTypeVars.addAll(superClassTypeVars) else nonGenericTypeVars.addAll(superClassTypeVars) @@ -391,7 +375,7 @@ class PyTypeHintsInspection : PyInspection() { if (type != null) { if (type is PyGenericType) { - if (!typeVars.addAll(PyResolveUtil.fullMultiResolveLocally(it))) { + if (!typeVars.addAll(multiFollowAssignmentsChain(it))) { registerProblem(it, "Parameters to 'Generic[...]' must all be unique", ProblemHighlightType.GENERIC_ERROR) } } @@ -402,5 +386,19 @@ class PyTypeHintsInspection : PyInspection() { } } } + + private fun followNotTypingOpaque(target: PyTargetExpression): Boolean { + return !PyTypingTypeProvider.OPAQUE_NAMES.contains(target.qualifiedName) + } + + private fun followNotTypeVar(target: PyTargetExpression): Boolean { + return !myTypeEvalContext.maySwitchToAST(target) || target.findAssignedValue() !is PyCallExpression + } + + private fun multiFollowAssignmentsChain(referenceExpression: PyReferenceExpression, + follow: (PyTargetExpression) -> Boolean = this::followNotTypingOpaque): List { + val resolveContext = PyResolveContext.noImplicits().withTypeEvalContext(myTypeEvalContext) + return referenceExpression.multiFollowAssignmentsChain(resolveContext, follow).mapNotNull { it.element } + } } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/psi/impl/PyReferenceExpressionImpl.java b/python/src/com/jetbrains/python/psi/impl/PyReferenceExpressionImpl.java index 2c707d367287..baf45f2200ee 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyReferenceExpressionImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyReferenceExpressionImpl.java @@ -47,6 +47,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; +import java.util.function.Predicate; import static com.jetbrains.python.psi.PyUtil.as; @@ -155,7 +156,8 @@ public class PyReferenceExpressionImpl extends PyElementImpl implements PyRefere @NotNull @Override - public List multiFollowAssignmentsChain(@NotNull PyResolveContext resolveContext) { + public List multiFollowAssignmentsChain(@NotNull PyResolveContext resolveContext, + @NotNull Predicate follow) { final List result = new ArrayList<>(); final Queue queue = new LinkedList<>(); final Set visited = new HashSet<>(); @@ -169,7 +171,7 @@ public class PyReferenceExpressionImpl extends PyElementImpl implements PyRefere for (ResolveResult resolveResult : node.myReferenceExpression.getReference(resolveContext).multiResolve(false)) { final PsiElement element = resolveResult.getElement(); - if (element instanceof PyTargetExpression) { + if (element instanceof PyTargetExpression && follow.test((PyTargetExpression)element)) { final PyTargetExpression target = (PyTargetExpression)element; final List assignedFromElements = context.maySwitchToAST(target) diff --git a/python/src/com/jetbrains/python/psi/resolve/PyResolveUtil.java b/python/src/com/jetbrains/python/psi/resolve/PyResolveUtil.java index 464a834c73c3..8acebf5d7acf 100644 --- a/python/src/com/jetbrains/python/psi/resolve/PyResolveUtil.java +++ b/python/src/com/jetbrains/python/psi/resolve/PyResolveUtil.java @@ -163,7 +163,7 @@ public class PyResolveUtil { } else { return StreamEx - .of(fullMultiResolveLocally(expression)) + .of(fullMultiResolveLocally(expression, new HashSet<>())) .select(PyImportElement.class) .map(PyResolveUtil::getImportedElementQName) .nonNull() @@ -172,7 +172,7 @@ public class PyResolveUtil { } @Nullable - public static QualifiedName getImportedElementQName(@NotNull PyImportElement element) { + private static QualifiedName getImportedElementQName(@NotNull PyImportElement element) { final PyStatement importStatement = element.getContainingImportStatement(); if (importStatement instanceof PyFromImportStatement) { @@ -264,7 +264,6 @@ public class PyResolveUtil { * * @param referenceExpression expression to resolve * @return resolved assigned value. - * @see PyResolveUtil#fullMultiResolveLocally(PyReferenceExpression) */ @Nullable public static PyExpression fullResolveLocally(@NotNull PyReferenceExpression referenceExpression) { @@ -293,14 +292,10 @@ public class PyResolveUtil { * Runs DFS on assignment chains and returns all reached assigned values. * * @param referenceExpression expression to resolve + * @param visited set to store visited references to prevent recursion * @return resolved assigned values. * Note: the returned list could contain null values. */ - @NotNull - public static List fullMultiResolveLocally(@NotNull PyReferenceExpression referenceExpression) { - return fullMultiResolveLocally(referenceExpression, new HashSet<>()); - } - @NotNull private static List fullMultiResolveLocally(@NotNull PyReferenceExpression referenceExpression, @NotNull Set visited) {