PyReferenceExpression.multiFollowAssignmentsChain has an ability to not follow some references (PY-28243)

Update PyTypeHintsInspection to use it.
This commit is contained in:
Semyon Proshev
2018-04-27 19:57:17 +03:00
parent 3d233e9f52
commit b8f41c5b84
5 changed files with 85 additions and 72 deletions
@@ -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
* <i>Note: the returned list does not contain null values.</i>
*/
@NotNull
List<QualifiedRatedResolveResult> multiFollowAssignmentsChain(@NotNull PyResolveContext resolveContext);
default List<QualifiedRatedResolveResult> 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.
* <i>Note: the returned list does not contain null values.</i>
*/
@NotNull
List<QualifiedRatedResolveResult> multiFollowAssignmentsChain(@NotNull PyResolveContext resolveContext,
@NotNull Predicate<? super PyTargetExpression> follow);
@Override
@NotNull
PsiPolyVariantReference getReference();
}
@@ -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<String> OPAQUE_NAMES = ImmutableSet.<String>builder()
public static final ImmutableSet<String> OPAQUE_NAMES = ImmutableSet.<String>builder()
.add(PyKnownDecoratorUtil.KnownDecorator.TYPING_OVERLOAD.name())
.add(ANY)
.add(TYPE_VAR)
@@ -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<PyImportElement>()
.mapNotNull { PyResolveUtil.getImportedElementQName(it) }
.filterIsInstance<PyQualifiedNameOwner>()
.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<PyTargetExpression>()
.mapNotNull { it.findAssignedValue() as? PySubscriptionExpression }
.filterIsInstance<PySubscriptionExpression>()
.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<PySubscriptionExpression>()
.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<PsiElement>?, Set<PsiElement>> {
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<PySubscriptionExpression>()
.filter { myTypeEvalContext.maySwitchToAST(it) }
.forEach {
val operand = it.operand
val generic =
@@ -368,7 +352,7 @@ class PyTypeHintsInspection : PyInspection() {
val superClassTypeVars = parameters
.asSequence()
.filterIsInstance<PyReferenceExpression>()
.map { PyResolveUtil.fullMultiResolveLocally(it).toSet() }
.map { multiFollowAssignmentsChain(it, this::followNotTypeVar).toSet() }
.fold(emptySet<PsiElement>(), { 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<PsiElement> {
val resolveContext = PyResolveContext.noImplicits().withTypeEvalContext(myTypeEvalContext)
return referenceExpression.multiFollowAssignmentsChain(resolveContext, follow).mapNotNull { it.element }
}
}
}
@@ -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<QualifiedRatedResolveResult> multiFollowAssignmentsChain(@NotNull PyResolveContext resolveContext) {
public List<QualifiedRatedResolveResult> multiFollowAssignmentsChain(@NotNull PyResolveContext resolveContext,
@NotNull Predicate<? super PyTargetExpression> follow) {
final List<QualifiedRatedResolveResult> result = new ArrayList<>();
final Queue<MultiFollowQueueNode> queue = new LinkedList<>();
final Set<PyReferenceExpression> 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<PsiElement> assignedFromElements = context.maySwitchToAST(target)
@@ -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.
* <i>Note: the returned list could contain null values.</i>
*/
@NotNull
public static List<PsiElement> fullMultiResolveLocally(@NotNull PyReferenceExpression referenceExpression) {
return fullMultiResolveLocally(referenceExpression, new HashSet<>());
}
@NotNull
private static List<PsiElement> fullMultiResolveLocally(@NotNull PyReferenceExpression referenceExpression,
@NotNull Set<PyReferenceExpression> visited) {