PY-88281 typing: partially unresolved union leads to complete Any

(cherry picked from commit 12afe1b92d81a439887680383ae0e4afbb8decb7)

GitOrigin-RevId: 6796d0fd9da021e9f66165021be3b29c508515d3
This commit is contained in:
Morgan Bartholomew
2026-03-23 04:40:25 +00:00
committed by intellij-monorepo-bot
parent 5d241e05ff
commit 069606d508
5 changed files with 77 additions and 41 deletions
@@ -1190,7 +1190,7 @@ class PyTypingTypeProvider : PyTypeProviderWithCustomContext<Context?>() {
}
private fun typeHasOverloadedBitwiseOr(
type: PyType, expression: PyExpression,
type: PyType, expression: PyExpression, operator: String = "__or__",
context: Context,
): Boolean {
if (type !is PyClassType) {
@@ -1202,7 +1202,7 @@ class PyTypingTypeProvider : PyTypeProviderWithCustomContext<Context?>() {
return false
}
val resolved = metaClassType
.resolveMember("__or__", expression, AccessDirection.READ, PyResolveContext.defaultContext(typeContext))
.resolveMember(operator, expression, AccessDirection.READ, PyResolveContext.defaultContext(typeContext))
if (resolved.isNullOrEmpty()) return false
return StreamEx.of(resolved)
@@ -1279,11 +1279,11 @@ class PyTypingTypeProvider : PyTypeProviderWithCustomContext<Context?>() {
if (neverType != null) {
return Ref(neverType)
}
val unionType: Ref<PyType?>? = getUnionType(resolved, context)
val unionType = getUnionType(resolved, context)
if (unionType != null) {
return unionType
}
val intersectionType: Ref<PyType?>? = getIntersectionType(resolved, context)
val intersectionType = getIntersectionType(resolved, context)
if (intersectionType != null) {
return intersectionType
}
@@ -1467,19 +1467,16 @@ class PyTypingTypeProvider : PyTypeProviderWithCustomContext<Context?>() {
}
private fun getIntersectionType(resolved: PsiElement, context: Context): Ref<PyType?>? {
if (resolved is PyBinaryExpression && resolved.operator === PyTokenTypes.AND) {
val left = resolved.leftExpression
val right = resolved.rightExpression
if (left == null || right == null) return null
if (resolved !is PyBinaryExpression || resolved.operator !== PyTokenTypes.AND) return null
val left = resolved.leftExpression ?: return null
val right = resolved.rightExpression ?: return null
val leftTypeRef: Ref<PyType?>? = getType(left, context)
val rightTypeRef: Ref<PyType?>? = getType(right, context)
if (leftTypeRef == null || rightTypeRef == null) return null
val leftTypeRef = getType(left, context)
val rightTypeRef = getType(right, context)
if (leftTypeRef == null && rightTypeRef == null) return null
val intersection = intersection(leftTypeRef.get(), rightTypeRef.get())
return if (intersection != null) Ref(intersection) else null
}
return null
val intersection = intersection(leftTypeRef?.get(), rightTypeRef?.get())
return intersection?.let { Ref(it) }
}
private fun getNoneType(typeHint: PyExpression, resolved: PsiElement): Ref<PyType?>? {
@@ -2095,18 +2092,20 @@ class PyTypingTypeProvider : PyTypeProviderWithCustomContext<Context?>() {
}
}
else if (element is PyBinaryExpression && element.operator === PyTokenTypes.OR) {
val left = element.leftExpression
val right = element.rightExpression
if (left == null || right == null) return null
val left = element.leftExpression ?: return null
val right = element.rightExpression ?: return null
val leftTypeRef: Ref<PyType?>? = getType(left, context)
val rightTypeRef: Ref<PyType?>? = getType(right, context)
if (leftTypeRef == null || rightTypeRef == null) return null
val leftTypeRef = getType(left, context)
val rightTypeRef = getType(right, context)
if (leftTypeRef == null && rightTypeRef == null) return null
val leftType = leftTypeRef.get()
if (leftType != null && typeHasOverloadedBitwiseOr(leftType, left, context)) return null
// if the class type defines __or__ then don't create a union
val leftType = leftTypeRef?.get()
if (leftType != null && typeHasOverloadedBitwiseOr(leftType, left, context = context)) return null
val rightType = rightTypeRef?.get()
if (rightType != null && typeHasOverloadedBitwiseOr(rightType, left, "__ror__", context)) return null
val union = PyUnionType.union(leftType, rightTypeRef.get())
val union = PyUnionType.union(leftType, rightType)
return if (union != null) Ref(union) else null
}
return null
@@ -920,6 +920,7 @@ public abstract class PyCompatibilityVisitor extends PyElementVisitor {
node, new ReplaceWithOldStyleUnionQuickFix(), new AddFromFutureImportAnnotationsQuickFix());
}
else {
if (node.getLeftExpression().getType(context) == null) return;
registerForAllMatchingVersions(level -> level.isOlderThan(LanguageLevel.PYTHON310),
PyPsiBundle.message("INSP.compatibility.new.union.syntax.not.available.in.earlier.version"),
node, new ReplaceWithOldStyleUnionQuickFix());
@@ -1,18 +1,4 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python;
import com.jetbrains.python.fixtures.PyTestCase;
@@ -119,6 +105,30 @@ public class Py3HighlightingTest extends PyTestCase {
doTestWithLanguageLevel(LanguageLevel.PYTHON39, false, false);
}
public void testNoErrorMetaClassOverloadBitwiseRorChain() {
runWithLanguageLevel(
LanguageLevel.PYTHON39,
() -> {
myFixture.configureByText(
"test.py", """
class M(type):
def __ror__(self, other):
return other
class A(metaclass=M):
...
class B(metaclass=M):
...
class C(metaclass=M):
...
print(A | B | C)
""");
myFixture.testHighlighting(false, false, true);
}
);
}
// PY-32067
public void testAwaitInNonAsyncFunction() {
doHighlightingQuickfixTest("Convert to async function");
@@ -5229,6 +5229,32 @@ public class Py3TypeTest extends PyTestCase {
""");
}
@TestFor(issues = "PY-88281")
public void testUnionPartialUnresolved() {
doTest("int | Any", """
expr: int | asdf
""");
}
@TestFor(issues = "PY-88281")
public void testIntersectionPartialUnresolved() {
doTest("int & Any", """
expr: int & asdf
""");
}
public void testRightHandOrClass() {
doTest("UnionType | type[str] | int", """
class M(type):
def __ror__(self, other: object) -> int:
return 1
class A(metaclass=M): ...
expr = str | A
""");
}
private void doTest(final String expectedType, final String text) {
myFixture.configureByText(PythonFileType.INSTANCE, text);
final PyExpression expr = myFixture.findElementByText("expr", PyExpression.class);
@@ -1,4 +1,4 @@
// Copyright 2000-2018 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.
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.inspections;
import com.jetbrains.python.fixtures.PyInspectionTestCase;
@@ -1549,7 +1549,7 @@ public class PyTypeHintsInspectionTest extends PyInspectionTestCase {
doTestByText("""
from typing import Self
something: <warning descr="Invalid type annotation"><warning descr="Cannot use 'Self' outside class">Self</warning> | None</warning> = None
something: <warning descr="Cannot use 'Self' outside class">Self</warning> | None = None
""");
}