diff --git a/python/src/com/jetbrains/python/psi/impl/PyBinaryExpressionImpl.java b/python/src/com/jetbrains/python/psi/impl/PyBinaryExpressionImpl.java index a400c5a3d5e3..30b0b885a57c 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyBinaryExpressionImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyBinaryExpressionImpl.java @@ -159,15 +159,9 @@ public class PyBinaryExpressionImpl extends PyElementImpl implements PyBinaryExp boolean matched = true; for (Map.Entry entry : result.getMappedParameters().entrySet()) { - final PyExpression argument = entry.getKey(); - final PyCallableParameter parameter = entry.getValue(); - if (parameter.isPositionalContainer() || parameter.isKeywordContainer()) { - continue; - } - final Map substitutions = new HashMap<>(); - final PyType parameterType = parameter.getType(context); - final PyType argumentType = context.getType(argument); - if (!PyTypeChecker.match(parameterType, argumentType, context, substitutions)) { + final PyType parameterType = entry.getValue().getArgumentType(context); + final PyType argumentType = context.getType(entry.getKey()); + if (!PyTypeChecker.match(parameterType, argumentType, context, new HashMap<>())) { matched = false; } } @@ -179,11 +173,11 @@ public class PyBinaryExpressionImpl extends PyElementImpl implements PyBinaryExp } } } - if (!matchedTypes.isEmpty()) { - return PyUnionType.union(matchedTypes); - } - if (!types.isEmpty()) { - return PyUnionType.union(types); + final boolean bothOperandsAreKnown = operandIsKnown(getLeftExpression(), context) && operandIsKnown(getRightExpression(), context); + final List resultTypes = !matchedTypes.isEmpty() ? matchedTypes : types; + if (!resultTypes.isEmpty()) { + final PyType result = PyUnionType.union(resultTypes); + return bothOperandsAreKnown ? result : PyUnionType.createWeakType(result); } } if (PyNames.COMPARISON_OPERATORS.contains(getReferencedName())) { @@ -223,6 +217,15 @@ public class PyBinaryExpressionImpl extends PyElementImpl implements PyBinaryExp return op != null ? op.getNode() : null; } + private static boolean operandIsKnown(@Nullable PyExpression operand, @NotNull TypeEvalContext context) { + if (operand == null) return false; + + final PyType operandType = context.getType(operand); + if (operandType instanceof PyStructuralType || PyTypeChecker.isUnknown(operandType, context)) return false; + + return true; + } + private static boolean isTrueDivEnabled(@NotNull PyElement anchor) { final PsiFile file = anchor.getContainingFile(); if (file instanceof PyFile) { diff --git a/python/testData/inspections/PyTypeCheckerInspection/BinaryExpressionWithUnknownOperand.py b/python/testData/inspections/PyTypeCheckerInspection/BinaryExpressionWithUnknownOperand.py new file mode 100644 index 000000000000..c690b938735b --- /dev/null +++ b/python/testData/inspections/PyTypeCheckerInspection/BinaryExpressionWithUnknownOperand.py @@ -0,0 +1,17 @@ +from typing import Any + + +def f(x: Any) -> str: + return x * 2 + + +def f(x: Any) -> str: + return 2 * x + + +def f(x) -> str: + return x * 2 + + +def f(x) -> str: + return 2 * x \ No newline at end of file diff --git a/python/testData/inspections/PyTypeCheckerInspection/MapReturnElementType.py b/python/testData/inspections/PyTypeCheckerInspection/MapReturnElementType.py index 32540fec5a80..fe2d2c35cff8 100644 --- a/python/testData/inspections/PyTypeCheckerInspection/MapReturnElementType.py +++ b/python/testData/inspections/PyTypeCheckerInspection/MapReturnElementType.py @@ -1,5 +1,5 @@ def test(): xs = map(lambda x: x + 1, [1, 2, 3]) - print('foo' + xs[0]) + print('foo' + xs[0]) ys = map(tuple, iter([1, 2, 3])) print(1 + ys[0], 'bar' + ys[1]) diff --git a/python/testSrc/com/jetbrains/python/PyTypeTest.java b/python/testSrc/com/jetbrains/python/PyTypeTest.java index 74734d6cbf95..ba611eb66edd 100644 --- a/python/testSrc/com/jetbrains/python/PyTypeTest.java +++ b/python/testSrc/com/jetbrains/python/PyTypeTest.java @@ -2237,6 +2237,32 @@ public class PyTypeTest extends PyTestCase { ); } + // PY-9662 + public void testBinaryExpressionWithUnknownOperand() { + runWithLanguageLevel( + LanguageLevel.PYTHON35, + () -> { + doTest("Union[int, Any]", + "from typing import Any\n" + + "x: Any\n" + + "expr = x * 2"); + + doTest("Union[int, Any]", + "from typing import Any\n" + + "x: Any\n" + + "expr = 2 * x"); + + doTest("Union[int, Any]", + "def f(x):\n" + + " expr = x * 2"); + + doTest("Union[int, Any]", + "def f(x):\n" + + " expr = 2 * x"); + } + ); + } + private static List getTypeEvalContexts(@NotNull PyExpression element) { return ImmutableList.of(TypeEvalContext.codeAnalysis(element.getProject(), element.getContainingFile()).withTracing(), TypeEvalContext.userInitiated(element.getProject(), element.getContainingFile()).withTracing()); diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java index 44f6f553c665..10d1c830cb98 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java @@ -442,6 +442,11 @@ public class PyTypeCheckerInspectionTest extends PyTestCase { doTest(); } + // PY-9662 + public void testBinaryExpressionWithUnknownOperand() { + runWithLanguageLevel(LanguageLevel.PYTHON35, this::doTest); + } + // PY-16066 public void testBasestringMatchesType() { doTest();