[python] PY-81651 respect explicit Any on binary operators

Merge-request: IJ-MR-174645
Merged-by: Morgan Bartholomew <morgan.bartholomew@jetbrains.com>

GitOrigin-RevId: 9062f58b163d99f526f2bb0c3ebfa29039508f54
This commit is contained in:
Morgan Bartholomew
2025-09-05 11:56:19 +00:00
committed by intellij-monorepo-bot
parent 0fc7813dec
commit a13764a6b7
3 changed files with 26 additions and 9 deletions
@@ -68,15 +68,17 @@ public class PyBinaryExpressionImpl extends PyElementImpl implements PyBinaryExp
return PyBuiltinCache.getInstance(this).getBoolType();
}
PyType callResultType = PyCallExpressionHelper.getCallType(this, context, key);
if (callResultType != null) {
boolean bothOperandsAreKnown = operandIsKnown(getLeftExpression(), context) && operandIsKnown(getRightExpression(), context);
// TODO requires weak union. See PyTypeCheckerInspectionTest#testBinaryExpressionWithUnknownOperand
return bothOperandsAreKnown ? callResultType : PyUnionType.createWeakType(callResultType);
if (callResultType == null) {
if (referencedName != null && PyNames.COMPARISON_OPERATORS.contains(referencedName)) {
// we don't know if it was explicit or not, so we form an unsafe union of Any and bool
// TODO: when { explicit Any -> Any, Unknown -> UnsafeUnion[bool | Any] }
return PyUnsafeUnionType.unsafeUnion(null, PyBuiltinCache.getInstance(this).getBoolType());
}
return null;
}
if (referencedName != null && PyNames.COMPARISON_OPERATORS.contains(referencedName)) {
return PyBuiltinCache.getInstance(this).getBoolType();
}
return null;
boolean bothOperandsAreKnown = operandIsKnown(getLeftExpression(), context) && operandIsKnown(getRightExpression(), context);
// TODO requires weak union. See PyTypeCheckerInspectionTest#testBinaryExpressionWithUnknownOperand
return bothOperandsAreKnown ? callResultType : PyUnionType.createWeakType(callResultType);
}
private static boolean operandIsKnown(@Nullable PyExpression operand, @NotNull TypeEvalContext context) {
@@ -11,7 +11,7 @@ def test():
c = C()
f(<warning descr="Expected type 'str', got 'bool' instead">1 < 2</warning>)
f(<warning descr="Expected type 'str', got 'bool' instead">o == o</warning>)
f(<warning descr="Expected type 'str', got 'bool' instead">o >= o</warning>)
f(o >= o)
f(<warning descr="Expected type 'str', got 'bool' instead">'foo' > 'bar'</warning>)
f(<warning descr="Expected type 'str', got 'bool' instead"><warning descr="Expected type 'int', got 'C' instead">c</warning> < 1</warning>)
f(<warning descr="Expected type 'str', got 'List[Any]' instead">c > 1</warning>)
@@ -1,6 +1,7 @@
// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python;
import com.intellij.idea.TestFor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiFile;
import com.jetbrains.python.fixtures.PyTestCase;
@@ -3975,6 +3976,20 @@ public class Py3TypeTest extends PyTestCase {
""");
}
@TestFor(issues="PY-81651")
public void testEqWithAny() {
// the actual result is `Any`, but we don't have the technology yet
doTest("UnsafeUnion[Any, bool]", """
from typing import Any
class A:
def __eq__(self, other) -> Any:
return "hello :)"
expr = A() == 1
""");
}
private void doTest(final String expectedType, final String text) {
myFixture.configureByText(PythonFileType.INSTANCE, text);
final PyExpression expr = myFixture.findElementByText("expr", PyExpression.class);