mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-49165 Add tests for PyPatternInspection and related quickfixes
GitOrigin-RevId: 0e35bd5d50e2d6b544cbd4467d184b6be21b7574
This commit is contained in:
committed by
intellij-monorepo-bot
parent
6fd9133f29
commit
152f63401c
@@ -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
|
||||
+11
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user