From a13764a6b7bbe4bc6d7aac09023fa49c009fc221 Mon Sep 17 00:00:00 2001 From: Morgan Bartholomew Date: Fri, 5 Sep 2025 11:56:19 +0000 Subject: [PATCH] [python] PY-81651 respect explicit `Any` on binary operators Merge-request: IJ-MR-174645 Merged-by: Morgan Bartholomew GitOrigin-RevId: 9062f58b163d99f526f2bb0c3ebfa29039508f54 --- .../psi/impl/PyBinaryExpressionImpl.java | 18 ++++++++++-------- .../ComparisonOperators.py | 2 +- .../com/jetbrains/python/Py3TypeTest.java | 15 +++++++++++++++ 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyBinaryExpressionImpl.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyBinaryExpressionImpl.java index 5451d98720d9..09d53f29f2f5 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyBinaryExpressionImpl.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyBinaryExpressionImpl.java @@ -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) { diff --git a/python/testData/inspections/PyTypeCheckerInspection/ComparisonOperators.py b/python/testData/inspections/PyTypeCheckerInspection/ComparisonOperators.py index 3b745411b446..0f9b83515070 100644 --- a/python/testData/inspections/PyTypeCheckerInspection/ComparisonOperators.py +++ b/python/testData/inspections/PyTypeCheckerInspection/ComparisonOperators.py @@ -11,7 +11,7 @@ def test(): c = C() f(1 < 2) f(o == o) - f(o >= o) + f(o >= o) f('foo' > 'bar') f(c < 1) f(c > 1) diff --git a/python/testSrc/com/jetbrains/python/Py3TypeTest.java b/python/testSrc/com/jetbrains/python/Py3TypeTest.java index b512b53957a9..696c8396a105 100644 --- a/python/testSrc/com/jetbrains/python/Py3TypeTest.java +++ b/python/testSrc/com/jetbrains/python/Py3TypeTest.java @@ -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);