PY-49165 Add tests for PyPatternInspection and related quickfixes

GitOrigin-RevId: 0e35bd5d50e2d6b544cbd4467d184b6be21b7574
This commit is contained in:
Aleksandr.Govenko
2025-12-22 22:36:14 +00:00
committed by intellij-monorepo-bot
parent 6fd9133f29
commit 152f63401c
25 changed files with 360 additions and 26 deletions
@@ -1370,7 +1370,7 @@ INSP.NAME.patterns=Invalid pattern in pattern matching
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}1
INSP.patterns.attribute.already.specified.as.positional.pattern.at.position=Attribute ''{0}'' is already specified as positional pattern at position {1}
QFIX.simplify.as.pattern=Simplify 'as' pattern
QFIX.NAME.remove.list.member=Remove list member
QFIX.remove.pattern=Remove pattern
@@ -12,6 +12,7 @@ import com.intellij.psi.util.PsiTreeUtil
import com.jetbrains.python.PyNames
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.PyTokenTypes
import com.jetbrains.python.documentation.PythonDocumentationProvider
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.impl.PyBuiltinCache
import com.jetbrains.python.psi.impl.PyClassPatternImpl
@@ -83,7 +84,7 @@ private class PyPatternInspectionVisitor(holder: ProblemsHolder, context: TypeEv
val keywordName = (keywordPattern as PyKeywordPattern).keyword
val positionalIndex = positionalAttributeNames.indexOf(keywordName)
if (positionalIndex >= 0) {
holder.problem(keywordPattern, PyPsiBundle.message("INSP.patterns.attribute.already.specified.as.positional.pattern.at.position", keywordName, positionalIndex))
holder.problem(keywordPattern, PyPsiBundle.message("INSP.patterns.attribute.already.specified.as.positional.pattern.at.position", keywordName, positionalIndex + 1))
.fix(RemoveListMemberFix(keywordPattern))
.register()
}
@@ -96,12 +97,16 @@ private class PyPatternInspectionVisitor(holder: ProblemsHolder, context: TypeEv
?.findAssignedValue()
?.let { PyPsiUtils.flattenParens(it) } ?: return
val matchArgsType = myTypeEvalContext.getType(matchArgs)
val strType = PyBuiltinCache.getInstance(matchArgs).strType
val goodTuple = PyTupleType.createHomogeneous(matchArgs, strType)
val matchArgsType = myTypeEvalContext.getType(matchArgs) ?: return
val strType = PyBuiltinCache.getInstance(matchArgs).strType ?: return
val goodTuple = PyTupleType.createHomogeneous(matchArgs, strType) ?: return
if (PyTypeChecker.match(goodTuple, matchArgsType, myTypeEvalContext)) return
// __match_args__ must be a tuple[str, ...]
holder.problem(matchArgs, "__match_args__ must be a tuple[str, ...]").register()
holder.problem(matchArgs, PyPsiBundle.message(
"INSP.type.checker.expected.type.got.type.instead",
PythonDocumentationProvider.getTypeName(goodTuple, myTypeEvalContext),
PythonDocumentationProvider.getTypeName(matchArgsType, myTypeEvalContext))
).register()
}
}
@@ -167,7 +172,11 @@ class AddMatchArgsFix(element: PyClass) : PsiUpdateModCommandAction<PyClass>(ele
?.toList() ?: emptyList()
val generator = PyElementGenerator.getInstance(pyClass.project)
val matchArgsValue = "(" + positionalArgs.joinToString(", ") { "'$it'" } + ")"
val matchArgsValue = positionalArgs.joinToString(
prefix = "(",
postfix = if (positionalArgs.size != 1) ")" else ",)",
separator = ", "
) { "'$it'" }
val matchArgsAssignment = generator.createFromText(
LanguageLevel.forElement(pyClass),
PyAssignmentStatement::class.java,
@@ -137,15 +137,25 @@ public class PyClassPatternImpl extends PyElementImpl implements PyClassPattern,
}
public static @Nullable List<@NotNull String> getMatchArgs(@NotNull PyClassType type, @NotNull TypeEvalContext context) {
List<String> matchArgs = type.getPyClass().getOwnMatchArgs();
if (matchArgs == null) {
matchArgs = PyNamedTupleTypeProvider.Companion.getGeneratedMatchArgs(type, context);
}
if (matchArgs == null) {
matchArgs = PyDataclassTypeProvider.Companion.getGeneratedMatchArgs(type, context);
final PyClass cls = type.getPyClass();
// TODO: change to getMemberType, when PyLiteralType can be created without PyExpression
List<String> matchArgs = cls.getOwnMatchArgs();
if (matchArgs != null) return matchArgs;
matchArgs = PyNamedTupleTypeProvider.Companion.getGeneratedMatchArgs(type, context);
if (matchArgs != null) return matchArgs;
matchArgs = PyDataclassTypeProvider.Companion.getGeneratedMatchArgs(type, context);
if (matchArgs != null) return matchArgs;
for (PyClassLikeType baseType : type.getSuperClassTypes(context)) {
if (baseType instanceof PyClassType baseClassType) {
final List<String> inherited = getMatchArgs(baseClassType, context);
if (inherited != null) return inherited;
}
}
return matchArgs;
return null;
}
@Nullable
@@ -0,0 +1,9 @@
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def f(p):
match p:
case Point(<warning descr="Class Point does not support pattern matching with positional arguments"><caret>1</warning>, <warning descr="Class Point does not support pattern matching with positional arguments">2</warning>):
pass
@@ -0,0 +1,11 @@
class Point:
__match_args__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
def f(p):
match p:
case Point(1, 2):
pass
@@ -0,0 +1,9 @@
class C:
def __init__(self, a, b, c):
self.a = a
# b is not assigned to instance attribute
def f(obj):
match obj:
case C(<warning descr="Class C does not support pattern matching with positional arguments"><caret>1</warning>, <warning descr="Class C does not support pattern matching with positional arguments">2</warning>):
pass
@@ -0,0 +1,11 @@
class C:
__match_args__ = ('a',)
def __init__(self, a, b, c):
self.a = a
# b is not assigned to instance attribute
def f(obj):
match obj:
case C(1, 2):
pass
@@ -0,0 +1,7 @@
class Empty:
pass
def f(obj):
match obj:
case Empty(<warning descr="Class Empty does not support pattern matching with positional arguments"><caret>1</warning>):
pass
@@ -0,0 +1,8 @@
class Empty:
__match_args__ = ()
pass
def f(obj):
match obj:
case Empty(1):
pass
@@ -0,0 +1,8 @@
class OnlySelf:
def __init__(self):
pass
def f(obj):
match obj:
case OnlySelf(<warning descr="Class OnlySelf does not support pattern matching with positional arguments"><caret>1</warning>):
pass
@@ -0,0 +1,10 @@
class OnlySelf:
__match_args__ = ()
def __init__(self):
pass
def f(obj):
match obj:
case OnlySelf(1):
pass
@@ -0,0 +1,10 @@
class Point:
__match_args__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
def f(p):
match p:
case Point(0, <warning descr="Attribute 'x' is already specified as positional pattern at position 1"><caret>x=0</warning>):
pass
@@ -0,0 +1,10 @@
class Point:
__match_args__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
def f(p):
match p:
case Point(0):
pass
@@ -0,0 +1,10 @@
class Point:
__match_args__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
def f(p):
match p:
case Point(a, b, <warning descr="Too many positional patterns, expected 2"><caret>c</warning>):
pass
@@ -0,0 +1,10 @@
class Point:
__match_args__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
def f(p):
match p:
case Point(a, b):
pass
@@ -0,0 +1,7 @@
class Z:
__match_args__ = ()
def f(z):
match z:
case Z(<warning descr="Too many positional patterns, expected 0"><caret>1</warning>):
pass
@@ -0,0 +1,7 @@
class Z:
__match_args__ = ()
def f(z):
match z:
case Z():
pass
@@ -0,0 +1,5 @@
x = 42
match x:
case <weak_warning descr="Pattern can be simplified"><caret>int() as n</weak_warning>:
pass
@@ -0,0 +1,5 @@
x = 42
match x:
case int(n):
pass
@@ -0,0 +1,5 @@
x = []
match x:
case <weak_warning descr="Pattern can be simplified"><caret>list() as xs</weak_warning>:
pass
@@ -0,0 +1,5 @@
x = []
match x:
case list(xs):
pass
@@ -55,16 +55,7 @@ def f(p):
public void testInvalidMatchArgsType() {
doTestByText("""
class D:
__match_args__ = <warning descr="__match_args__ must be a tuple[str, ...]">42</warning>
""");
}
public void testUnknownAttributeInMatchArgs() {
doTestByText("""
class E:
def __init__(self):
self.x = 0
__match_args__ = ("x", <warning descr="String 'y' does not refer to any instance attribute">'y'</warning>)
__match_args__ = <warning descr="Expected type 'tuple[str, ...]', got 'int' instead">42</warning>
""");
}
@@ -140,7 +131,7 @@ class A:
def f(a):
match a:
case A(x, <warning descr=\"Too many positional patterns, expected 1\">y</warning>, <warning descr=\"Too many positional patterns, expected 1\">z</warning>):
case A(x, <warning descr="Too many positional patterns, expected 1">y</warning>, <warning descr="Too many positional patterns, expected 1">z</warning>):
pass
""");
}
@@ -150,7 +141,7 @@ def f(a):
class D:
def __init__(self):
self.x = 0
__match_args__ = ("x", 1)
__match_args__ = (<warning descr="Expected type 'tuple[str, ...]', got 'tuple[str, int]' instead">"x", 1</warning>)
""");
}
@@ -233,6 +224,105 @@ class E:
""");
}
public void testSimplifyAsPatternInt() {
doTestByText("""
x = 42
match x:
case <weak_warning descr="Pattern can be simplified">int() as n</weak_warning>:
pass
""");
}
public void testSimplifyAsPatternDict() {
doTestByText("""
x = {}
match x:
case <weak_warning descr="Pattern can be simplified">dict() as d</weak_warning>:
pass
""");
}
public void testSimplifyAsPatternWithArgumentsNoWarning() {
doTestByText("""
x = []
match x:
case list(1, 2) as xs:
pass
""");
}
public void testMatchArgsInvalidTypeList() {
doTestByText("""
class D:
__match_args__ = <warning descr="Expected type 'tuple[str, ...]', got 'list[str]' instead">["x", "y"]</warning>
""");
}
public void testMatchArgsInvalidTypeSet() {
doTestByText("""
class D:
__match_args__ = <warning descr="Expected type 'tuple[str, ...]', got 'set[str]' instead">{"x", "y"}</warning>
""");
}
public void testMatchArgsInvalidTypeNone() {
doTestByText("""
class D:
__match_args__ = <warning descr="Expected type 'tuple[str, ...]', got 'None' instead">None</warning>
""");
}
public void testMatchArgsInvalidTupleOfInts() {
doTestByText("""
class D:
__match_args__ = (<warning descr="Expected type 'tuple[str, ...]', got 'tuple[int, int, int]' instead">1, 2, 3</warning>)
""");
}
public void testInheritedMatchArgsFromBase() {
doTestByText("""
class Base:
__match_args__ = ("x",)
def __init__(self, x):
self.x = x
class Derived(Base):
pass
def f(d):
match d:
case Derived(1):
pass
""");
}
public void testClassWithoutInitNoWarning() {
doTestByText("""
class C:
pass
def f(c):
match c:
case C(a=1):
pass
""");
}
public void testEmptyClassWithPositionalPatternWarning() {
doTestByText("""
class C:
pass
def f(c):
match c:
case C(<warning descr="Class C does not support pattern matching with positional arguments">1</warning>):
pass
""");
}
@NotNull
@Override
protected Class<? extends PyInspection> getInspectionClass() {
@@ -0,0 +1,26 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.quickFixes
import com.intellij.testFramework.TestDataPath
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.PyQuickFixTestCase
import com.jetbrains.python.inspections.PyPatternInspection
@TestDataPath("\$CONTENT_ROOT/../testData/quickFixes/PyAddMatchArgsFixTest")
class PyAddMatchArgsFixTest : PyQuickFixTestCase() {
fun testAddMatchArgsBasic() {
doQuickFixTest(PyPatternInspection::class.java, PyPsiBundle.message("QFIX.add.match.args.to.class", "Point"))
}
fun testAddMatchArgsFilterAttributes() {
doQuickFixTest(PyPatternInspection::class.java, PyPsiBundle.message("QFIX.add.match.args.to.class", "C"))
}
fun testAddMatchArgsNoInit() {
doQuickFixTest(PyPatternInspection::class.java, PyPsiBundle.message("QFIX.add.match.args.to.class", "Empty"))
}
fun testAddMatchArgsOnlySelf() {
doQuickFixTest(PyPatternInspection::class.java, PyPsiBundle.message("QFIX.add.match.args.to.class", "OnlySelf"))
}
}
@@ -0,0 +1,22 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.quickFixes
import com.intellij.testFramework.TestDataPath
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.PyQuickFixTestCase
import com.jetbrains.python.inspections.PyPatternInspection
@TestDataPath($$"$CONTENT_ROOT/../testData/quickFixes/PyRemovePatternFixTest")
class PyRemovePatternFixTest : PyQuickFixTestCase() {
fun testRemoveLastExtraPositional() {
doQuickFixTest(PyPatternInspection::class.java, PyPsiBundle.message("QFIX.remove.pattern"))
}
fun testRemoveConflictingKeyword() {
doQuickFixTest(PyPatternInspection::class.java, PyPsiBundle.message("QFIX.remove.pattern"))
}
fun testRemoveSingleExtraPositional() {
doQuickFixTest(PyPatternInspection::class.java, PyPsiBundle.message("QFIX.remove.pattern"))
}
}
@@ -0,0 +1,30 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.quickFixes
import com.intellij.testFramework.TestDataPath
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.PyQuickFixTestCase
import com.jetbrains.python.inspections.PyPatternInspection
@TestDataPath("\$CONTENT_ROOT/../testData/quickFixes/PySimplifyAsPatternFixTest")
class PySimplifyAsPatternFixTest : PyQuickFixTestCase() {
fun testSimplifyListAsPattern() {
doQuickFixTest(PyPsiBundle.message("QFIX.simplify.as.pattern"))
}
fun testSimplifyIntAsPattern() {
doQuickFixTest(PyPsiBundle.message("QFIX.simplify.as.pattern"))
}
// Overriden to enable weak warnings
override fun doQuickFixTest(hint: String) {
val testFileName = getTestName(true)
myFixture.enableInspections(PyPatternInspection::class.java)
myFixture.configureByFile("$testFileName.py")
myFixture.checkHighlighting(false, false, true)
val intentionAction = myFixture.findSingleIntention(hint)
assertNotNull(intentionAction)
myFixture.launchAction(intentionAction)
myFixture.checkResultByFile(testFileName + "_after.py", true)
}
}