PyBinaryExpression#getReceiver() is aware of chained comparisons

Otherwise, it might lead to some subtle problems when this method is
used for unification of generic types.

Also, PyPrefixExpression now extends PyCallSiteExpression since
technically it is and should have been marked this way long ago.
This commit is contained in:
Mikhail Golubev
2017-11-28 18:37:02 +03:00
parent 1542b02d60
commit 8eda017f0f
8 changed files with 81 additions and 51 deletions
@@ -16,34 +16,13 @@
package com.jetbrains.python.psi;
import com.intellij.psi.PsiElement;
import com.jetbrains.python.PyNames;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
/**
* @author yole
*/
public interface PyBinaryExpression extends PyQualifiedExpression, PyCallSiteExpression, PyReferenceOwner {
@Nullable
@Override
default PyExpression getReceiver(@Nullable PyCallable resolvedCallee) {
return isRightOperator(resolvedCallee) ? getRightExpression() : getLeftExpression();
}
@NotNull
@Override
default List<PyExpression> getArguments(@Nullable PyCallable resolvedCallee) {
return Collections.singletonList(isRightOperator(resolvedCallee) ? getLeftExpression() : getRightExpression());
}
default boolean isRightOperator(@Nullable PyCallable resolvedCallee) {
return resolvedCallee != null && PyNames.isRightOperatorName(getReferencedName(), resolvedCallee.getName());
}
PyExpression getLeftExpression();
@Nullable PyExpression getRightExpression();
@@ -57,4 +36,6 @@ public interface PyBinaryExpression extends PyQualifiedExpression, PyCallSiteExp
PyExpression getOppositeExpression(PyExpression expression)
throws IllegalArgumentException;
boolean isRightOperator(@Nullable PyCallable resolvedCallee);
}
@@ -27,6 +27,16 @@ import java.util.List;
*/
public interface PyCallSiteExpression extends PyExpression {
/**
* Returns an expression that is treated as a receiver for this explicit or implicit (read, operator) call.
* <p>
* For most operator expressions it returns the result of {@code getOperator()} since it naturally represents
* the object on which a special magic method is called. However for binary expressions that additionally
* can be reversible such as {@code __add__} and {@code __radd__} it also takes into account name of the
* actual callee method and chained comparisons order if any.
*
* @param resolvedCallee optional callee corresponding to the call. Without it the receiver is deduced purely syntactically.
*/
@Nullable
PyExpression getReceiver(@Nullable PyCallable resolvedCallee);
@@ -20,7 +20,7 @@ import org.jetbrains.annotations.Nullable;
/**
* @author yole
*/
public interface PyPrefixExpression extends PyQualifiedExpression, PyReferenceOwner {
public interface PyPrefixExpression extends PyQualifiedExpression, PyReferenceOwner, PyCallSiteExpression {
@Nullable
PyExpression getOperand();
PyElementType getOperator();
@@ -33,10 +33,9 @@ import com.jetbrains.python.psi.types.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import static com.jetbrains.python.psi.PyUtil.as;
/**
* @author yole
@@ -215,6 +214,34 @@ public class PyBinaryExpressionImpl extends PyElementImpl implements PyBinaryExp
return op != null ? op.getNode() : null;
}
@Nullable
@Override
public PyExpression getReceiver(@Nullable PyCallable resolvedCallee) {
return isRightOperator(resolvedCallee) ? getRightExpression() : getChainedComparisonAwareLeftExpression();
}
@NotNull
@Override
public List<PyExpression> getArguments(@Nullable PyCallable resolvedCallee) {
return Collections.singletonList(isRightOperator(resolvedCallee) ? getChainedComparisonAwareLeftExpression() : getRightExpression());
}
public boolean isRightOperator(@Nullable PyCallable resolvedCallee) {
return resolvedCallee != null && PyNames.isRightOperatorName(getReferencedName(), resolvedCallee.getName());
}
@Nullable
private PyExpression getChainedComparisonAwareLeftExpression() {
final PyExpression leftOperand = getLeftExpression();
if (PyTokenTypes.COMPARISON_OPERATIONS.contains(getOperator())) {
final PyBinaryExpression leftBinaryExpr = as(leftOperand, PyBinaryExpression.class);
if (leftBinaryExpr != null && PyTokenTypes.COMPARISON_OPERATIONS.contains(leftBinaryExpr.getOperator())) {
return leftBinaryExpr.getRightExpression();
}
}
return leftOperand;
}
private static boolean operandIsKnown(@Nullable PyExpression operand, @NotNull TypeEvalContext context) {
if (operand == null) return false;
@@ -20,6 +20,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
@@ -126,6 +127,18 @@ public class PyPrefixExpressionImpl extends PyElementImpl implements PyPrefixExp
return op != null ? op.getNode() : null;
}
@Nullable
@Override
public PyExpression getReceiver(@Nullable PyCallable resolvedCallee) {
return getOperand();
}
@NotNull
@Override
public List<PyExpression> getArguments(@Nullable PyCallable resolvedCallee) {
return Collections.emptyList();
}
@Nullable
private static Ref<PyType> getGeneratorReturnType(@Nullable PyType type) {
if (type instanceof PyClassLikeType && type instanceof PyCollectionType) {
@@ -18,7 +18,6 @@ package com.jetbrains.python.psi.impl.references;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.resolve.PyResolveContext;
import com.jetbrains.python.psi.resolve.RatedResolveResult;
@@ -33,8 +32,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static com.jetbrains.python.psi.PyUtil.as;
/**
* @author vlan
*/
@@ -92,31 +89,12 @@ public class PyOperatorReference extends PyReferenceImpl {
@Nullable
public PyExpression getReceiver() {
if (myElement instanceof PyBinaryExpression) {
return getBinaryOperatorReceiver((PyBinaryExpression)myElement);
}
else if (myElement instanceof PySubscriptionExpression) {
return ((PySubscriptionExpression)myElement).getOperand();
}
else if (myElement instanceof PyPrefixExpression) {
return ((PyPrefixExpression)myElement).getOperand();
if (myElement instanceof PyCallSiteExpression) {
return ((PyCallSiteExpression)myElement).getReceiver(null);
}
return null;
}
@Nullable
private static PyExpression getBinaryOperatorReceiver(PyBinaryExpression expr) {
final PyExpression leftOperand = expr.getLeftExpression();
// Chained comparisons
if (PyTokenTypes.COMPARISON_OPERATIONS.contains(expr.getOperator())) {
final PyBinaryExpression leftBinaryExpr = as(leftOperand, PyBinaryExpression.class);
if (leftBinaryExpr != null && PyTokenTypes.COMPARISON_OPERATIONS.contains(leftBinaryExpr.getOperator())) {
return leftBinaryExpr.getRightExpression();
}
}
return leftOperand;
}
@NotNull
private List<RatedResolveResult> resolveLeftAndRightOperators(@NotNull PyBinaryExpression expr, @Nullable String name) {
final List<RatedResolveResult> result = new ArrayList<>();
@@ -125,7 +103,7 @@ public class PyOperatorReference extends PyReferenceImpl {
typeEvalContext.trace("Trying to resolve left operator");
typeEvalContext.traceIndent();
try {
result.addAll(resolveMember(getBinaryOperatorReceiver(expr), name));
result.addAll(resolveMember(expr.getReceiver(null), name));
}
finally {
typeEvalContext.traceUnindent();
@@ -0,0 +1,14 @@
from typing import Generic, TypeVar
T = TypeVar('T')
class MyClass(Generic[T]):
def __init__(self, x: T):
pass
def __lt__(self, other: 'MyClass[T]'):
pass
x = MyClass(1) < MyClass(2) < <weak_warning descr="Expected type 'MyClass[int]' (matched generic type 'MyClass[T]'), got 'MyClass[str]' instead">MyClass('foo')</weak_warning>
@@ -273,4 +273,11 @@ public class Py3TypeCheckerInspectionTest extends PyInspectionTestCase {
public void testMatchingOpenFunctionCallTypesPy3() {
doMultiFileTest();
}
public void testChainedComparisonsGenericMatching() {
runWithLanguageLevel(LanguageLevel.PYTHON36, () -> {
myFixture.copyDirectoryToProject("typing/typing.py", TEST_DIRECTORY);
doTest();
});
}
}