PY-86019 [python]: show error when class pattern uses non-class type

GitOrigin-RevId: 1e0d0926155459d3112bf921967a822073f327f8
This commit is contained in:
Aleksandr.Govenko
2026-01-08 18:07:12 +00:00
committed by intellij-monorepo-bot
parent 9beeab25eb
commit f2b7fcd315
3 changed files with 80 additions and 5 deletions
@@ -1373,6 +1373,7 @@ INSP.patterns.pattern.can.be.simplified=Pattern can be simplified
INSP.patterns.class.does.not.support.pattern.matching.with.positional.arguments=Class {0} does not support pattern matching with positional arguments
INSP.patterns.too.many.positional.patterns.expected=Too many positional patterns, expected {0}
INSP.patterns.attribute.already.specified.as.positional.pattern.at.position=Attribute ''{0}'' is already specified as positional pattern at position {1}
INSP.patterns.not.a.class=Class pattern requires a class, but ''{0}'' can be ''{1}''
QFIX.simplify.as.pattern=Simplify 'as' pattern
QFIX.NAME.remove.element=Remove element
QFIX.remove.pattern=Remove pattern
@@ -15,10 +15,7 @@ import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.impl.PyBuiltinCache
import com.jetbrains.python.psi.impl.PyClassPatternImpl
import com.jetbrains.python.psi.impl.PyPsiUtils
import com.jetbrains.python.psi.types.PyClassType
import com.jetbrains.python.psi.types.PyTupleType
import com.jetbrains.python.psi.types.PyTypeChecker
import com.jetbrains.python.psi.types.TypeEvalContext
import com.jetbrains.python.psi.types.*
class PyPatternInspection : PyInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
@@ -49,7 +46,20 @@ private class PyPatternInspectionVisitor(holder: ProblemsHolder, context: TypeEv
override fun visitPyClassPattern(node: PyClassPattern) {
val classType = myTypeEvalContext.getType(node.classNameReference) as? PyClassType ?: return
val type = myTypeEvalContext.getType(node.classNameReference)
val types = PyTypeUtil.toStream(type).toList()
if (types.isNotEmpty() && types.none { PyTypeChecker.isUnknown(it, myTypeEvalContext) }) {
val invalidTypes = types.filter { it !is PyClassType || !it.isDefinition }
if (invalidTypes.isNotEmpty()) {
val invalidTypesUnion = PyUnionType.union(invalidTypes)
val invalidTypeName = PythonDocumentationProvider.getTypeName(invalidTypesUnion, myTypeEvalContext)
holder.problem(node.classNameReference,
PyPsiBundle.message("INSP.patterns.not.a.class", node.classNameReference.text, invalidTypeName)).register()
return
}
}
val classType = type as? PyClassType ?: return
val pyClass = classType.pyClass
if (pyClass.name in PyClassPattern.SPECIAL_BUILTINS) return
@@ -323,6 +323,70 @@ def f(c):
""");
}
// PY-86019
public void testFunctionPattern() {
doTestByText("""
def f():
pass
match 1:
case <warning descr="Class pattern requires a class, but 'f' can be '() -> None'">f</warning>():
pass
""");
}
// PY-86019
public void testInstancePattern() {
doTestByText("""
x = 1
match 1:
case <warning descr="Class pattern requires a class, but 'x' can be 'int'">x</warning>():
pass
""");
}
// PY-86019
public void testUnionOfClassesPattern() {
doTestByText("""
class A: pass
class B: pass
def g(cond):
if cond:
c = A
else:
c = B
match 1:
case c():
pass
""");
}
// PY-86019
public void testUnionOfClassAndFunctionPattern() {
doTestByText("""
class A: pass
def f(): pass
def g(cond):
c = A if cond else f
match 1:
case <warning descr="Class pattern requires a class, but 'c' can be '() -> None'">c</warning>():
pass
""");
}
// PY-86019
public void testAnyPattern() {
doTestByText("""
from typing import Any
def g(c: Any):
match 1:
case c():
pass
""");
}
@NotNull
@Override
protected Class<? extends PyInspection> getInspectionClass() {